Introducing AppFormer.js: React components on Red Hat Business Central

Introducing AppFormer.js: React components in Red Hat Business Central

Recently AppFormer.js initiative PRs were merged into KIE master branches. This means that it’s now possible to write React componentes and use them inside Business Central! 🎉

In this post, I’ll show how we achieved this and what needs to be done to create your own AppFormer.js components using React and integrate them in Business Central.

Content

  1. The programming model
  2. Setting up your environment
  3. Creating a Maven module for your NPM package
  4. Creating your AppFormer.js component
  5. Integrating into Business Central
  6. Running and live-reloading your changes during development

The programming model

AppFomer.js was created with the goal of being a next generation of the AppFormer programming model. By implenting a well-defined contract, components can be reused by different platforms and abstract their underlying technologies.

If you’re familiar with UberFire, you know that Perspectives, Screens, Editors (and others) are what make this programming model consistent. With AppFormer.js, it’s no different. We chose Perspectives and Screens as the first two contracts to be defined, but it can be extended to cover more of it, like Editors, for instance.

In this first moment, we built these contracts having React and GWT components in mind so we could end up with an abstraction that covered both bases. We’re also going to use TypeScript in order to keep our types explicit and have a good understanding of the programming model.

So what does an AppFormer.js component look like? It’s pretty straightforward. Imagine you have a React component like this.

import * as React from "react";
interface Props {
exposing: (self: MyReactComponent) => void;
}
interface State {
someData: any[];
}
class MyReactComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { someData: [] };
}
public fetchDataAndUpdate() {
//fetches some data and updates state
}
public render() {
return <div></div>;
}
}
view raw block1.tsx hosted with ❤ by GitHub

Turning it into an AppFormer.Screen or AppFormer.Perspective is simple:

import * as React from "react";
import * as AppFormer from "appformer-js";
export class MyScreen extends AppFormer.Screen {
private screen: MyReactComponent;
constructor() {
super("my-screen");
this.af_isReact = true;
this.af_componentTitle = "MyScreen title";
}
public af_onOpen(): void {
this.screen.fetchDataAndUpdate();
}
public af_componentRoot(): AppFormer.Element {
return <MyReactComponent exposing={self => (this.screen = self)} />;
}
}
AppFormer.registerScreen(new MyScreen());
view raw block2.tsx hosted with ❤ by GitHub

A few things are worth noting in these examples:

  1. this.af_isReact = true is what indicates that a component is a React component.
  2. the super constructor call is what defined the unique id of the component.
  3. AppFormer.js has its own lifecycle. I encourage you to explore the AppFormer.Screen definition to see which methods are available.
  4. React components that translate into AppFormer.js components must be class components, so you can integrate it with AppFormer.js lifecycle.
  5. This prop called self is what exposes the React component to AppFormer.js. Although that is an antipattern, it’s very useful here. Be aware that you should not abuse this construction and always try and keep the single directional data-flow consistent.
  6. AppFormer.register[Screen/Perspective] is what actually creates your components. Remember to call it!

Setting up your environment

If you already have the kiegroup repositories set up, you can skip this section.

First, make sure you have these:

Now clone this repo https://github.com//tiagobento/kiegroup-all and follow the instructions on the README.md file. Instead of a normal mvn clean install, use this command:

mvn clean install -Denforcer.skip -Dgwt.compiler.skip -DskipTests -Dgwt.skipCompilation

It will speed up the compilation a bit.

You should see a BUILD SUCCESS message.

Great!

We’re ready to create a new Mavenzied NPM Package integrated with Business Central’s build!

Creating a Maven module for your NPM package

We’ll be creating a very simple AppFormer.Screen inside Business Central’s Library, just like the new Spaces Screen. But before we start coding, we need to configure our Mavenized NPM package inside Business Central’s build. Because Business Central’s Library is inside kie-wb-common, that’s where we’re going to add our new module.

The first thing we need is an NPM package where our TypeScript code will be, I’ll provide an starter NPM package configuration with the tools we encourage you to use, but feel free to make your own configuration. We’re gonna call our NPM package kie-wb-common-library-afjs-first-screen.

Two things are very important, though:

  1. You must use the same react and react-dom versions that Business Central provides: 16.6.0
  2. React and ReactDOM themselves must NOT be inside your final component bundle, so if you’re using Webpack, make sure that both react and react-dom are marked as external.

One other very important thing is that appformer-js must be fetched using mvn, not npm. How does that work? We create a pom.xml file that will fetch the org.ubefire:appformer-js:[version] JAR for us and will extract it inside a directory on the root of our NPM package. Then we will alter our NPM package’s package.json to point to appformer-js as a local NPM package instead of a NPM package coming from the NPM registry. Like this:

"dependencies": {
"appformer-js": "./target/appformer-js"
}
view raw block4.json hosted with ❤ by GitHub

So you if feel more comfortable downloading a starter Mavenized NPM package, click here.

If you want to configure it by yourself, make sure to call your directory kie-wb-common-library-afjs-first-screen.

Now, move the kie-wb-common-library-afjs-first-screen directory to kiegroup-all/kie-wb-common/kie-wb-common-screens/kie-wb-common-library.

We just have to change kiegroup-all/kie-wb-common/kie-wb-common-screens/kie-wb-common-library/pom.xml to see our new module, so apply this:

<modules>
<module>kie-wb-common-library-api</module>
<module>kie-wb-common-library-backend</module>
<module>kie-wb-common-library-client</module>
<module>kie-wb-common-library-spaces-screen</module>
+ <module>kie-wb-common-library-afjs-first-screen</module>
</modules>
view raw block5.diff hosted with ❤ by GitHub

On kiegroup-all root, run:

mvn clean install -DskipTests -f kie-wb-common/kie-wb-common-screens/kie-wb-common-library/pom.xml.

You should see a BUILD SUCCESS message.

Great!

We already have our NPM package configured and integrated as a Maven module inside kiegroup/kie-wb-common build. Now it’s time for us to create our AppFormer.js component inside it!

Creating your AppFormer.js component

On your favorite editor, open kie-wb-common-library-afjs-first-screen/src/index.tsx. For the sake of simplicity, we’re going to create our AppFormer.Screen and our sub-components on the same file. You can download the file here.

The first thing we have to do is create a class extending AppFormer.Screen an make sure to register it. Like so:

import * as React from "react";
import * as AppFormer from "appformer-js";
export class AfjsFirstScreen extends AppFormer.Screen {
constructor() {
super("AfjsFirstScreen");
this.af_isReact = true;
this.af_componentTitle = "AppFormer.js First Screen";
}
public af_componentRoot() {
return <div>I am alive!</div>;
}
}
AppFormer.register(new SpacesScreenAppFormerComponent());
view raw block6.tsx hosted with ❤ by GitHub

Now, we can get creative and create a React component to go along with our screen. If you’re familiar with React tutorials, you know that we’re creating a Counter component 🙂

interface Props {
exposing: (self: CounterCard) => void;
}
interface State {
count: number;
}
class CounterCard extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { count: 0 };
this.props.exposing(this);
}
public reset() {
this.setState({ count: 0 });
}
private up() {
this.setState(state => ({ count: state.count! + 1 }));
}
private down() {
this.setState(state => ({ count: Math.max(state.count! 1, 0) }));
}
public render() {
return (
<div className={"col-xs-12 col-sm-6 col-md-4 col-lg-3"}>
<div className={"card-pf card-pf-view"}>
<div className={"card-pf-body"}>
<div>
<h2 className={"card-pf-title"}>
<CounterButton label={"+"} onClick={() => this.up()} />
<CounterButton label={"-"} onClick={() => this.down()} />
</h2>
<h5>Click to change count!</h5>
</div>
<div className={"right"}>
<span className={"card-pf-icon-circle"}>{this.state.count}</span>
</div>
</div>
</div>
</div>
);
}
}
function CounterButton(props: { onClick: () => void; label: string }) {
return (
<button
style={{ minWidth: "30px", marginRight: "5px" }}
className={"btn btn-primary"}
onClick={props.onClick}
>
{props.label}
</button>
);
}
view raw block7.tsx hosted with ❤ by GitHub

More than just a Counter, this is a Counter Card, as it contains CSS that will match Business Central’s look & feel. At last, we can use the CounterCard component we just created inside our AppFormer.Screen. So apply these changes to AfjsFirstScreen.

export class AfjsFirstScreen extends AppFormer.Screen {
+ private counter: CounterCard;
constructor() {
super("AfjsFirstScreen");
this.af_isReact = true;
this.af_componentTitle = "AppFormer.js First Screen";
}
+ public af_onClose(): void {
+ alert("AppFormer.js First Screen was closed.");
+ }
+
+ private resetCounter() {
+ this.counter.reset();
+ }
public af_componentRoot() {
return <div>I am alive!</div>;
+ return (
+ <>
+ <div className={"library container-fluid"}>
+ <div className={"row page-content-kie"}>
+ <div className={"toolbar-pf"}>
+ <div className={"toolbar-pf-actions"}>
+ <div className={"toolbar-data-title-kie"}>
+ AppFormer.js First Screen
+ </div>
+ <div className={"btn-group toolbar-btn-group-kie"}>
+ <button
+ className={"btn btn-default"}
+ onClick={() => this.resetCounter()}
+ >
+ Reset counter
+ </button>
+ </div>
+ </div>
+ </div>
+ <div className={"container-fluid"}>
+ <div className={"library container-fluid container-cards-pf"}>
+ <div className={"row row-cards-pf"}>
+ <CounterCard exposing={self => (this.counter = self)} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+ }
}
view raw block8.diff hosted with ❤ by GitHub

Like the CounterCard component we just created, these changes also contain CSS to match Business Central’s look & feel.

On kiegroup-all root, run:

mvn clean install -DskipTests -f kie-wb-common/kie-wb-common-screens/kie-wb-common-library/kie-wb-common-library-afjs-first-screen/pom.xml.

You should see a BUILD SUCCESS message.

Great!

Your first AppFormer.js component is ready to be used in Business Central! Let’s do this with some very simple configurations.

Integrating into Business Central

For the sake of this tutorial, we’re not going to use the full sized Business Central version. Instead, we’re going to work with Drools Workbench, which is an application that has some of the features Business Central and is optimized for development. Don’t worry! The process of adding an AppFormer.js component on Business Central is exactly the same.

Note that because Business Central and Drools Workbench both already have AppFormer.js components integrated, there’s already a line of code to “activate” the AppFormer.js capabilities. It’s usually located on an @EntryPoint class for AppFormer.js to be available at the very beginning of the app. You can take a look at the @PostConstruct-annotated method on org/drools/workbench/client/DroolsWorkbenchEntryPoint.java if you want. The argument to AppFormerJsBridge#init should be the exact same name of the GWT module being compiled because we’ll use that to assemble the URL to lazyly load the AppFormer.js components.

The repository where we’re going to work now is drools-wb, and the module is drools-wb-webapp. Open it on your favorite editor or IDE and apply the changes below.

On kiegroup-all/drools-wb/drools-wb-webapp/pom.xml, add this execution configuration on the maven-dependency-plugin executions tag:

<execution>
<id>unpack-afjs-first-screen</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.kie.workbench.screens</groupId>
<artifactId>kie-wb-common-library-afjs-first-screen</artifactId>
<type>jar</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/kie-wb-common-library-afjs-first-screen</outputDirectory>
</artifactItem>
</artifactItems>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
view raw block9.xml hosted with ❤ by GitHub

On the same file, add this execution configuration on the maven-resources-plugin executions tag.

<execution>
<id>copy-afjs-first-screen</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/org/uberfire/jsbridge/public</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/kie-wb-common-library-afjs-first-screen/META-INF/resources/webjars/kie-wb-common-library-afjs-first-screen/${project.version}</directory>
<includes>
<include>kie-wb-common-library-afjs-first-screen-bundle.js</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
view raw block10.xml hosted with ❤ by GitHub

These togheter will unpack kie-wb-common-library-afjs-first-screen JAR, which contains our component’s bundled JavaScript and copy this bundle to the application’s output directory. It’s conventioned that every AppFormer.js component bundle must be under /org/uberfire/jsbridge/public, or it won’t work.

Last configuration we have to do before changing the application to have some way to access our new screen is to pur our component’s information on AppFormer.js registry. All AppFormer.js components are lazily loaded, so we have to register it so AppFormer.js knows when and what to fetch.

On kiegroup-all/drools-wb/drools-wb-webapp/src/main/resources/org/uberfire/jsbridge/public/AppFormerComponentsRegistry.js, add this entry:

"AfjsFirstScreen": {
"type": "screen",
"source": "kie-wb-common-library-afjs-first-screen-bundle.js",
"params": {}
}
view raw block11.json hosted with ❤ by GitHub

We’re almost there. To make our screen accessible, we’re going to add an item to the Menu of the application. Go to org.drools.workbench.client.DroolsWorkbenchEntryPoint and add these lines on the setupMenu method.

menuBar.addMenuItem(
"AfjsFirstScreen",
"AppFormer.js First Screen",
null,
() > placeManager.goTo("AfjsFirstScreen"),
MenuPosition.LEFT);
view raw block12.java hosted with ❤ by GitHub

Now let’s bring the application up so we can (finally! 🎉) see our new screen.

On kiegroup-all root, run:

mvn clean gwt:run -f drools-wb/drools-wb-webapp

Wait for the “errai bus started.” message on your console and go to “ http://localhost:8888“. Log in with admin/admin and wait for the compilation to finish. When the application starts, go to Menu > AppFormer.js First Screen. Voilà! There it is. Click around so you can see that our component actually works.

That’s what you should see:

Great!

Your AppFormer.js component is up and running inside Business Central! Now let’s see how to speed up the development workflow.

Running and live-reloading your changes during development

This only works on macOS and Linux at the moment.

If we want to change something on our recently created AppFormer.js component without bringing Business Central (or Drools Workbench in this case) down, everything we have to do it to change the bunlded JavaScript file inside the running application. To do that, we can simply execute a “copy” command. With a bit of bash scripting knowledge, we can achieve this:

Go to kiegroup-all/kie-wb-common/kie-wb-common-screens/kie-wb-common-library/kie-wb-common-library-afjs-first-screen and run:

npx watch ‘yarn run live-reload-on ~/redhat/kiegroup-all/drools-wb/drools-wb-webapp’ ./src

This will run the watch command inside /node_modules/.bin and execute the command yarn run live-reload-on ~/redhat/kiegroup-all/drools-wb/drools-wb-webapp everytime something on ./src changes. The argument yo yarn run live-reload-on is the location of your running application. In my case it was ~/redhat/kiegroup-all/drools-wb/drools-wb-webapp. Pretty handy!

If you’re curious you can see how it’s done on kie-wb-common-library-afjs-first-screen/package.json.

To see that it works, change something on kie-wb-common-library-afjs-first-screen/src/index.tsx, wait for the build to finish, go to your browser and refresh the page. After navigating to AppFormer.js First Screen, you should see your changes there.

Great!

Now you can easily change and see the changes you made inside Business Central!


Introducing AppFormer.js: React components on Red Hat Business Central was originally published in kie-tooling on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post was original published on here.
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments