Power Apps Grid: Cell Renderer Dependency – Trigger Your Own Events

When we use Power Apps Grid customizer control, there could be cases when the rendering depends on the other cells. We cannot control when the cell rendering happens, and the Power Apps Grid doesn’t always rerender the cells when the data wasn’t changed (or doesn’t provide the updated data). In this blog I’m talking about how to work arround this issue, and create your own events for cell renderer.

The use case

This blog applies to all dependency cases, but it’s easier to explain looking to a specific use-case. Do you remember the example from the docs, which shows the CreditLimit in red or blue, depending on the amount value?

Sample of Power Apps Grid customizer control from the docs

Now let’s create a similar control, where the colors depend on another cell. Let’s consider a table Consumption“. The “Amount” column should be shown in different colors, depending on a column “Plan” (of type Choice/OptionSet). An example:

  • For “Plan A”, it should turn orange, when the “Amount” is over 10.000. (otherwise green)
  • For “Plan B” it should turn red when the Amount is over 100.000 (it’s a more expensive plan)
  • For “Plan C” it should turn “darkred” when the Amount is over 500.000

Or if you prefer the definition as code:

const rules = {
    //Plan A
    "341560000" : (value: number) => value > 10000 ? "orange" : "green",
    //Plan B
    "341560001" : (value: number) => value > 100000 ? "red" : "green",
   //Plan C
    "341560001" : (value: number) => value > 500000 ? "darkred" : "green
}

The problem

The code is pretty simple, and similar with the example from the docs (which can be found here). I just have to check the plan code first. Here is my code:

 {
  ["Integer"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {
      const {columnIndex, colDefs, rowData } = rendererParams;  
      const columnName = colDefs[columnIndex].name;             
      if(columnName==="diana_amount") {
          const plan = (rowData as any)["diana_plan"] as string;
          //console.log(plan); 
          const value = props.value as number;                
          const color = (rules as any)[plan](value);
          return <Label style={{color}}>{props.formattedValue}</Label>
          }
     }        
 }  

And it seems to work, until we edit the “Plan” value. The problem: after changing the plan, then “Amount” will be re-rendered, but the data we get as a parameter contains (sometimes) the old value for the “plan”. And there is no other event to force the re-rendering. Have a look:

I’m not sure if it’s a bug, or not. It’s an edge case, since we need to refresh another cell, not the one being edited.

The solution

We could try to force a refresh, by changing the control in edit mode, and then back (stopEditing). That could force a refresh, but it would cause a flickering on the screen, and that’s not nice.

I went with another approach: trigger an own event. And here is how I’ve implemented it.

The complete code can be found in my github repository: https://github.com/brasov2de/GridCustomizerControl/tree/main/CellDependency

EventManager

First I’ve created an EventManager, a class which can trigger events and let the cells to attach to them. It’s easy to create; based on the CustomEvent API

export class EventManager{
    private target : EventTarget;
    private eventName: string;

    constructor(eventName: string){
       //didn't wanted to attach to a real DOM element, so I've created an own target
        this.target = new EventTarget(); 
        this.eventName = eventName;
    }

    public publish(rowId: string, value: any | null ){        
        //trigger an event, passing the rowId and value to the listeners
        this.target.dispatchEvent(new CustomEvent(this.eventName, {detail: {rowId, value } }));        
    }
    
    public subscribe(callback: any){
        this.target.addEventListener(this.eventName, callback);
    }
    public unsubscribe(callback : any){
        this.target.removeEventListener(this.eventName, callback);
    }
}

I think is self explaining. The publish method will be called when the “plan” was changed. The subscribe and unsubscribe is supposed to be called inside the react component rendering the coloured labels.

This eventManager class instance is created inside my cellRenderer closure:

export const generateCellRendererOverrides = () => {

    const eventManager = new EventManager("OnPlanChange");

    return  {       
        ["Integer"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {          
           //code for "Amount" renderer goes here
        },
        ["OptionSet"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {
            //code to check if the "plan" value was changed
            //return null, so let the grid create the control for you
             return null;
        }

    }  
}

React component for rendering the dependent cells – event listener

const Amount: React.FC<IAmountProps> = 
({ value, plan, rowId , formattedValue, eventManager}: IAmountProps) => {
   //keep the plan (dependency) in an internal state
    const [currentPlan, setCurrentPlan] = React.useState(plan);

    //this part takes care to detect when the cell was unloaded by the grid
    const mounted = React.useRef(false); 
    React.useEffect(() => {
        mounted.current = true;
        return () => {
            mounted.current = false;        
        };
    }, []);
    
    //this callback is responsible to change the "currentPlan" state; 
   //that way React rerenders the label
    const onChanged = (evt: any) => {        
        const detail = evt.detail;
        if(!mounted.current) return;
        if(detail.rowId === rowId){ //ignore the events for the other cells
            setCurrentPlan(detail.value);
        }
    };
    
   // the magic happens here: 
   //when the component is created, subscribes to the eventManager 
   //the return at the end of effect takes care to unsubscribe this component from the eventManager
    React.useEffect( () => {
        if(!mounted.current){
            return;
        }         
        eventManager.subscribe(onChanged);
        return () => { eventManager.unsubscribe(onChanged);}
    }, [rowId]);

    const colorFn = currentPlan ? rules.get(currentPlan) : undefined;
    return <Label style={{color: colorFn ? colorFn(value) : "black"}}>{formattedValue}</Label>
};

export default Amount;

To say it in a few words: we keep the dependency in an internal state, and use the “React.useEffect” to attach when the component is created/unloaded. There we subscribe to the eventManager, and get notified when there was a change. The callback attached will set the state “currentPlan”, and React will rerender the cell. All cells from all rows are listening to the event, so we filter only the events for the current rowId .

The cell renderer looks now like this:

export const generateCellRendererOverrides = () => {
    const eventManager = new EventManager("OnPlanChange");

    return  {       
        ["Integer"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {          
            const {columnIndex, colDefs, rowData } = rendererParams;  
            const columnName = colDefs[columnIndex].name;                                 
            if(columnName==="diana_amount") {
                const plan = (rowData as any)["diana_plan"] as string;                                       
                return <Amount 
                           value={props.value as number | null} 
                           plan={plan} 
                           eventManager={eventManager} 
                           rowId={rowData?.[RECID] as string} 
                           formattedValue={props.formattedValue}/>;
            }
        },
        ["OptionSet"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {
         //code goes here
            return null;
        }

    }  
}

Triggering the event

There could be 2 cases to trigger the event, depending if the dependency cell has it’s own renderer or not:

  1. In case you have implemented your own cell renderer, you know when the value was changed, so you could trigger there the eventManager.publish() method
  2. You don’t need to implement your own cell renderer for the cell you depend on

The case 1 is not very common, and means only calling the publish method, so I won’t go with that one. I’ll go with the case 2. The problem here is to detect that the value was changed. So I’ve implemented an own cache, where I track the last “plan” value per row (using a Map). Where a change is detected, we just call the eventManager.publish.

We just return null at the end of the render function. That way the the Power Apps Grid will use the standard controls.

export const generateCellRendererOverrides = () => {

    const eventManager = new EventManager("OnPlanChange");
    //we create a cache containing the combination: rowId->planValue
    const planCache = new Map<string, string | null >();

    return  {       
        ["Integer"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {          
           //we saw that above
        },
        ["OptionSet"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {
            const {columnIndex, colDefs, rowData } = rendererParams;  
            const columnName = colDefs[columnIndex].name; 
            if(columnName!=="diana_plan") return;
            const rowId = rowData?.[RECID];
            if(rowId===undefined) return;

            const oldValue = planCache.get(rowId);
            if(oldValue!=null && oldValue !== props.value){
                //when there is a change, we trigger the event
                eventManager.publish(rowId, props.value);
            }
            planCache.set(rowId, props.value as string ?? null);
             //return null will use the standard component for Choices/OptionSet
            return null;
        }

    }  
}

That’s it. Now the color of the amount is changed right away.

Another use-case

Remember my older blog about calculated cells using Power Apps Grid customizer control: https://dianabirkelbach.wordpress.com/2022/09/19/implement-calculated-columns-with-power-apps-grid-control/ (s. screenshot below).

There I had a similar problem: the calculated “Next Appointment” was calculated based on “Scheduled Appointment” but it got refreshed only after I’ve clicked on another row. The fix works similar: creating own events, so I can force the refreshing right away. I’ve implemented this one too; you can find the code in my github repository: https://github.com/brasov2de/GridCustomizerControl/tree/main/CalculatedColumn_ForceRefresh

And here is the result:

Hope it helps!

Photo by The Lazy Artist Gallery: https://www.pexels.com/photo/woman-holding-remote-of-drone-1170064/

13 thoughts on “Power Apps Grid: Cell Renderer Dependency – Trigger Your Own Events

Add yours

  1. Hi Diana, thank you for all of these posts, they are extremely helpful! I have a question about how I can control where the editable grid control is displayed within my environment. For example am I able to only have it displayed within a single model driven app instead of all of them, or can I control showing it for one specific view in the system?

    1. Hi Ross,
      You can define a dataset PCF (so also the Power Apps Grid) for a specific table (all views) or specific view or a subgrid.
      But the definition on table or view level is possible only with the classic designer (as far as I know).
      Does this answers your question?

  2. Hello Diana, I really appreciate you for the efforts on these posts, and I’ve been able to learn a lot from them.

    I have a question, so currently I’m using a PCF Grid customizer to customize a view. This view shows a list of tasks and is linked to a dataverse table called tasks. I also have a column in this view which contains a button, and when I click on the button it updates the tasks table, and changes the status of the task to ‘completed’. Now the changes aren’t reflected on the view, as it needs to be refreshed. So I was wondering if there is a way to perform the refresh from within the PCF, or access the refresh button from the Command bar via code?

    Thanks
    Vimal

      1. Thanks Diana, for this approach. But I’m guessing that the form-scripting can be applied in the context of a form. But can the same be achievable if I just have a view on a page?

      2. Hi Vimal,

        I haven’t tried it out on views in sitemap, but we can register scripts on the grid for sitemap too (unfortunately only with the old customizing, but it works).
        So you should be able to post a message to that scripts.

  3. Hi Diana,

    Thank you so much for your very useful posts !

    Do you believe it is possible to dynamically hide/show grid columns of the PowerApps Grid control based on the CRM App User language setting ?

    Here in Belgium, we are building a CRM which provides Client Data, INCLUDING STATIC DATA (countries, document types, insurances, etc), in 3 languages : Dutch, French and German.

    These data are NOT option sets but Entity records. And therefore for each record, we store the 3 translations in the record. With a PowerApps grid control, we would return the 3 translations in 3 columns and use the cell renderer to hide/show a column based on the CRM app User language setting.

    1. Hi Francis,
      With Power Apps Grids, it’s not possible to hide columns. I don’t know if you can design the column width to be 1px.. maybe then you have the columns available for your customizer control , which you render in a common column (could be a dummy column with no data).

      A much cleaner solution, would be to always use only one column (could be dummy column), and render the translated labels which you take from the other columns. You can retrieve the corresponding translated column by making your own fetches.
      I’ve did something similar in these blogs:

      Optimized Fetching of Related Records inside Power Apps Grid Customizer PCF


      or

      Fetch Related Records with Power Apps Grid Customizer Control

      1. Hi Diana

        Thank you very much for your quick answer, and indeed your idea of a “dummy empty” column filled in conditionally by code based on the 3 language specific others is worth trying. Thank you for this !

        I confirm you that, using Chrome and the old D365 customizer, you can set the width of a column to 1px, We do this regularly when we want to extend beyond 300px. How ? You open the view design, double click on the column to change column properties, click on a given width (ex: 300), then push F12 and click on “select” icon in toolbar, then select the radio button visually. This brigs you to the HTML location defining the radio button. You replace “300” by “1” in ‘ value=”300″ ‘

        Thank you again for your reply !

Leave a comment

Website Powered by WordPress.com.

Up ↑