DEV Community

Cover image for Form-based Dataverse Web Resources with React, Typescript and FluentUI
Riccardo Gregori
Riccardo Gregori

Posted on

Form-based Dataverse Web Resources with React, Typescript and FluentUI

The main difference between a Dataverse WebResource that must be accessed from outside a form context (e.g. from the navbar, as a modal dialog, or a side pane), and one that must be injected into a form to enrich the form experience, is that the latter must receive Xrm and FormContext from the Form itself, and cannot rely on ClientGlobalContext.js.aspx.

In this quick tutorial we'll see an example on how to do it when we have a React+Typescript+FluentUI WebResource, as described in my previous article.

Let's go! πŸš€

The initial setup steps are the same of the previous article, I won't deep dive on those. Just a quick recap.

Prerequisites 🧳

  • The folder that will contain your webresources
  • PAC CLI and PACX installed on the local machine
  • Both PAC CLI and PACX already connected to the target environment

Set-up steps πŸ‘¨πŸ»β€πŸ’»

Quickly:

pacx solution create --name master -pp ava # to create the dataverse solution that will contain our customizations
pacx solution setDefault --name master # to set the solution we've just created as default for the environment
pacx wr init # to initialize the webresource project
npx create-react-app account-details --template @_neronotte/cra-template-dataverse-webresource # to create our form-based webresource project
Enter fullscreen mode Exit fullscreen mode

Then let's perform the manual operation steps that are described on the README.md file and build our project. Then, just run:

cd account-details
npm run build
Enter fullscreen mode Exit fullscreen mode

Your project structure now should look something like:

Project structure

You can type

npm run start
Enter fullscreen mode Exit fullscreen mode

To run your WebResource locally.

Prepare the WebResource to receive formContext externally πŸ’†πŸ»β€β™‚οΈ

In order to accept both formContext and Xrm object from the parent form, the WebResource must expose a public JS function.
We'll create this public function directly within the /account-details/public/index.html file. Open that file and, in order:

  • Remove line 10
<script src="../../../ClientGlobalContext.js.aspx" type="text/javascript" ></script>
Enter fullscreen mode Exit fullscreen mode

To get the context from the outside, we have a problem: the actual React app gets rendered immediately, while the form context may be passed lately. We want to ensure to render everything only when the actual formContext is injected in our app, to be sure to have everything wired properly.

To achieve this there are various techniques. For the sake of this tutorial, we'll update our index.tsx file:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { initializeIcons } from '@fluentui/font-icons-mdl2';

initializeIcons();

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

function render() {
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

const w = window as any;
w.setClientApiContext = function(xrm : Xrm.XrmStatic, formContext : Xrm.FormContext) {
  w.Xrm = xrm;
  w._formContext = formContext;
  render();
}
Enter fullscreen mode Exit fullscreen mode

This script exposes an API called setClientApiContextto the parent form. When called, this API saves the pointers to Xrm and formContext, and starts the rendering of the app. Until the function is called, the app is not rendered.

Tweak the local SDK πŸ“

The local sdk shipped with @_neronotte/cra-template-dataverse-webresources expects Xrm and GlobalContext to be provided via ClientGlobalContext.js.aspx. We need to change it a bit in order to manage formContext injected from the outside.

In a future update of the package we are planning to manage this scenario directly, without any tweak.

First of all, access the src/sdk/GlobalContext.tsx file, and change the isWired() function this way:

public static isWired(): boolean {
    var w = window as any;
    return typeof(w.Xrm) !== 'undefined';
}
Enter fullscreen mode Exit fullscreen mode

Then remove the getGlobalContext() function and replace it with:

import LocalFormContext from "./LocalFormContext"; // this must be placed on top of the .tsx file

public static getFormContext(): Xrm.FormContext {
    var w = window as any;
    if (typeof(w._formContext) !== 'undefined') {
        return w._formContext;
    }

    return new LocalFormContext();
}
Enter fullscreen mode Exit fullscreen mode

The LocalFormContext class referenced above is a class we want to create to stub locally the form context. Just create a file in the same sdk folder called LocalFormContext.tsx with the following content:

import LocalPageData from "./LocalPageData";
import LocalPageUi from "./LocalPageUi";

export default class LocalFormContext implements Xrm.FormContext
{
    constructor() {
        this.data = new LocalPageData();
        this.ui = new LocalPageUi();
    }


    data: Xrm.Data;
    ui: Xrm.Ui;
    getAttribute(): Xrm.Attributes.Attribute[];
    getAttribute<T extends Xrm.Attributes.Attribute>(attributeName: string): T;
    getAttribute(attributeName: string): Xrm.Attributes.Attribute;
    getAttribute(index: number): Xrm.Attributes.Attribute;
    getAttribute(delegateFunction: Xrm.Collection.MatchingDelegate<Xrm.Attributes.Attribute>): Xrm.Attributes.Attribute[];
    getAttribute(delegateFunction?: unknown): Xrm.Attributes.Attribute<any> | Xrm.Attributes.Attribute<any>[] {
        throw new Error("Method not implemented.");
    }
    getControl(): Xrm.Controls.Control[];
    getControl<T extends Xrm.Controls.Control>(controlName: string): T;
    getControl(controlName: string): Xrm.Controls.Control;
    getControl<T extends Xrm.Controls.Control>(index: number): T;
    getControl(index: number): Xrm.Controls.Control;
    getControl(delegateFunction: Xrm.Collection.MatchingDelegate<Xrm.Controls.Control>): Xrm.Controls.Control[];
    getControl(delegateFunction?: unknown): Xrm.Controls.Control | Xrm.Controls.Control[] {
        throw new Error("Method not implemented.");
    }

}
Enter fullscreen mode Exit fullscreen mode

Update the app to show something meaningful

For the sake of this example, let's update App.tsx to show some info took directly from the formContext.

import "./App.scss";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import GlobalContext from "./sdk/GlobalContext";

function App() {
    return (
        <FluentProvider theme={webLightTheme} className="container">
            <h1>Form Wired WebResource</h1>
            <p>
                {GlobalContext.getFormContext().data.entity.getEntityName()}: {GlobalContext.getFormContext().data.entity.getId()}
            </p>
        </FluentProvider>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Build and push πŸ“¦

Now we can build our app and push it in the Dataverse. Open a terminal in the project root folder and type

cd account-details
npm run build
cd ..\ava_\pages # move to the folder that contains the outputs
pacx wr push # i like to use PACX to push the webresources in the dataverse
Enter fullscreen mode Exit fullscreen mode

Push the webresource to dataverse

Inject the formContext πŸ’‰

Now it's time to create a JS WebResource containing a function that, bound to the onLoad event of the account form, takes a reference to the webresource and injects the formContext. Let's do it via PACX:

cd ava_\scripts
pacx wr create js --table account
Enter fullscreen mode Exit fullscreen mode

A file called account.js will be created in the ava_\scripts folder, with the following content:

class Form {
    formType = {
        Create: 1,
        Update: 2,
        ReadOnly: 3,
        Disabled: 4,
        BulkEdit: 6
    };

    onLoad(executionContext) {
        const formContext = executionContext.getFormContext();
        const formType = formContext.ui.getFormType();
    }
}

ava = window.ava || {};
ava.account = ava.account || {};
ava.account.Form = new Form();
Enter fullscreen mode Exit fullscreen mode

You wanna update the onLoad method with:

class Form {
    formType = {
        Create: 1,
        Update: 2,
        ReadOnly: 3,
        Disabled: 4,
        BulkEdit: 6,
    };

    onLoad(executionContext) {
        const formContext = executionContext.getFormContext();
        const formType = formContext.ui.getFormType();

        if (formType === this.formType.Create || formType === this.formType.Update) {
            this.setClientApiContext(formContext);
        }
    }

    setClientApiContext(formContext) {
        const wrControl = formContext.getControl("WebResource_new_1"); // this is the name of the wr control that will be put in the form, you can change it if you want
        if (!wrControl) return;

        wrControl.getContentWindow().then(function (contentWindow) {
            contentWindow.setClientApiContext(Xrm, formContext);
        });
    }
}

ava = window.ava || {};
ava.account = ava.account || {};
ava.account.Form = new Form();
Enter fullscreen mode Exit fullscreen mode

now we're ready to push this WR to Dataverse:

pacx wr push
Enter fullscreen mode Exit fullscreen mode

account.js pushed to Dataverse via PACX

Wire everything together πŸ”—

Now it's time to:

  • Add the account entity form into our solution (I won't show you how to do it. If you can't do it, maybe you're in the wrong page 😎)
  • Open the form editor window, and add a new Section
  • Into that section, drag/drop an HTML web resource control, and select the React WebResource we have just created:

Peek the webresource

  • We'll leave that web resource control name default (WebResource_new_1), be sure to set the same value you typed in the account.js script.

WebResource Name

  • Let's add account.js to our form

Add account.js to the form

  • And bind the OnLoad event to ava.account.Form.onLoad

Bind the **OnLoad** event to  raw `ava.account.Form.onLoad` endraw

  • Then save and publish.
  • Now... just open any account record... et voilΓ !

Final result!

Conclusions πŸ‘πŸ»

😊 Hope you enjoyed this tutorial 😊!

Drop a comment below if you want to see more about React & Typescript & Fluent UI development with a twist of PACX!

References

Top comments (1)

Collapse
 
jcirujales profile image
John

great article! I actually just needed this and saw it was posted just an hour ago, thank you.