Views: 260
Number of votes: 4
Average rating:

A react widget in Episerver CMS (Revisited)

Back in January 2017, Magnus Baneryd created a blog post about creating a gadget using react (https://world.episerver.com/blogs/magnus-stalberg/Dates/2017/1/creating-a-react-widget-for-episerver-cms/). Given this was nearly two years ago and the technology has changed a bit since then, I thought that I would revisit his idea.

I am going to assume that if this topic is interesting for you then you already have some knowledge about creating react applications and using different bundling tools. So I'm not going to explain what node modules you need need to install but rather just point out what might be different from a regular react application setup.

Aside: I his blog, Magnus creates a custom gadget. I personally hate the gadgets in the left and right panes since they are so small and adding more than two basically makes them unusable. So for this demo I will use a custom edit view since it is easy to demo. However this same approach will still work for the custom gadget.

Register the view server-side

To create a new edit view for content data we need to register a ViewConfiguration class on the server side and point to the JavaScript file that should be loaded. In this case the file will be our bundled react view. The following code will do this:

[ServiceConfiguration(typeof(ViewConfiguration))]
public class MyView : ViewConfiguration<IContentData>
{
    public MyView()
    {
        Key = "my-view";
        Name = "React View";
        ControllerType = "alloy/components/ReactGadget";
        IconClass = "epi-iconStar";
        SortOrder = 100;
    }
}

Now when you look in edit mode you will have an extra view listed in the view switcher.

Create the entry point

If you have ever made a react application before you will be familiar with having an entry point called index.js that simply calls ReactDOM.render (https://reactjs.org/docs/react-dom.html#render). However in this case, since our view in running inside a dojo application, the entry point needs to pretend to be a widget and that widget will instead call ReactDOM.render. This can be done with code that is basically the same as what Magnus had written in his blog post:

index.js

import React from "react";
import ReactDOM from "react-dom";
import declare from "dojo/_base/declare";
import WidgetBase from "dijit/_WidgetBase";
import MyComponent from "./MyComponent";

export default declare([WidgetBase], {
    postCreate: function () {
        ReactDOM.render(<MyComponent />, this.domNode);
    },
    destroy: function () {
        ReactDOM.unmountComponentAtNode(this.domNode);
    }
});

This will create a widget which will then render the react component to it's DOM node. It will also unmount the component when the view is destroyed.

This code can be used as a boilerplate, just replace MyComponent with your react component.

Building the bundle

At this point let's assume you have your MyComponent implemented in react. In order to get it to appear in the UI it needs to be bundled into a single file (the one that our view configuration is pointing to). Nowadays the defacto bundler seems to be webpack (https://webpack.js.org/) but I would argue that rollup (https://rollupjs.org/) is better suited in this scenario. However I will give an example of both configurations since they are very similar.

The main point to note is that since the Episerver CMS uses AMD for its module loading, the bundle we produce also needs to be AMD formatted. Luckily the build tools will do this for you will a single line of configuration.

The second thing to note is that in the entry point code, in the previous example, there are imports for dojo and dijit. These however exist inside the Episerver CMS application and shouldn't be bundled, so they need to be marked as external. This is also done with a few lines of configuration.

The entry point for your bundle will be the index.js file from the previous example and the output file should what you configured in the view configuration. Change the paths in the examples below to suit your needs.

webpack.config.js

const path = require("path");

module.exports = {
    entry: path.resolve(__dirname, "src/index.js"),
    output: {
        filename: "ReactGadget.js",
        libraryTarget: "amd",
        libraryExport: "default",
        path: path.resolve(__dirname, "ClientResources/Scripts/components/")
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            }
        ]
    },
    externals: [
        "dojo/_base/declare",
        "dijit/_WidgetBase"
    ]
};

The interesting parts of this configuration are the settings for libraryTarget, libraryExport, and externals.

For webpack it is important to set the libraryExport so that it returns the default export from the entry point rather than an object containing all the exports.

rollup.config.js

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import replace from "rollup-plugin-replace";
import resolve from "rollup-plugin-node-resolve";

export default {
    input: "src/index.js",
    output: {
        file: "ClientResources/Scripts/components/ReactGadget.js",
        format: "amd"
    },
    plugins: [
        babel({
            exclude: "node_modules/**"
        }),
        commonjs(),
        replace({ "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) }),
        resolve()
    ],
    external: [
        "dojo/_base/declare",
        "dijit/_WidgetBase"
    ]
};

The interesting parts of this configuration are the settings for format, and external.

Accessing CMS features from the react component

At this point you should have your react component loading in the UI. But as you develop your great new react view you may realise that it would be really nice to be able to use a component from the Episerver CMS. For example, you may want the user to be able to select a page using the content selector.

Well luckily I have created a react component that will do exactly this for you. The code for this component can be copied from this gist into your project (https://gist.github.com/ben-mckernan/b06137f4bc076f862b33d7dd0dbf9a88). It has a dependency on @episerver/amd-loader-proxy so make sure you install this node module. I will note however that I have only done very limited testing on this component so there maybe some quirks.

Events will automatically be connected from the props to the widget based on the naming convention onEventName. And settings for the widget will only be passed to the widget during construction. Changes to settings will not be propagated after mount, although it is probably not hard to implement.

Example usage could look like this:

import React, { Component } from "react";
import DojoWidget from "./DojoWidget";

class MyComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            value: null
        };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(value) {
        this.setState({ value });
    }

    render() {
        const { value } = this.state;
        const label = value ? <div>ContentLink: {value}</div> : null;

        return (
            <div>
                <DojoWidget type="epi-cms/widget/ContentSelector" settings={{ repositoryKey: "pages" }} onChange={this.handleChange} />
                {label}
            </div>
        );
    }
}

export default MyComponent;

Something else that maybe worth looking at is the @episerver/amd-proxy-loader I mentioned previously (https://www.npmjs.com/package/@episerver/amd-loader-proxy). With this it is possible to dynamically require AMD modules from Episerver CMS from your JavaScript application. This is framework agnostic so it can be used with whichever JavaScript framework you prefer.

Final words

This is quite a rough example but I hope it inspires someone to make something much better.

Please login to comment.