SmartUI Routers in OpenText Content Server

# Some background

The OpenText Content Server SmartUI makes it possible to extend the router with custom routes. It allows custom URLs, which are linkable, bookmarkable, and shareable.

Consider the following example of a custom route:

https://myserver/otcs/cs.exe/app/my-custom-route/1235?arg1=abc&arg2=def

In this example, we have 12345 as a URL parameter, and arg1=abc&arg2=def as query parameters.

In order for a route to work, we need to be able to:

  • navigate directly to the URL via a bookmark, page refresh, or link from an external location,
  • programmatically navigate to the URL within SmartUI,
  • parse the URL and query parameters, and
  • provide the parsed data to the view for data loading and rendering.

Applications such as Vue Router (opens new window) and Nuxt (opens new window) handles most of this for you. You simply define the routes, its properties, and it works.

Adding a router in SmartUI requires you to write the management code yourself. It's a complex process of extending modules, interlinking them with listeners to react to change events, and writing code to generate the URL. It's complex and repetetive, which makes it highly error prone. Adding a new route requires a considerable amount of code.

To simplify this, I wrote a plugin, which abstracts the complex management parts into a simple interface. In particular::

  • it defines a new route with a few lines of code,
  • provides a simple API for programmatic navigation to the route, and
  • automatically extracts the URL and query parameters to make the data available to the view.

Let me show you how it works.

# Simplifying the SmartUI router

As with any new route, you need to create a csui/pages/start/perspective.routing extension (i.e., using an CSUI::CSUIExtension orphan in your OSpace) to register the plugin with SmartUI. For this example, I named the extension geniusui/perspective.routers/my.perspective.router.

{
  "csui/pages/start/perspective.routing": {
    "extensions": {
      "geniusui": ["geniusui/perspective.routers/my.perspective.router"]
    }
  }
}
1
2
3
4
5
6
7

The router rules are extended from rhcore/routing/rhcore.router.register. This is an extension of csui/pages/start/perspective.router, which augments its functionality to handle the management of the route.

define("geniusui/perspective.routers/my.perspective.router", [
  "rhcore/routing/rhcore.router.register",
  "geniusui/routes/my.route.view",
], function (RHCoreRouterRegister, MyGeniusView) {
  return RHCoreRouterRegister.extend({
    RHCoreRoutes: [
      {
        path: "my-custom-route",
        name: "myCustomRoute",
        view: MyGeniusView,
        meta: { somekey: "value1" },
      },
      {
        path: "my-custom-route/:id",
        name: "myCustomRouteId",
        view: MyGeniusView,
        meta: { somekey: "value2" },
      },
    ],
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

The routes are defined in the RHCoreRoutes property, and for each route we have the following parameters:

  • path - the url of the route, appended to the SmartUI base url (e.g., cs.exe/app/my-custom-route or cs.exe/app/my-custom-route/12345),
  • name - a unique name for the route (useful for programmatic navigation),
  • view - a Marionette view (e.g., a Marionette.ItemView instance), and
  • meta - any extra data for the view (which is useful when using the same view in different contexts).

The router extension automatically passes additional metadata in the options parameter of the view constructor, which is useful for rendering. For example:

define("geniusui/routes/my.route.view", ["csui/lib/marionette"], function (
  Marionette,
) {
  return Marionette.ItemView.extend({
    constructor: function (options) {
      Marionette.ItemView.prototype.constructor.apply(this, arguments);

      const { router, sessionData, route } = options.rhcore;
      // ...
    },
  });
});
1
2
3
4
5
6
7
8
9
10
11
12

The options.rhcore object contains three values:

  • router - an object that can be used for programmatic navigation,
  • sessionData - session information, and
  • route - information about the current route.

For example, the router object can be used for navigation:

// navigates to a named route, e.g., /app/my-custom-route/67890?hello=1
router.navigateTo({
  name: "myCustomRoute",
  params: { id: "67890" },
  query: { hello: 1 },
});

// open a node, e.g., /app/nodes/12345
router.openDataId(12345);
1
2
3
4
5
6
7
8
9

The sessionData object contains the Content Server baseUrl, support directory path, and an otcsticket. These can be used with @kweli/cs-rest (opens new window) to make REST requests to Content Server.

Finally, route contains information about the route, which can be used to load data and render the view:

{
  "id": "myCustomRouteId",
  "params": { "id": "12345" },
  "query": {
    "arg1": "abc",
    "arg2": "def"
  },
  "meta": {
    "somekey": "value2"
  }
}
1
2
3
4
5
6
7
8
9
10
11

# Usage with React or Vue

In a previous blog post, I discussed the possibility of using SmartUI with a modern framework such as React, Vue, or Angular. The idea is to compile the project to UMD for use in SmartUI. The approach is still valid, but the same can also be accomplished by compiling the project to an ES6 Javascript module, which can then be imported from SmartUI using an asynchronous import.

As before, the project should export a function to mount the application. The function can also accept context parameters, such as the sessionData and route discussed above. The router object could also be passed in to allow SmartUI navigation from within the application.

For example, the view could look something like this:

Marionette.ItemView.extend({
  onRender: function ({ options }) {
    const { router, sessionData, route } = options.rhcore;

    import("./myVueOrReactApp.mjs").then(({ default: mount }) => {
      this.app = mount(this.el, { router, sessionData, route });
    });
  },
  onBeforeDestroy: function () {
    // cleanup
    this.app?.unmount();
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13

The protyping so far has shown this to work well. What's also great is that apps can be made universal, which allows them to work from either SmartUI or the classic UI. Perhaps I'll write a followup post about that sometime.