UI frameworks: Vue, React, Angular

How to create widgets?

Most parts of the Paperbits engine (designers and rendering) got written using the Knockout framework. However, there are no obstacles to use other frameworks (like Vue, Angular, or React) to create run-time widgets (those running as part of a published website).

It is possible thanks to customElements - one of the JavaScript features introduced in version ES2015 (or ES6). This feature allows running every widget as a small standalone application with its markup, styles, and executable code within the website. 

Frameworks like Angular and Vue support customElements out of the box, and for the other frameworks (like React and Knockout), Paperbits has extensions to make it work. No matter what UI framework gets to use, the whole wiring eventually comes down to defining a component in custom elements registry of the browser, like this:

customElements.define("click-counter", ClickCounter);

Here ClickCounter is a ES6 class that extends HTMLElement:

class ClickCounter extends HTMLElement {
    constructor() {
      super();
    }
}

When registered, it can be just added to the page in the form of a simple HTML tag:

<click-counter initial-count="0"></click-counter>

Once in the DOM, the browser will instantiate it, bindings will kick in, and you'll see your widget in action.

From Paperbits perspective, any run-time component is also just an HTML tag with certain attributes and/or content. In design- and publish-time, the engine takes care of forming the tag and passing it the configuration from editors (which doesn't know anything about the run-time scope or how widgets in it work, its goal is only to render HTML tag). In simple cases, like in the example above, the parameter (initial-count) gets passed in the attribute. If the configuration happened to be more complex, the widget could use an identifier of the required setting(s), and fetch it with from somewhere else, i.e., database.

Now, when we covered the theory behind run-time components, let's see how some of the frameworks could be employed.

In paperbits-demo project, we prepared a simple click-counter widget, which has its run-time part implemented in several UI frameworks, and each of them has a designated folder.

/click-counter
/angular
/ko
/react
/vue

Depending of your choice you need to uncomment respective module registration in demo.runtime.module.ts (this is a file where we declare all our run-time dependencies):

/* Knockout example component */
import { ClickCounterRuntimeModule } from "./click-counter/ko/runtime";

/* Uncomment to switch to Vue example component */
// import { ClickCounterRuntimeModule } from "./click-counter/vue/runtime;

/* Uncomment to switch to React example component */
// import { ClickCounterRuntimeModule } from "./click-counter/react/runtime";

/* Uncomment to switch to Angular example component */
// import { ClickCounterRuntimeModule } from "./click-counter/angular/runtime";

As you can see, Knockout module gets imported by default.

Another place that needs to be adjusted is the design-time widget template (a host for the run-time widget) defined in clickCounter.html file.

<!-- Knockout -->
<click-counter-runtime class="w-100" data-bind="attr: { params: runtimeConfig }"></click-counter-runtime>

<!-- Vue -->
<!-- <click-counter-runtime class="w-100" v-bind:initial-count="10"></click-counter-runtime> -->

<!-- React -->
<!-- <click-counter-runtime class="w-100" initial-count="10"></click-counter-runtime> -->

<!-- Angular -->
<!-- <click-counter-runtime class="w-100" initial-count="10"></click-counter-runtime> -->

When the required HTML tag uncommented, you're almost ready to see it in action. At this time, you'll need to jump to a specific UI framework setup section below.

Vue

1. Install npm packages:

> npm i vue vue-custom-element

2. Add vue-loader and vue$ alias in ./webpack.runtime.js file:

const runtimeConfig = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
...
]
},
...
resolve: {
...
alias: {
"vue$": "vue/dist/vue.esm.js"
}
...
}
}

React

Install npm packages:

> npm i react react-dom --save
> npm i @types/react --save-dev

Angular

1. Install npm packages:

> npm i @angular/common @angular/compiler @angular/core @angular/elements @angular/platform-browser @angular/platform-browser-dynamic zone.js rxjs

2. Add zone.js polyfill to in ./polyfills.ts:

import "zone.js/dist/zone";
import "@webcomponents/custom-elements/src/native-shim";

How to host designer in your app?

Paperbits designer is just another component that bound to a specified HTML tag. Therefore, it is super simple to wrap it up into a component of your UI framework.


React

import * as React from "react";
import * as ko from "knockout";

export class PaperbitsInReact extends React.Component {
private readonly ref = React.createRef<HTMLDivElement>();

constructor(props) {
super(props);
}

public async componentDidMount(): Promise<void> {
ko.applyBindingsToNode(this.ref.current, { component: "app" }, null);
}

public render(): JSX.Element {
return (
<div ref={this.ref}></div>
);
}
}

For full application example refer to examples repo.

Angular

import * as ko from "knockout";
import { Component, ViewChild, ElementRef } from "@angular/core";

@Component({
selector: "paperbits",
template: `<div #ref></div>`
})
export class PaperbitsInAngular {
@ViewChild("ref", { static: true })
public ref: ElementRef;

public async ngOnInit(): Promise<void> {
ko.applyBindingsToNode(this.ref.nativeElement, { component: "app" }, null);
}
}

As you probably noted, in these examples we do the same thing as in startup.design.ts file:

document.addEventListener("DOMContentLoaded", () => {
    setImmediate(() => ko.applyBindingsToNode(document.body, { component: "app" }, null));
});

For full application example refer to examples repo.

Copyright © 2020 Paperbits LLC. All rights reserved.