We’ve saw the Power Apps grid control (Preview) in the latest Wave Announcements. Each time we hear about great improvements. In Release Wave 1/2022 we’ve got inline-editing and infinite scrolling. But as we’ve tried it out, we saw the last parameter which sounded really promising:

The description of the parameter was really exciting:

Full logical name of the control (…) that provides custom cell renders and/or editors for the Power Apps grid control

The description was confirmed in the Release Wave 2/2022 :

Customizable cells: If the default visualization for the cells in one or more columns doesn’t meet your needs, makers can create custom cell renderers and editors to modify how cells look when showing data and when users are editing data.

Since yesterday we can find in the docs how the control works. And there is also an example in the github Power-Apps-Samples. Of course I had to try it out :-). Please remember that this is a preview, and shouldn’t be used in production for now.

Why is this control better than a dataset PCF?

Of course, we were able to make a dataset PCF before too. And there are a lot of cases where a Dataset PCF is still the best choice. But if we only want to change one or two columns, the implementation effort is pretty big. I’ve wrote several blogs about the difficulties of implementing a dataset PCF. And even after solving a lot of problems, there are some features (like implementing the column filter) where the effort is just too big compared with the advantage of having just one column rendered different.

The Power Apps Grid Control offers us a light-weight way, to have the best of both worlds.

Here is the example of the implementation of ColorfulOptionset Grid using a Power Apps Grid control: full editable and having all the goodies

For a short demo, have a look to this video:

How it renders?

First the whole Power Apps Grid will be rendered. Including the columns where you’ve defined your own renderer. After that the “init” method of your PCF will be called, and then your control renderer will be applied/shown. Usually it doesn’t take that long until the final rendering is done, but sometimes it could be a little delay there, so the user might see that change too.

The complete code for this examples is available on my GitHub: https://github.com/brasov2de/GridCustomizerControl/tree/main/CellRenderer

Is this control a PCF?

Yes, kind of. The Power Apps Grid Control is developed and deployed inside a PCF. Which is great because we can (re)use the same knowledge we have already. By using the PCF we can deploy code components, use also external libraries which are bundled together with the control, deploy CSS, declare the feature-usage like webAPI, utilities or some device features.

If the PCF is of type “virtual”, we can also use of the Platform own React and FluentUI.

But it is a special PCF!

Let’s see how it’s different:

The manifest

The Power Apps Grid Control manifest doesn’t look different that a classic PCF. Except that there is only one property: “EventName”. We cannot define anything in the customizing; this property is used by the platform to pass an event name where we can attach to.


Index.ts

Inside the index.ts we have a “ComponentFramework.ReactControl” with the known methods: init, updateView, getOutputs, destroy. But we actually use only the “init”.

The updateView is called too, but the ReactElement we return there is rendered hidden somewhere in the DOM. We won’t get to see it. So it’s enough to return there an empty react fragment:

    public updateView(context: ComponentFramework.Context): React.ReactElement {
        return React.createElement(React.Fragment);
    }

The getOutputs won’t be used nether. This is not the way we communicate with the platform, it’s not the way we send the changes.

This PCF is not a control that will be rendered in each cell of the grid. We can register only one control per Power Apps Grid, and I see it more like a wrapper for the cell renderer.

The magic happens inside the “init” method. There we can fire an event, which will be process by the Grid. Here is where the “EventName” comes into play. The platform tells us this way, which event should be triggered.

public init(
     context: ComponentFramework.Context,
     notifyOutputChanged: () => void,
      state: ComponentFramework.Dictionary
 ): void {
     const eventName = context.parameters.EventName.raw;
     if (eventName) {
         //this describes the cell renderer for read-only or editable mode.
         const paOneGridCustomizer: PAOneGridCustomizer = { 
                     cellRendererOverrides, 
                     cellEditorOverrides 
          };
         //seems that the context.factory has a "fireEvent" method 
         //which is not documented yet
         (context as any).factory.fireEvent(eventName, paOneGridCustomizer);
     }
 }

The EventName will be different each time:

The cell renderer

The TypeScript types are provided in the template from the Power Apps Samples

/**
 * Provide cell renderer overrides per column data type.
 */
export type CellRendererOverrides = {
  [dataType in ColumnDataType]?: 
     (props: CellRendererProps, rendererParams: GetRendererParams) 
        => React.ReactElement | null | undefined;
};

An example for a cell renderer could look like this:

//for each date type we can provide a renderer
const cellRendererOverrides: CellRendererOverrides = {
   ["Text"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {          
      //if it returns null, the default Power Apps Grid renderer will be used  
      return null;
    },
    ["OptionSet"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {    
       //now all OptionSet columns will be rendered with Icons and labels                
       return (
{props.formattedValue}
) }, ["TwoOptions"]: (props: CellRendererProps, rendererParams: GetRendererParams) => { const column = rendererParams.colDefs[rendererParams.columnIndex]; if(column.name==="diana_ishappy"){ //in this case I want to change only a specific column const smiley = props.value === "1" ? "Emoji2" : "Sad"; const label = props.formattedValue; return
} } }

When we implement a cell renderer, the props contains information about the cell data, while the rendererParams information about the columns/rowData. Here is an extract

//props 
{
   columnDataType: "TwoOptions",
   columnEditable: true,
   value: "1",
   formattedValue: "Happy", 
   rowHeight: 42
}
//rendererParams
{
   colDefs: [
      {
         name: "diana_ishappy",
         dataType: "TwoOptions",
         displayName: "Is Happy", 
         editable: true,
         isPrimary: false,
         isRequired: false,
         sortDirection: -1 , 
         width : 100     
      }, 
     {
         name: "diana_technologycode",
         displayName: "Technology Code", 
         editable: true,
         isPrimary: false,
         isRequired: false,
         sortDirection: -1, 
         width:353
      }, 
     ....
   ], 
   rowData: {
      __rec_id: "3cb517f8-2960-ec11-8f8f-000d3aa823b8",
      //... the other attributes
   }
}

The “rowData” provides the id (“__rec_id”). I’m not so sure about the format of the other values. Booleans are “1”, optionsetcode is as string, but multioptionset is an array of numbers. But it’s a preview, so I’m sure if will get better.

The cell editor

The types for a cell editor looks like this

/**
 * Provide cell editor overrides per column data type.
 */
export type CellEditorOverrides = {
  [dataType in ColumnDataType]?: 
    (defaultProps: CellEditorProps, rendererParams: GetEditorParams)
    => React.ReactElement | null | undefined;
};

The defaultProps has less data that the cell renderer. For instance there is no “formattedValue”

The rendererParams are similar to the ones from the cell renderer: has colDefs, rowData, and a few more.

When is the editor triggered

In my test, the cell editor was triggered if I didn’t have a cell renderer for that data type. For instance, for “Text” my cell renderer returned null. In that case, the cell editor was called automatically when I’ve clicked on a text cell and entered the edit mode.

For OptionSet or Booleans, I had already an own cell renderer, and the cell editor was not called automatically. In order to make it work, I had to use the props provided for the cell renderer: there is a “startEditing” method, which activated my cell editor code.

 ["OptionSet"]: 
(props: CellRendererProps, rendererParams: GetRendererParams) => {            
    const onCellClicked = (event?: React.MouseEvent | MouseEvent) => {
       if(props.startEditing) props.startEditing();               
    } 
    return (
{props.formattedValue}
) }

Implementing the editor

The implementation of the editor is a little challenging. We don’t get the formattedValues in the defaultProps. We also need to show all the options in the DropDown. I’ve found them only in a property which not documented in the types: customizerParams.

When the value changes, we need to notify the Grid. We can do that with a method from the rendererParams: onCellValueChanged.

If we want to leave the edit mode after a value was changed, we have also a “stopEditing” method. This one will set the Grid row in a “non-edit” mode.

Here is the code for my ColorfulOptionSet editor:

 ["OptionSet"]: (defaultProps: CellEditorProps, rendererParams: GetEditorParams) => {
      const defaultValue = defaultProps.value as number;
      
      //get the cell and the options
      const cell = rendererParams.colDefs[rendererParams.columnIndex];  
      const options =  (cell as any).customizerParams.dropDownOptions.map((option:any) => { 
        return {
          ...option, data: {color: colors[option.key]}
          }
        });
      //when the value changes, we notify the Power Apps Grid 
      //by using "onCellValueChanged"
      const onChange=(value: number | null) =>{
        rendererParams.onCellValueChanged(value);      
        //rendererParams.stopEditing(false);
      }
  
      //I've created a react component ColorfulDropDown in a separate tsx file. 
     // You can find it in my repository
      return 
    
    }

A similar challenge was to get the labels for the Boolean control; I’ve found them only inside the not documented “customizerParams”:

 const column = rendererParams.colDefs[rendererParams.columnIndex];
 const value  = defaultProps.value as string === "1" ? false : true; 
 const label = value === true 
              ? (column as any).customizerParams.labels.onText 
              : (column as any).customizerParams.labels.offText;

Recap for cell renderer and editor

  • inside the renderer: (props: CellRendererProps, rendererParams: GetRendererParams) =>
    • props.value & props.formattedValue
    • props.startEditing()
  • inside the editor: (defaultProps: CellEditorProps, rendererParams: GetEditorParams) =>
    • rendererParams
    • rendererParams.onCellValueChanged(newValue);
    • rendererParams.stopEditing(cancel?: boolean)
  • in both renderer and editor
    • column = rendererParams.colDefs[rendererParams.columnIndex]
    • data = rendererParams.rowData

With these ones I could make my control work. It doesn’t mean these are the best. Maybe I should add defaultProps.onChange method (but somehow it didn’t work properly for me). And an interesting CellRendererProps.onCellClicked (found in the types.ts but not in my environment). Maybe we’ll find out later if there is a better way. Let’s see.

When you need extended data

[Edit] I’ve realized later, that the colors for the OptionSets were actually delivered by the platform, if I would have configured the Power Apps Grid to retrieve the colors (an option in the cvustomizations of the Power Apps Grid). But the colors for Booleans are not retrieved. So I still let this part of the blog here, because it could be useful for booleans or other kind of metadata needed.

As I’ve mentioned, I’ve found the options for an OptionSet inside the column.customizerParams”. Unfortunately, even there the color for the Options was missing

To solve that problem, I’ve retrieved the metadata first, and when the Promise was resolved, I’ve defined the renderer and the editors inside a closure which had access to my colors. The changed code for my “init” method:

context.utils.getEntityMetadata("diana_pcftester", ["diana_technologycode"]).then((metadata)=>{
   const options = metadata.Attributes.get("diana_technologycode")?.attributeDescriptor.OptionSet as Array|null|undefined;          
   const colors = options?.reduce((prev, current)=>{
      return {...prev, [current.Value] : current.Color}
    }, {}) ?? {};          
    if (eventName) {
       const paOneGridCustomizer: PAOneGridCustomizer = { 
          cellRendererOverrides: generateCellRendererOverrides(colors), 
          cellEditorOverrides : generateCellEditorOverrides(colors)
       };
       (context as any).factory.fireEvent(eventName, paOneGridCustomizer); 
    }                
 })           
//for instance the generateCellRenderer looks like this
export const generateCellEditorOverrides = (colors : any)=>{
   const cellEditorOverrides= {....}
    return cellEditorOverrides;
}
   

Final thoughts

…this is huge! A game changer

I’m sure this is just the beginning. There are a lot of aspects I didn’t covered. But even so we can see that the value added to the mode-driven apps is huge. I’m looking forward to find how much more is possible to archieve.

…but I’m hoping for some improvements

How generic are these controls? When we develop PCFs, we are used to think generic; they have parameters which can be defined in customizing. The Power Apps Grid control though, has no way to define any kind of customization. If I want to show an icon in a control, I need to hard-code it. If the same control can be used somewhere else, but with another icon, I need to make another control.

I’m sure there are ways to archieve some generic levels. For instance by defining a EnvironmentVariable, which can be read by the control. Somehow the control must know which configuration should be used. I wish there was another parameter to the Power Apps Grid, where we can pass a JSON configuration to the control.

Another issue might be the idea of having only one control with all the renderers for all cells. If one implements a control for a special case of optionset and another one for Boolean, you might have to bundle them in one control. It depends on which view is applied. Maybe is a good idea to create a renderer controls which knows

But this control is not only a use-case for special editors. It could be a great way to implement some kind of calculated columns, which doesn’t have to be saved in the Dataverse. That case doesn’t need to be generic.

Looking forward to hear about your use-cases.

Advertisement