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