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

Cell Renderer Dependency

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 re-render 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 around 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
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 colorsdepending 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
Power Apps active consumptions

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 re-render 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.

Active consumptions

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).

Power Apps My Active Accounts

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/

About the Author

Hi! My name is Diana Birkelbach and I’m a developer, working at ORBIS SE . Together with the team we develop generic components and accelerators for Dynamics 365 and Power Platform.

One of the biggest accomplishments are the Microsoft Most Valuable Professional Award (MVP) in Business Applications for 2021.  

You can also find me in the Power Platform Community Forum, where I’m part of the Power Platform Community Super User Program (Appstronaut) .

Read more.

References

Birkelback, D., (2023), ‘Power Apps Grid: Cell Renderer Dependency – Trigger Your Own Events’, available at: https://dianabirkelbach.wordpress.com/2023/10/26/power-apps-grid-cell-renderer-dependency-trigger-your-own-events/, [accessed 26th March 2024].

Share this on...

Rate this Post:

Share: