Best Practice non-SHIM/SHIM Development

The best practice is to accomodate both non-SHIM and SHIM development. Here's how you do it:

Create a robust SHIM, call it react_shim.html.

  • Place the shim into your root resource, say under new_/react_shim.html

  • Create your main component module: MyComponent.tsx

  • Create a "runner" module that gathers input parameters and then mounts the component into the dom using ReactDom.render().

    • Call the runner: MyComponentRunner.tsx

    • Bundle this to the appropriate js location e.g. new_/js/MyComponentRunner.js

    • You could also call this MyComponentMain.tsx depending on your preferences.

  • Create a SHIM friendly module that calls the runner.

    • if your SHIM is robust enough, you could use the resulting bundled js file as the load point for react_shim.html

    • Bundle this to the appropriate js location e.g. new_/js/MyComponentShim.js

  • Create a HTML file in the appropriate location that loads MyComponentfor production use.

    • Example file: new_/MyComponent.html

    • This HTML loads MyComponentRunner.js and then calls run

This allows you to

  • Have a stable HTML file for production you use when setting up your WebResource in the Form Editor.

  • Allow hot-reload for development and bypass the need to continuously load js files to the server--they are served directly from your development system i.e. laptop.

  • Keep a semblance of separation of concerns.

You might think this seems complicated and it is more complicated, but it is also worth it since you get hot reloading for free on any development workstation.

Also, Dynamics itself uses React+Redux (in the new UCI) and script loaders in a very similar fashion.

MyComponent.tsx

This module holds your component and it typically receives a variety of repo/DAO props as well some top level information such as the entity name and entity id (assuming you use something like EntityForm described elsewhere). Using EntityForm allows you to detect when a form is saved and entity id information is available and we show how to inherit from those props in the example below.

export interface MyComponentProps extends Partial<EntityFormChildProps> {
  repo: MyEntityRepo // for data access
}

export class MyComponent extends React.Component<MyComponentProps, State> {
...
  public render() { return (<div>awesome sauce</div>)}
}

MyComponentRunner.tsx

This module holds a general run function that can be called with the target element and other parameters to load MyComponent into the DOM.

export interface RunProps {
    target?: HTMLElement | null // target container
    repo?: MyEntityRepo
    myComponentProps?: Partial<MyComponentProps>
}

export function run(props: RunProps) {
    getXrmP().then(xrm => {

        const repo = props.repo || makeRep(...)
        const loadConfigPromise = getMyComponentConfig("new.mycomponent")

        loadConfigPromise.then(config => {   
          if(target)
            ReactDOM.render(
            <EntityForm xrm={xrm}>
                <MyComponent
                    repo = {reop}
                    {...props.myComponentProps}
                    { ...config}
                />
            </EntityForm>  ,
            target)  
         }
}

Notice how we initialize key parameters for the component freeing our component from initialization tasks and keeping these concerns separated cleanly. You can make props whatever you want, but typically want to allow as much flexibility as possible for drilling in properties from the run call. Since the run call is called from the SHIM, you can pass in parameters for the view directly from the FormProperties data property, through the URL or by loading initialization props from a remote location.

There are several patterns you can develop depending on where you obtain your configuration and initial pros from. Typically, you need metadata and config data and those may be asynchronous. Hence in the example above, we have a Promise that we must work with. It's easier, obviously, if there is no asynchronous initialization needed.

MyComponentShim.ts

This is the SHIM load point that when you need a SHIM load js file, load this module. You would use this as your "targeturl" in the example SHIM in another section below.

import { run } from "./MyComponentRunner"

if (process.env.NODE_ENV !== "production" && typeof runmain !== "undefined" && runmain) {
  window.addEventListener("load", () => {
    run({ target: document.getElementById("container") })
  })
}

MyComponent.html

Although you could use a SHIM to do a production load, and is kind of easier since you would not need this file, it is less abstract to define a simple HTML loader.

<html>
    <head>
    <meta charset="utf-8" />
<title>Address Manaer</title>
    <script src="../ClientGlobalContext.js.aspx"></script>
</head>
<body>
    <div id="container"/>
    <script type="text/javascript" src="./js/MyComponent.js"></script>
    <script>
     window.addEventListener("load", function() {
         var el = document.getElementById("container");
         MyComponent.run({target: el});
     })
    </script>
</body>
</html>

webpack bundling

You'll need to run your bundler to bundle the two key outputs:

const config = {
    entry: {
        // an entry point or each webresource
        // ...
        // assumes MyComponent modules are in a src/MyComponent directory
        MyComponentShim: path.join(paths.srcdir, "MyComponent", "MyComponentShim.ts"),
        MyComponent: path.join(paths.srcdir, "MyComponent", "MyComponentRunner.tsx")
...

Running webpack produces MyComponentShim.js and MyComponent.js in your target output directory. You only ever need to load MyComponent.js since the SHIM .js file is designed for use only when using webpack-dev-servser. MyComponent.js should be moved to your Dynamics server via a WebResources uploader or whatever process you normally use to move incrementally developed WebResources to Dynamics. The SHIM file is designed to be loaded via a local web server running. You need to arrange for it to be picked up via the mechanism in your react shim in the next section.

react_shim.html

A robust shim is included below

<!--
React based shim to load react code either for hot module development
or for production us. The shim can load the code into the targeted
WebResource iframe or into the parent, which is one up from the
standard dynamics WebResource iframe. This is useful when you need
access to the entire screen. ReactDOM.createPortal would also work
but you'll need to adjust your styles vs a direct load into the
parent of the iframe. You can also run code in the targeted load window
through `code` or run code in the WebResource iframe through codeIFrame.
To hide the iframe after loading in to the parent, set hideParent=true.
All settings come from the WebResource form edito "data" parameter.
You have limited "length" in "data" so you may want to create a regular
HTML entry point that has your final set of configuration prameters in it
or acesss the CRM server for full configuration information.

It is suggested that you set your component's runprops in a sub-object
of data so it is not full of shim "control" properties.
-->
<html>

<head>
    <script src="../ClientGlobalContext.js.aspx"></script>
</head>
<script>
    function getURLParameter(name, search) {
        return decodeURIComponent((new RegExp('[?|&]' + name + '=' +
            '([^&;]+?)(&|#|;|$)').exec(search) || [, ""])[1].replace(/\+/g, '%20')) || null
    }
    function load(targetDoc, src, id, code) {
        var scriptHtml = targetDoc.createElement("script")
        if (code) {
            scriptHtml.appendChild(targetDoc.createTextNode(code))
        }
        else if (src) {
            scriptHtml.setAttribute("src", src)
        }
        if (id) scriptHtml.setAttribute("id", id)
        targetDoc.head.appendChild(scriptHtml)
    }

    var runmain = true
    var data = decodeURIComponent(getURLParameter("data", document.location.search))
    var config = {}
    try {
        config = JSON.parse(data) || {}
    } catch (e) {
        window.alert("[" + window.frameElement.id + "]: Error parsing data parameter: " + e.message)
    }
    // inject data into this window and parent windnow
    var defaultName = "_" + window.frameElement.id + "_data"
    var injectionName = config.injectionName || defaultName
    window[injectionName] = config
    var parentInjectionName = config.parentInjectionName || injectionName
    window.parent[parentInjectionName] = config

    var targeturl = config.targeturl || 'https://localhost:8080/app.js';
    var loadIntoParent = config.loadIntoParent || false;
    var id = config.scriptElementId ? config.scriptElementId : null
    if (config.runmain && !!config.runmain) runmain = config.runmain
    var code = null
    if (config.code) code = config.code

    console.log("SHIM: query parameters: " + document.location.search)
    console.log("SHIM: data value (string): ", data)
    console.log("SHIM: targeturl: ", targeturl)
    console.log("SHIM: loadIntoParent: ", loadIntoParent)
    console.log("SHIM: all config", config)

    var targetDoc = document
    if (loadIntoParent) targetDoc = window.parent.document
    load(targetDoc, targeturl + document.location.search, id, null)
    if (code) load(targetDoc, null, null, code)
    if (config.iFrameCode) load(document, null, null, config.iFrameCode)
    if (loadIntoParent && config.hideFrame) window.frameElement.style.display = "none"
</script>

<div id="container" class="shimContainer">
</div>
</body>
</html>

Now you want to load your SHIM file, specify this generic "react_shim.html" file as your WebResource in the Form Editor. The "data" parameters should be set to the following. You must be careful in that "data" can only handle so much data as it passes the data in the URL and URLs have length restrictions.

{ 
   "targeturl": "https://localhost:8080/new_/js/MyComponentShim.js",
   // anything else you want here per the SHIM above 
}

Last updated