How to use ReactJS Components in Control Add-ins

Control Add-in is an Object type in Business Central to develop User Controls using JavaScript, and CSS. Though it is a very good option to enhance UI / UX in Business Central, we often have to struggle to build HTML markup in JavaScript, because we have to use string concatenate functions, or complex DOM methods build HTML elements. ReactJS library can give us the flexibility to write HTML markup inside JavaScript.


About ReactJS

ReactJS is a JavaScript library developed by the Facebook team to develop single page applications. ReactJS uses a new file type called JSX (JavaScript and Xml) which allows to write HTML markup in JavaScript (this is the exact requirement). Using npm scripts ReactJS application can be transpiled into JavaScript and CSS files.


A Simple React Component:

class HelloMessage extends React.Component {
  render() {
    return (
      <div>
        Hello {this.props.name}
      </div>
    );
  }
}

ReactDOM.render(
  <HelloMessage 
name="Taylor" />,
  document.getElementById('hello-example')
);


ReactJS Application

A new ReactJS application can be created using the following npx command. npx is a node package runner. You must install node.js before executing this command.

npx create-react-app my-app

The following command build the ReactJS application and creates a build folder with transpiled JavaScript, and CSS files.

npm run build

build folder:

  330.3 KB  build\static\js\3.2860d5eb.chunk.js
  19.96 KB  build\static\js\0.3e8005e8.chunk.js
  13.43 KB  build\static\js\4.c188f31c.chunk.js
  11.62 KB  build\static\css\3.fb09108b.chunk.css
  6.76 KB   build\static\js\5.eb238462.chunk.js
  5.16 KB   build\static\js\6.1a36259c.chunk.js
  1.21 KB   build\static\js\runtime-main.386c5c00.js
  856 B     build\static\js\main.da43e367.chunk.js
  584 B     build\static\js\7.3093821e.chunk.js
  214 B     build\static\css\main.b2c4a7d2.chunk.css

Bundle JS, CSS files using Gulp

Gulp is a JavaScript task runner. Using Gulp, multiple JavaScript and CSS files can be bundled into a single JavaScript, and into a single CSS file.


install the following packages:

npm install --save-dev gulp
npm install --save-dev gulp-clean-css
npm install --save-dev gulp-concat

"gulp-clean-css", "gulp-concat" are the gulp plugins to clean CSS files and concatenate files respectively.


gulpfile.js

Create "gulpfile.js" file in React Application's root folder. This takes JavaScript and CSS files from the build folder and bundles them into a single JavaScript and CSS files and saves in Control Add-in folder.


ControlAddInFolder: is the relative path to Control Add-in AL project folder.

var gulp = require('gulp');
var concat = require('gulp-concat');
var cleanCss = require('gulp-clean-css');

const ControlAddInFolder = '../app/src/ContentEditor';
const ControlAddInName = 'content-editor';

gulp.task('pack-js', function () {
    return gulp.src(['build/static/js/*.js'])
        .pipe(concat(`${ControlAddInName}.js`))
        .pipe(gulp.dest(`${ControlAddInFolder}/js`));
});

gulp.task('pack-css', function () {
    return gulp.src(['build/static/css/*.css'])
        .pipe(concat(`${ControlAddInName}.css`))
        .pipe(cleanCss())
        .pipe(gulp.dest(`${ControlAddInFolder}/css`));
});

gulp.task('default', gulp.series('pack-js', 'pack-css'));

Update build script in package.json

Update "package.json" to execute gulp after completing build automatically.


"react-scripts build && gulp" :- will execute gulp (gulpfile.js) after being build.

 "scripts": {
 "start": "react-scripts start",
 "build": "react-scripts build && gulp"
  },

ReactJS Component (ContentEditor.js)

The following React component renders "wix-rich-content-editor" in "controlAddIn" element on receiving "onLoadContent" custom event. And it will dispatch "onContentChange" custom event on content change.

import React from 'react';
import ReactDOM from 'react-dom';
import { EditorState, RichContentEditor } from 'wix-rich-content-editor';
import { createLinkPlugin } from 'wix-rich-content-plugin-link';
import { createCodeBlockPlugin } from 'wix-rich-content-plugin-code-block';
import { createHashtagPlugin } from 'wix-rich-content-plugin-hashtag';
import { createHtmlPlugin } from 'wix-rich-content-plugin-html';
import 'wix-rich-content-editor-common/dist/styles.min.css';
import 'wix-rich-content-editor/dist/styles.min.css';
import 'wix-rich-content-plugin-link/dist/styles.min.css';
import 'wix-rich-content-plugin-code-block/dist/styles.min.css';
import 'wix-rich-content-plugin-hashtag/dist/styles.min.css';
import 'wix-rich-content-plugin-html/dist/styles.min.css';
import './App.css';

import {
  convertFromRaw,
  convertToRaw,
} from 'wix-rich-content-editor-common';

const PLUGINS = [createLinkPlugin, createCodeBlockPlugin, createHashtagPlugin, createHtmlPlugin];
const ON_LOAD_CONTENT_EVENT = 'onLoadContent';
const ON_CONTENT_CHANGE_EVENT = 'onContentChange';

window.addEventListener(ON_LOAD_CONTENT_EVENT, (e) => {
  ReactDOM.render(
    <React.StrictMode>
      <ContentEditor content={e.detail} />
    </React.StrictMode>,
    document.getElementById('controlAddIn')
  );
})

class ContentEditor extends React.Component {
  constructor(props) {
    super(props);

    this.refsEditor = React.createRef();
    if (this.props.content) {
      const contentState = convertFromRaw(this.props.content);
      this.state = {
        editorState: EditorState.createWithContent(contentState),
      }
    } else {
      this.state = { editorState: EditorState.createEmpty(), };
    }

    this.onLoadContent = this.onLoadContent.bind(this);
    window.addEventListener(ON_LOAD_CONTENT_EVENT, this.onLoadContent)
  }

  onLoadContent(e) {
    if (!e.detail)
      return;

    const contentState = convertFromRaw(e.detail);
    this.setState({
      editorState: EditorState.createWithContent(contentState),
    });
  }

  onChange = editorState => {
    this.setState({
      editorState,
    });

    const rawContent = convertToRaw(editorState.getCurrentContent());
    let event = new CustomEvent(ON_CONTENT_CHANGE_EVENT, { detail: rawContent });
    window.dispatchEvent(event);
  };

  componentDidMount() {
    this.refsEditor.current.focus();
  }

  componentWillUnmount() {
    window.removeEventListener(ON_LOAD_CONTENT_EVENT, this.onLoadContent);
  }

  render() {
    return (
      <div className='content-editor-container'>
        <RichContentEditor
          ref={this.refsEditor}
          plugins={PLUGINS}
          onChange={this.onChange} editorState={this.state.editorState} />
      </div>
    );
  }
}

export default ContentEditor;

Install the following packages to use "wix-rich-content-editor":

npm install classnames
npm install wix-rich-content-editor
npm install wix-rich-content-plugin-code-block
npm install wix-rich-content-plugin-hashtag
npm install wix-rich-content-plugin-html
npm install wix-rich-content-plugin-link

Custom Events (content-editor-events.js)

Custom events are used to communicate ReactJS component with Control Add-in.


onLoadContent: this event tells React Component that it has to render new content.

onContentChange: this event tells Control Add-in that the content is modified in the React Component.

function onLoadContent(content) {
    const event = new CustomEvent('onLoadContent', { detail: content });
    window.dispatchEvent(event);
}

window.addEventListener('onContentChange', function (e) {
    const content = e.detail;
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('OnContentChange', [content])
});

window.LoadContent = function (content) {
    if (JSON.stringify(content) == '{}')
        onLoadContent(null);
    else
        onLoadContent(content);
}


Control Add-in (ContentEditor.ControlAddin.al)

The above "gulpfile.js" creates "content-editor.js", "content-editor.css" files in "app/src/ContentEditor" folder when ReactJS application's build command is executed. These files should be used in Scripts and StyleSheets in Control Add-ins.

controladdin ContentEditor
{
    RequestedHeight = 300;
    VerticalStretch = true;
    VerticalShrink = true;
    HorizontalStretch = true;
    HorizontalShrink = true;
    Scripts =
        './src/ContentEditor/js/content-editor-events.js',
        './src/ContentEditor/js/content-editor.js';
    StyleSheets = './src/ContentEditor/css/content-editor.css';

    event OnContentChange(content: JsonObject)
    procedure LoadContent(content: JsonObject)
}


Customer Card (CustomerCardExt.PageExt.al)

Using "ContentEditor" Control Add-in, the "About" and the "Twitter" group controls are added after the "General" group.

This will allow to write rich text content in the "About" and the "Twitter" groups.


pageextension 50120 CustomerCardExt extends "Customer Card"
{
    layout
    {
        addafter(General)
        {
            group(About)
            {
                Caption = 'About';

                usercontrol("ContentEditor"; ContentEditor)
                {
                    trigger OnContentChange(Content: JsonObject)
                    var
                        ContentText: Text;
                        OStream: OutStream;
                    begin
                        Detail.CreateOutStream(OStream, TextEncoding::UTF8);
                        Content.WriteTo(OStream);
                        Modify();
                    end;
                }
            }
            group(Twitter)
            {
                Caption = 'Twitter';

                usercontrol("Twitter ContentEditor"; ContentEditor)
                {
                    trigger OnContentChange(Content: JsonObject)
                    var
                        ContentText: Text;
                        OStream: OutStream;
                    begin
                        Twitter.CreateOutStream(OStream, TextEncoding::UTF8);
                        Content.WriteTo(OStream);
                        Modify();
                    end;
                }
            }
        }
    }

    trigger OnAfterGetCurrRecord()
    begin
        LoadAbout();
        LoadTwitter();
    end;

    local procedure LoadAbout()
    var
        JObject: JsonObject;
        IStream: InStream;
    begin
        CalcFields(Detail);
        if Detail.HasValue() then begin
            Detail.CreateInStream(IStream, TextEncoding::UTF8);
            JObject.ReadFrom(IStream);
        end;

        CurrPage.ContentEditor.LoadContent(JObject);
    end;

    local procedure LoadTwitter()
    var
        JObject: JsonObject;
        IStream: InStream;
    begin
        CalcFields(Twitter);
        if Twitter.HasValue() then begin
            Twitter.CreateInStream(IStream, TextEncoding::UTF8);
            JObject.ReadFrom(IStream);
        end;

        CurrPage."Twitter ContentEditor".LoadContent(JObject);
    end;
}

Code in Action

The following screenshot shows "About" and "Twitter" tabs with rich text content embeded.


Conclusion

The sample ReactJS Component used in this post is Wix Rich Content. This is one of my favorite React Component for rich text editing. For Control add-ins, you can create your own React Component, or use any existing component. To match the Business Central UI you can use the Fluent UI framework (Business Central also uses this framework internally).


References:


Source Code

You can download the complete source code at GitHub


Happy Coding!!!


#MSDyn365 #MSDyn365BC #BusinessCentral #DynamicsNAV #ReactJS