SmartUI development with a modern JavaScript framework

In my last blog post I demonstrated it is possible to write an OpenText Content Server SmartUI widget without the SmartUI SDK. In this blog post I continue the discussion and show how it's possible to bring a modern JavaScript framework into the SmartUI.

It's important to remember that modern JavaScript frameworks do more or less the same thing: they update the Document Object Model (DOM). In the end it's all JavaScript, and nothing prevents different frameworks from managing different sections of the DOM.

# Why use a different framework?

The OpenText Content Server SmartUI is based on Backbone.js (opens new window), Marionette.js (opens new window), and Bootstrap 3 (opens new window). There is nothing inherently wrong with this, but modern frameworks are more powerful and easier to work with. I also suspect there are developers who would prefer to use what they know and not be forced to learn an aging framework like Backbone or Marionette.

However, the biggest advantage to not using the SmartUI framework is that it allows you to write code independent of SmartUI. This ensures some degree of forwards compatibility, but also allows a widget to be used in different contexts (e.g., the classic interface). You still need to write code to bring the widget into the SmartUI, but the bulk of the code can be developed independent of it.

# How to build for SmartUI

My experience is with Vue.js (opens new window), but this should also be possible with React (opens new window), Angular (opens new window), Aurelia (opens new window), or any other modern JavaScript framework.

Many of these frameworks provide a way to transpile for different targets. For example, I can write an application in Vue.js and create a build that runs as a single page application. However, I can also transpile the application into the Universal Module Definition (opens new window) (UMD) pattern. UMD is interesting since it allows the same code to be loaded with a <script> tag, imported in Node.js (using CommonJS), or with the asynchronous module definition (AMD). UMD is compatible with RequireJS (which uses AMD), which is the module loader used by SmartUI.

This means we should be able to compile a Vue.js, React, Angular, etc. application into UMD and use it directly in SmartUI.

Well, almost. SmartUI uses RequireJS, but there is a small change. In my last blog post I expressed uncertainty as to why the SmartUI SDK maps the define and require global functions to csui.define and csui.require. I have since learned that creating a namespace (opens new window) is common practice in RequireJS for isolating code and preventing conflicts on a page. That's fine, and it seems I can just rename the define and require function calls in my build to make it work with SmartUI. It's a small hack that could certainly be automated in a build process.

Most JavaScript frameworks operate by mounting at a specific location in the DOM. This can be a simple <div> element in the HTML page:

<div id="app"></div>
1

A Vue.js application needs to be instructed where to mount itself. This is often hard coded in the entry point when creating a single page application:

new Vue({
  render: (h) => h(App),
}).$mount("#app"); // render where id="app"
1
2
3

Or, in React:

ReactDOM.render(<App />, document.getElementById("app"));
1

An alternative approach is to have the entry point return a function, which permits the mount point to be set by the calling function (this will be more relevant a bit later). For example:

// The el parameter can be a DOM element or selector
export default function (el) {
  new Vue({
    render: (h) => h(App),
  }).$mount(el);
}
1
2
3
4
5
6

Once this gets compiled into UMD it becomes a simple matter of loading the script and calling the function to mount the application. For example, with a regular HTML page and <script> tag:

<!DOCTYPE html>
<html>
  <head>
    <!-- Load the module, which creates a window.mymodule global variable -->
    <script src="./mymodule.umd.js"></script>
    <link href="./mymodule.css" rel="stylesheet" />
  </head>
  <body>
    <!-- Define the mount point -->
    <div id="app"></div>

    <!-- Mount the application at the mount point -->
    <script>
      window.mymodule.default("#app");
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Using a UMD build in SmartUI

A UMD build can also be used with SmartUI after editing and renaming the define and require calls (all within the first few lines of the build).

Let's take the sample widget from my last blog post, modify it to load the UMD build, and mount it.

Copy the modified UMD build to mymodule/smartui/bundles/mymodule.umd.js and edit the mymodule-all.js file:

csui.define(
  "mymodule/widgets/mymodule/mymodule.view",
  ["csui/lib/marionette", "rhcore/bundle/mymodule.umd"],
  function (Marionette, mymodule) {
    return Marionette.ItemView.extend({
      className: "mymodule-smartui",
      template: function () {
        return '<div id="mymodule">Loading...</div>';
      },
      onRender: function () {
        mymodule.default("#mymodule");
      },
    });
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Here's what is happening: The UMD build is loaded on line 2 and assigned to the mymodule variable on line 3. On line 11 the function is called to mount the application into the <div> tag defined on line 8.

It should be this is easy, but unfortunately it fails. You would think the onRender function would be called after the render has completed. However, this is not the case. There is a short delay after onRender is called until the template is actually rendered to the DOM. A simple workaround is to poll the existence of the #mymodule DOM element before mounting (please let me know if there is an easier way):

onRender: function() {
	this.interval = setInterval(function() {
		var element = document.getElementById('mymodule')

		if (element) {
			mymodule.default('#mymodule')
			clearInterval(this.interval)
		}
	}, 100)
}
1
2
3
4
5
6
7
8
9
10

That's it. The application is now running in SmartUI.

# What about context?

An application can't make REST API calls to Content Server unless it has an authentication token and knows where to connect. For this we need to pass some context:

  • an otcsticket token, which is required for Content Server REST API calls;
  • the baseURL, which defines where to make the REST API calls; and
  • the img path, which is useful for loading images or other resources from the support directory.

This can be obtained by adding csui/utils/contexts/factories/connector and csui/utils/url to the list of dependencies and assigning it to the ConnectorFactory and Url variables. Then add a function to the Marionette ItemView to aggregate and return the values:

fetchContext: function() {
	var context = this.options.context
	var connector = context.getModel(ConnectorFactory)
	var url = connector.connection.url
	var baseURL = new Url(url).getCgiScript()
	var ticket = connector.connection.session.ticket
	var supportPath = connector.connection.supportPath

	return {
		baseURL: baseURL,
		img: supportPath,
		otcsticket: ticket
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

The return value can be passed directly to the mount function of the UMD build:









 





onRender: function() {

	var context = this.fetchContext()

	this.interval = setInterval(function() {
		var element = document.getElementById('mymodule')

		if (element) {
			mymodule.default('#mymodule', context) // pass the context to the application
			clearInterval(this.interval)
		}
	}, 100)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Of course, this will require the application to be modified to accept the context:

export default function (el, context) {
  // do something with the the otcsticket, baseURL,
  // and img to make it available in your application

  new Vue({
    render: (h) => h(App),
  }).$mount(el);
}
1
2
3
4
5
6
7
8

It's no accident that context is compatible with the @kweli/cs-rest package I introduced in Simplifying the OpenText Content Server REST API.

Now your application can make calls to the Content Server REST API.

# Styling your application

The SmartUI is bundled with a modified version of Bootstrap 3 (opens new window). It provides a number of binf-* and csui-* classes you can use to style your application. However, if you've come this far then you likely want to use your own CSS or component library to keep your code independent of SmartUI.

The problem is that the SmartUI styles can mess with your style sheet (or the style sheet of the library you're using). This is just the nature of cascading style sheets.

I personally use Vuetify (opens new window), which is a Material UI component library for Vue.js. It's awesome, but injecting a Vuetify application into SmartUI introduces rendering errors due to the SmartUI CSS styles leaking into the application.

The problem comes down to CSS specificity (opens new window) or CSS precedence. It seems some of the SmartUI styles have a higher precedence than the styles from Vuetify, which causes the errors.

A simple solution is to wrap the application top level element in a <div> and give it an id (e.g., mymodule-smartui-container):

<div id="mymodule-smartui-container">
  <!-- Put your app here -->
</div>
1
2
3

The next step is to use a CSS preprocessor like Less (opens new window) to create a modified copy of the style sheets. The idea is to prefix each CSS rule with the id selector, which gives the rules a higher precedence. For example, with Vuetify:

#mymodule-smartui-container {
  @import (less)
    "<project path>/node_modules/@mdi/font/css/materialdesignicons.css";
  @import (less) "<project path>/node_modules/vuetify/dist/vuetify.min.css";

  // Vuetify uses 'inherit', which isn't compatible with SmartUI
  * {
    box-sizing: border-box;
  }
}
1
2
3
4
5
6
7
8
9
10

This small change increases the precedence of the Vuetify styles, which eliminates the rendering errors within SmartUI.

# Wrapping up

I find it satisfying being able to develop my widgets with a modern JavaScript framework independent of SmartUI. It makes prototyping and development much faster, and it's only near the end that I need to integrate my application into SmartUI.