Component framework editable grid in PowerApps Canvas app

Last time I wrote about PowerApps component framework and the cool things you can build with it. This time I will show step by step guide how to build and deploy editable grid code component and use it in canvas app.

Install required tools

Component framework uses a bunch of required tools to be able to create and build a custom component.

Go to MSDN documentation Install PowerApps CLI and install all from that list:

  • NodeJS with npm - for building React code.
  • .NET Framework 4.6.2 Developer Pack and .NET Core 3.1 SDK - for building solution project.
  • Visual Studio Code - as main development IDE.
  • PowerApps CLI - for generating project and building solution package.

Building code component

Start by opening Visual Studio code, then open a folder where you will create component.

If terminal does not show up, click Ctrl+` . So you have clear beginning that looks like this:

Start by writing this initialization command in terminal:

pac pcf init --namespace mcw --name EditableGrid --template field

When this command finishes, write this command to download all required nmp packages:

npm install

This command may take few minutes and produce several warnings, but this is normal.

To verify if all works you can already start the component framework test environment by writing this to terminal:

npm start watch

It will build code and start sandbox website to help build your code component:

To stop code watcher, press CTRL+C in terminal, then type Y and press Enter.

Code component structure

Let's review what files were generated and what they mean:

  • ManifestTypes.d.ts - auto generated types file for input and output properties types. It is automatically generated when you change manifest file.
  • ControlManifest.Input.xml - definition schema for code component, its properties and resource files.
  • index.ts - main Typescript file that is called by component framework.
  • node_modules - folder containing all npm packages we installed.
  • out - generated output files will be stored here.
  • .gitignore - git ignore configuration file, used when committing code to git, it helps git filter out files you do not need to commit, such as npm packages or generated files.
  • editable-grid.pcfproj - component framework project file.
  • package.json - npm configuration file, that stores used npm packages references.
  • package-lock.json - npm configuration file, that stores exact references to each npm package and subpackages, so that other developers could restore to exact same packages versions.
  • pcfconfig.json - component framework configuration file, that specifies output directory.
  • tsconfig.json - Typescript configuration file.

One of the most important file is index.ts, check my comments on methods:

    /*
       Method called when code control is added.
       It passes:
       - context, that gives access to properties, data and utilities.
       - notifyOutputChanged callback method, that allows to notify framework when component output is changed.
       - state dictionary, that allows to store component state.
       - container element, which gives us a place to store HTML content.
    */
    public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement)
    {
        // ...
    }

    /*
       Method called on any render or state changes during framework lifecycle.
       This is a place to render our component.
    */
    public updateView(context: ComponentFramework.Context<IInputs>): void
    {
        // ...
    }

    /*
        Method that returns component output when framework asks for it.
    */
    public getOutputs(): IOutputs
    {
        return {};
    }

    /*
        Method that is used to clear anything you have created when a component is deleted.
    */
    public destroy(): void
    {
       // ...
    }

Implement Editable Grid

To implement anything like that I always choose FluentUI components as they are easy to use, great design and fully customizable.

We also need ReactJS library as I always prefer using it instead of plain JavaScript or other kind of JS library (Microsoft prefer same more and more...).

Write this in terminal to install all them:

npm install @fluentui/react react react-dom

then

npm install @types/react @types/react-dom --save-dev

When both are done, we can add some code.

It is always a good practice to separate components and other type of files. Let's create components folder under EditableGrid folder and then create App.tsx inside it.

Copy this editable grid code to it:

import * as React from "react";
import { IInputs, IOutputs } from "../generated/ManifestTypes";
import { setIconOptions } from "office-ui-fabric-react/lib/Styling";
import { initializeIcons } from '@uifabric/icons';
import { DetailsList, TextField, IColumn, SelectionMode, IconButton } from '@fluentui/react';

initializeIcons();
setIconOptions({
    disableWarnings: true,
});

type AppProps = {
    context: ComponentFramework.Context<IInputs>;
    onChange: (outputs: IOutputs) => void;
}

const App: React.FC<AppProps> = (props) => {

    const { context, onChange } = props;
    const jsonData = JSON.parse(context.parameters.JSONdata.raw || '[]') as any[];

    let [ items, setItems ] = React.useState(jsonData);
    let columns: IColumn[] = (context.parameters.Columns.raw || '').split(',').map(columnDef => {
        let columnDefSplit = columnDef.split(':');
        let columnName = columnDefSplit[0];
        let width = columnDef.length > 0 ? +columnDefSplit[1] : undefined;
        return {
            key: columnName,
            name: columnName,
            fieldName: columnName,
            maxWidth: width,
            minWidth: width
        } as IColumn;
    });

    // Add delete row button
    columns.push({
        key: 'delete-row-btn',
        name: '',
        fieldName: '',
        maxWidth: 50,
        minWidth: 50,
        onRender: (item?: any, index?: number, column?: IColumn): any => {
            return (
                <IconButton 
                    iconProps=
                    onClick={() => {
                        if (index != undefined) {
                            let rowIndex = items.indexOf(item);
                            let data = items.slice(0, rowIndex).concat(items.slice(rowIndex + 1));
                            setItems(data);
                            onChange({
                                JSONdata: JSON.stringify(data),
                                InnerHeight: context.parameters.InnerHeight.raw || ''
                            });
                        }
                    }}
                />
            );
        }
    } as IColumn);

    // if last line is empty - push one for new lines creation
    if (items.length === 0 || JSON.stringify(items[items.length - 1]) !== '{}') {
        let data = items.slice();
        data.push({});
        setItems(data);
    }

    // if height not specify yet - do it
    if (isNaN(parseInt(context.parameters.InnerHeight.raw || ''))) {
        onChange({
            JSONdata: JSON.stringify(jsonData),
            InnerHeight: context.parameters.InnerHeight.raw || ''
        });
    }
    
    const onValueChange = (index: number, fieldName: string, newValue: any): void => {
        let data = items.slice();
        data[index][fieldName] = newValue;
        setItems(data);
        onChange({
            JSONdata: JSON.stringify(data),
            InnerHeight: context.parameters.InnerHeight.raw || ''
        });
    }

    const renderColumn = (item?: any, index?: number, column?: IColumn): React.ReactNode => {

        if (index === undefined || !column || !column.fieldName) {
            return null;
        }

        let value = item[column.fieldName];

        if (typeof value === 'string' || typeof value === 'undefined') {
            return (
                <TextField
                    value={value || ''}
                    onChange={(event, newValue?: string) => onValueChange(index, column.fieldName as string, newValue)}
                    borderless={true}
                    tabIndex={index * 100}
                    />
            )
        }

        return value;
    }
    

    return (
        <div style=>
            <DetailsList
                columns={columns}
                selectionMode={SelectionMode.none}
                compact={true}
                items={items}
                getKey={(item: any, index?: number) => index?.toString() || ''}
                onRenderItemColumn={renderColumn}
            />
        </div>
    );
}

export default App;

This is a demo code that adds DetailsList FluentUI component with custom column render method to add TextField in each row column.

Then lets add required properties for own component.

Open ControlManifest.Input.xml and add these under control element:

    <property name="Columns" display-name-key="Columns" description-key="Columns" of-type="SingleLine.TextArea" usage="input" required="true" />
    <property name="JSONdata" display-name-key="JSONdata" description-key="JSONdata" of-type="SingleLine.TextArea" usage="bound" required="true" />
    <property name="InnerHeight" display-name-key="InnerHeight" description-key="InnerHeight" of-type="SingleLine.TextArea" usage="bound" required="true" />

Then remove sampleProperty.

Last part is to combine index file with App component. Open index.ts and replace its content with this code:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as ReactDOM from "react-dom";
import * as React from "react";
import App from "./components/App";

export class EditableGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {

	private theContainer: HTMLDivElement;
	private propsState: IOutputs;
	private _notifyOutputChanged: () => void;

	/**
	 * Empty constructor.
	 */
	constructor() {

	}

	/**
	 * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
	 * Data-set values are not initialized here, use updateView.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
	 * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
	 * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
	 * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
	 */
	public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {
		this.theContainer = container;
		this.propsState = {
			JSONdata: context.parameters.JSONdata.raw || '',
			InnerHeight: container.clientHeight.toString()
		} as IOutputs;
		this._notifyOutputChanged = notifyOutputChanged;
	}

	/**
	 * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
	 */
	public updateView(context: ComponentFramework.Context<IInputs>): void {
		ReactDOM.render(
			React.createElement(
				App,
				{
					context: context,
					onChange: (outputs) => {
						this.propsState = outputs;
						this.propsState.InnerHeight = this.theContainer.clientHeight.toString();
						this._notifyOutputChanged();
					}
				}
			),
			this.theContainer
		);
	}

	/** 
	 * It is called by the framework prior to a control receiving new data. 
	 * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
	 */
	public getOutputs(): IOutputs {
		return this.propsState;
	}

	/** 
	 * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
	 * i.e. cancelling any pending remote calls, removing listeners, etc.
	 */
	public destroy(): void {
		ReactDOM.unmountComponentAtNode(this.theContainer);
	}
}

Then write npm start watch in terminal again. Code will be compiled and sandbox will start again.

Under Data Inputs put these values:

  • Columns: Title,Message
  • JSONdata: []

It will render editable grid like this:

Build PCF solution

When we ready with our component, we can generate solution and deploy it.

Create new folder in root folder named EditableGridSolution. Stop watcher and write cd .\EditableGridSolution in terminal to navigate it to newly created folder.

Then write this to terminal to generate solution project (replace publisher to what you prefer):

pac solution init --publisher-name Macaw --publisher-prefix mcw

Then write this to terminal to link solution to code component (mind the path to root folder of the component):

pac solution add-reference --path C:\_dev\ignas\editable-grid

Then write this to terminal to restore and build solution:

msbuild /t:build /restore

This command will take time, but when finishes will generate a solution zip file under EditableGridSolution\bin\Debug\EditableGridSolution.zip

Deploy solution to your environment

After we finished creating our solution, it is time to deploy it to a canvas app.

Open CDS solutions page in web browser:

Click Import button. Then Choose solution and click Next. Later click Publish customizations:

If it fails to import, you may need to increase attachments size, check my previous post about it here (section Artifact size limit): Component framework in PowerApps

Using custom code component in Canvas App

Finally, last part is to use your custom component in Canvas App.

Open PowerApps Studio and select New Blank App with Tablet layout:

When studio loads and you see nice blank screen to work with, select Insert tab, then expand Custom menu button and select Import component:

In the opened panel, select Code tab, it will load for a bit and you will then see your custom component here. Select it and click Import:

After doing that, custom code component will appear under new group Code components in all components insertion window. Drag it to the screen, select Advanced properties window and put sample properties for component to start working:

  • Columns - "Title,Description"
  • JSONdata - "[]"

When you fill in custom properties, component will render itself automatically to our editable table. Run the app (click F5) and start adding some lines:

Notice that this component has these cool features:

  • Always has an empty row for new line creation
  • Automatically expends down and you can map its InnerHeight property to Height to automatically set height to custom table height, making canvas apps part increase it's height too, when new rows are added or deleted.
  • JSONdata is a bind property, meaning it accepts values and returns changed values too. Use it how you want (i.e. save to SharePoint item text column).
  • Columns property accepts column names separated by comma, but also accepts column width. When you enter it like this: "Title,Description:200", Description column width will be set to 200. By default all columns trying to stretch to full width (but that does not always work nice in canvas app...).

Full code in Git

This is it!

Hope this will be useful, as it was fun writing for me. Full code is accessible in Git over here: PCF Editable Grid

Feel free to use it anyway you want.