Drag and Drop Rows in Power Apps Grid

Drag and Drop Rows in Power Apps Grid

Please note that this solution contains a part which is not supported by the platform right now, so it shouldn’t be used in production yet. If this eventually gets documented in the future (so if it will be supported), I’ll update the blog.

The requirement

Lately I was asked if it’s possible to implement a subgrid for model-driven apps, where each row has it’s own sort order position. Of course it should be possible to change the sorting order of the rows. The tricky part: the user should be able to use drag&drop to move to rows up or down.

It’s always possible to implement a “dataset PCF”, and there we have the control over the code, so we can implement drag&drop without a problem. But that takes much longer. Also, an own PCF grid has limited extensibility. If you think to a Order Details subgrid on the Order form, there are also another custom calculations needed. Of course we could do that in a dataset PCF too, but then it’s not a generic PCF; then it’s a dedicated OrderDetails PCF, with custom logic inside.

The best way would be to use Power Apps Grids, since it takes only a little effort and there we can implement also the other calculations too. But does drag&drop work inside the Power Apps Grid?

Privacy Settings

As an example I’ll talk about defining an agenda for a Power Platform training. I have for each subject a Basic and an Extended session, and I want to move the “Extended” to the end of the day. It should work like this:

Drag and Drop Rows in Power Apps Grid

The customizing

I have a subgrid called “Sortables” inside my form. The table has a column called “SortOrder”, which defines the position of the row.

If the user wants to move the row “5” on position “2”, the following changes need to be made

  • 5 -> 2
  • Rows [2, 3, 4] will be incremented -> [3, 4, 5]
Drag and Drop Rows in Power Apps Grid

The idea

Let’s see, how much can we implement using the Power Apps Grid customizer control.

  • The customizer control can render the cells of the “SortOrder” column with a different control
  • Inside the customizer control we get the rowId and the value of the SortOrder for the corresponding row
  • Inside the customizer control for SortOrder we should be able to mimic a drag&drop action. Inside the control we can use the HTML5 drag&drop possibilities (each cell is able to be dragged, and can be in the same time a drop target). I’ve described the idea in my other blog about drag&drop between PCFs.
  • We can change the value for the cell, but only for the current row. Since moving one row means changing a lot of other rows too, we cannot rely here on Power Apps Grid. We need to use webAPI (or maybe a Custom API) for that.
  • The customizer control is not able to refresh the grid. But the form-scripting can refresh the grid.
  • A customizer control for Power Apps Grid is not able to call code from form-scripting, but we can use postMessage to send the events to the hosting form.

Given this, the solution could look like this:

  1. Make a customizer control which is draggable and is a drop target
    • use it for the column “SortOrder”
  2. When we start dragging,
    • we’ll define the dragged data containing the rowId and the position for the row being dragged.
  3. When a row is dropped
    • from the drag event, we know rowId and the position for the row being dragged
    • the current customizer control knows the rowId and position of the target row
    • using this information we’ll use form scripting to change all the rows in between and refresh the grid.

The implementation of the PCF

The complete code can be found on my GitHub repository.

The implementation of the PCF for Power Apps Grid has 3 aspects.

The manifest

Nothing special here; we define only one property “EventName”.

<?xmlversion="1.0"encoding="utf-8"?><manifest>  <controlnamespace="Dianamics"constructor="DragRows"... control-type="virtual">    <propertyname="EventName"display-name-key="Property_Display_Key"description-key="Property_Desc_Key"of-type="SingleLine.Text"usage="bound"required="true"/>    <resources>      <codepath="index.ts"order="1"/>      <platform-libraryname="React"version="16.8.6"/>      <platform-libraryname="Fluent"version="8.29.0"/>    </resources>  </control></manifest>

Init method of Index.ts

All we have to do inside “init” method: register the cell renderer.

public init(    context: ComponentFramework.Context<IInputs>,    notifyOutputChanged: () => void,    state: ComponentFramework.Dictionary): void {           this.notifyOutputChanged = notifyOutputChanged;    const eventName = context.parameters.EventName.raw;           if(eventName) {            const paOneGridCustomizer: PAOneGridCustomizer = {            cellRendererOverrides : MyCellRenderer                };        (context as any).factory.fireEvent(eventName, paOneGridCustomizer);                }                }

Please note that this part needs some small changes in order to work. I’ll explain below in “Now it gets unsupported” section, what change we need to make.

DraggableCell component

The react component will render the “DragObject” icon, inside a draggable DIV. This is enclosed inside another DIV, which can be a drop target.

When we start dragging, we pass the rowId and rowIndex inside the “DianamicsDraggedRow” event data.

When another row is dropped over the component, the callback “onDropped” is executed.

exportfunctionDraggableCell({rowId, rowIndex, text, onDropped}:IDraggableCell): any{    const dragStart = (event: any) => {        event.dataTransfer.setData("DianamicsDraggedRow", JSON.stringify({rowId, rowIndex}));         }    functiondrop(event:any) {        event.preventDefault();        const targetId = rowId ?? "";                const source = JSON.parse(event.dataTransfer.getData("DianamicsDraggedRow") ?? "{}");            const sourceId = source?.rowId;        const sourceIndex = source?.rowIndex;        if(onDropped) onDropped(sourceId, sourceIndex, targetId, rowIndex );      }    return(                <div onDrop={drop} onDragOver={allowDrop} style={{width: "100%", height: "100%"}}>            <div draggable={true} onDragStart={dragStart}>            <Icon iconName="DragObject"aria-hidden="true"id={rowId} style={{fontSize: "xx-large"}}  />            {text}                    </div>        </div>           )        }

Cell renderer

Here we register the function which the Power Apps Grid should use to render the SortOrder cells.

We actually don’t implement the reordering of the rows here, we only send a postMessage event to the form-scripting. Since the scripts on the form are not inside the same window, I need to get all the child frames and send messages. The hand-shake will be taken by my message name “Dianamics.DragRows”. This name has to be detected in the form-scripting. The other windows/frames will ignore my message.

exportconst MyCellRenderer = {        ["Integer"]: (props: CellRendererProps, rendererParams: GetRendererParams) => {            const column = rendererParams.colDefs[rendererParams.columnIndex];            if(column.name==="diana_sortorder"){                //the onDropped is the callback we'll pass to the DraggableCell react component                const onDropped = (sourceId: string, sourceValue:number, targetId : string, targetValue:number) => {                                                 //gray zone: to interact with form-scripting I'll use window.postMessage to send a message with the drag&drop action                                       Array.from(parent.frames).forEach((frame) => {                        frame.postMessage({                            messageName: "Dianamics.DragRows",                             data: {                                sourceId,                                 sourceValue,                                targetId,                                 targetValue                            }                        }, "*");                    });                                                            }               return<DraggableCell rowId={rendererParams.rowData?.[RECID]} rowIndex={props.value} text={props.formattedValue} onDropped={onDropped}/>                                            }            returnnull;        }            };

Then we can upload the PCF and register the Power Apps Grid with the customizer control for the subgrid

Drag and Drop Rows in Power Apps Grid

The implementation of the form-scripting

We declare here a function “SortOrderChanged” which will be registered on the hosting form for the “OnLoad” event. There we listen to all messages sent to the window, and filter on my special message name “Dianamics.DragRows” (line 40). If this messages is received, we can start the “move” function.

When we start reordering the rows (move function), we can start with a progress dialog, to prevent the user making more actions until the rows are reordered.

Inside retrieveRecords we can retrieve all records between start value and end value (sortOrder) by keeping the records sorted by “diana_sortorder” column. For that, we need to take care that “sourceIndex” is lower than “targetIndex” otherwise the retrieveRecords won’t return any positions. I’ve applied that inside the “move” function, using Math.min and Math.max.

When the request returns the records, we calculate the delta. We need to know if we should increment or decrement the values for the rows in between (it depends in which direction the row was dragged). Then we need to change also the value of the row being dragged.

When everything is done, we can refresh the Power Apps Grid subgrid, and close the progress dialog. Done. 

functionSortOrderChanged(executionContext){   //retrieve all records between source Index and target Index, by keeping sorting on "diana_sortorder" column   const retrieveRecords = (sourceIndex, targetIndex) => {      const parentId = executionContext.getFormContext().data.entity.getId();      returnXrm.WebApi.retrieveMultipleRecords("diana_sortable",          `?$select=diana_sortableid,diana_sortorder&$filter=(_diana_accountid_value eq '${parentId}'` +         `and Microsoft.Dynamics.CRM.Between(PropertyName='diana_sortorder',PropertyValues=['${sourceIndex}','${targetIndex}']))&$orderby=diana_sortorder asc`)   }   //refresh the grid, after the rows were moved   const refreshGrid = () => {      executionContext.getFormContext().ui.controls.get("Sortables")?.refresh();      Xrm.Utility.closeProgressIndicator();   }    const move = (sourceId, sourceValue, targetId, targetValue)=> {      Xrm.Utility.showProgressIndicator("updating sort order");      //detecting the direction: we'll need to increment or decrement the value for sortorder      const delta = sourceValue<targetValue ? -1 : 1;      retrieveRecords(Math.min(sourceValue, targetValue), Math.max(sourceValue, targetValue)).then((response)=>{         //update currentValue + delta         const updates = response.entities.map((record)=> {            if(record.diana_sortableid!=sourceId){               returnXrm.WebApi.updateRecord("diana_sortable", record.diana_sortableid, {"diana_sortorder": record.diana_sortorder + delta});            }            returnPromise.resolve();         })                  //update source (the row being dragged) with the targetValue         updates.push(Xrm.WebApi.updateRecord("diana_sortable", sourceId, {"diana_sortorder": targetValue}))                     returnPromise.all(updates).then(refreshGrid, refreshGrid)         });              }        //register on the messages sent to this window    window.addEventListener("message", (e) => {                 console.log("registered OnMessage", e);       if(e.data?.messageName === "Dianamics.DragRows") {               const data = e.data.data;         move(data.sourceId, data.sourceValue, data.targetId, data.targetValue);                }     })    }

Then we only need to register this script for the hosting form.

Drag and Drop Rows in Power Apps Grid

Now everything’s done, but unfortunately the dragging doesn’t work

Drag and Drop Rows in Power Apps Grid
Unfortunately dragging doesn’t work like this- it gets prevented by the grid

Now it gets unsupported – GridCustomizer to the resque

We see the Icon we’ve implemented for the Customizer Control, but the drag&drop doesn’t work. After some investigations, I could see that the “dragStart” function gets called, but unfortunatelly the dragging gets prevented and the “drop” function doesn’t get called.

But I didn’t stop there. The the PCF samples, there is a sample of a Power Apps Grid customizer control, and we’ve got there a “Types.ts”. Here we find a lot of interesting definitions. Here we can see that the GridCustomizer for Power Apps Grid can be defined by providing cellRender and cellEditor or a gridCustomizer

Drag and Drop Rows in Power Apps Grid

So I’ve tried out, how it works if instead of defining only the cellRenderer or the cellEditor I would go with the gridCustomizer.

The GridCustomizer needs a lot more definitions, and should be used only if the cell-renderer or the cell editor doesn’t work.

Drag and Drop Rows in Power Apps Grid

Please note that using the GridCustomizer is unsupported for now. There is no official example about the GridCustomizer until now, and no other documentation.

To try it out I had to make a few small changes:

Init method inside index.ts

Here I’ve changed the cellRenderer to GridCustomizer.

In the types.ts seems that gridCustomizer is the property which needs to be defined. I couldn’t make it work by using the gridCustomizer, but I’ve found out that cellCustomization seems to be actually used. So I’ve defined both, just to be sure.That’s also a sign that the gridCustomizer is not ready to be used in production for now.

public init(    context: ComponentFramework.Context<IInputs>,    notifyOutputChanged: () => void,    state: ComponentFramework.Dictionary): void {           this.notifyOutputChanged = notifyOutputChanged;    const eventName = context.parameters.EventName.raw;           if(eventName) {        const draggableGrid = DraggableRowsGridRenderer(context);        const paOneGridCustomizer: PAOneGridCustomizer = {             cellCustomization : draggableGrid,            gridCustomizer : draggableGrid           //  cellRendererOverrides : MyCellRenderer                };        (context as any).factory.fireEvent(eventName, paOneGridCustomizer);                }                }

GridCustomizer

My GridCustomizer looks almost the same as my cellRenderer, except that now I need to provide the cell renderer for all the columns in my grid. It’s not enough to take care only about the columns I’m interested in.

exportconst DraggableRowsGridRenderer = (context: ComponentFramework.Context<IInputs>) : GridCustomizer  => {        return{    GetLoadingRowRenderer: (): React.ReactElement => {        return<div>Loading...</div>;    } ,     GetCellRenderer : (params: GetRendererParams): React.ReactElement => {                const cellName = params.colDefs[params.columnIndex].name;        const formattedValue = params.colDefs[params.columnIndex].getFormattedValue(params.rowData?.[RECID]) ?? (params.rowData as any)[cellName];        if(cellName==="diana_sortorder"){                       const index = (params.rowData as any)?.diana_sortorder ?? (params as any).rowIndex;                    const onDropped = (sourceId: string, sourceValue:number, targetId : string, targetValue:number) => {                                              Array.from(parent.frames).forEach((frame) => {                    frame.postMessage({                        messageName: "Dianamics.DragRows",                         data: {                            sourceId,                             sourceValue,                            targetId,                             targetValue                        }                    }, "*");                });                                                    }            return(<div>                <DraggableCell rowId={params.rowData?.[RECID]} rowIndex={index} text={formattedValue} onDropped={onDropped}/>                                     </div>)        }        return(<div>                            {formattedValue}        </div>)        }                    }}

The GridCustomizer vs CellCustomizer

Well, now the drag & drop of the rows works. But I’ve lost a lot of the standard features

Drag and Drop Rows in Power Apps Grid
  • the header is showing only the column names. I don’t have the sorting or the column-filter features. I could define my own header renderer together with my grid customizer, but I would have to implement it. I don’t know if there is a way to call the standard header renderer.
  • the colors for the optionset are not shown anymore. We would need to implement that too
  • the navigation-links for the lookup are gone too.

The GridCustomizer implements the main grid functionality (columns, scrolling, row-selection) but we need to take care of the header and all the cells.

This workaround doesn’t have anything to do with drag&drop functionality, but it happened that using the GridCustomizer the dragging was not prevented by the grid.

Small improvement – illusion of dragging rows

We could improve the component a little by creating the illusion that the whole rows are dragged; not only the cell content.

For that we can use the HTML DataTransfer.setDragImage ( more details in my last blog too ).

All I have to do, is to extend the startDrag function from my DraggableCell component, and use a parentElement for setDragImage

exportfunctionDraggableCell({rowId, rowIndex, text, onDropped}:IDraggableCell): any{    const dragStart = (event: any) => {        event.dataTransfer.setData("DianamicsDraggedRow", JSON.stringify({rowId, rowIndex}));        //console.log("Started to drag the text", rowId);                      varcrt = event.currentTarget.parentElement?.parentElement?.parentElement?.parentElement;//?.parentElement;             if(crt){               event.dataTransfer.setDragImage(crt, 0, 0);         }    }....
Drag and Drop Rows in Power Apps Grid
Now we can see a shadow image of the whole row being dragged
Drag and Drop Rows in Power Apps Grid

Not sure if it makes a big difference. Maybe it’s enough to simulate the dragging of the cells. It would make a bigger impact in case there is more than only text inside the dragged rows (colors, images).

Conclusion

I hope the the future we’ll have a supported way to implement the Power Apps Grid customizer control for drag&drop.

About the Author

Dianamics PCF Lady | Microsoft MVP | Blogger | Power Platform Community Super User | 👩‍💻 Dynamics 365 & PowerPlatform Developer | ORBIS SE

Reference

Birkelbach, D., 2023, Upload documents from Power Pages to SharePoint Document Library without SharePoint Integration, Dianaberkelbach.wordpress.com, Available at: https://www.agarwalritika.com/post/upload-documents-from-power-pages-to-sharepoint-document-library-without-sharepoint-integration [Accessed on 7 September 2023]

STAY UP TO DATE

Catch up on the latest blogseBookswebinars, and how-to videos.
Not a member? Sign up today to unlock all content.
Subscribe to our YouTube channel for the latest community updates.

Share this on...

Rate this Post:

Share: