Understanding Jahia's component registry

November 14, 2023

Introducing the Jahia component registry

An essential element of Jahia’s UI is the component registry. To make the UI truly modular, there needs to be a way for components to “insert” themselves into existing navigation menus or other types of component lists. 

Jahia’s solution to this need is the component registry. For those familiar with OSGi, it is similar in design to the Service Registry. For those who are not, you can think of it as a component hashmap where the key is the component type+key, and the value is a list of component instances added for that type. We can then retrieve all the registered components for a given type. This makes it possible, for example, to retrieve all the menu entries of type primary-nav-item for the first level of navigation in Jahia’s UI.

Component registry UI Example

The following example shows how the navigation entries for Jahia’s top navigation are registered in the registry. On the left side of the illustration, you can see the Javascript code used to register the component in the registry; on the right side, you can see the associated render. As you can see, this system makes it possible to define the various navigation elements at runtime, making it very flexible and extendable.

ui-component-registry-nav-example.png

Examples of how Jahia's UI uses the component registry

The following list is just a few examples of how Jahia's UI uses the component registry. It is in no way exhaustive, especially since UI components increase or add registry usage on each UI update.

  • Jahia navigation utilizes the component registry to know what to display

  • jContent and Content Editor uses the registry to know which actions to display for the current user

  • Content Editor uses the registry to know how to render fields

  • Other UI components use registry components to be extendable/modifiable

Main features of the component registry

The interface of the Javascript component registry looks like this:

class Registry {
    addOrReplace(type, key, arguments);
    add(type, key, arguments)
    get(type, key);
    remove(type, key);
    find(filters);
}
let registry = new Registry();
export {registry};

Using the registry interface, you can:

  • Add or replace components into the registry using a type and a key. The add function will fail if an entry with that type and key already exists.

   registry.add('action', 'backButton', goBackAction, {
        buttonIcon: <ArrowLeft/>,
        buttonLabel: 'content-editor:label.contentEditor.edit.action.goBack.name',
        targets: ['editHeaderPathActions:1'],
        showIcons: true
    });
    registry.addOrReplace('adminRoute', 'manageModules', {
        targets: ['administration-server-systemComponents:10'],
        requiredPermission: 'adminTemplates',
        icon: null,
        label: 'module-manager:modules.label',
        isSelectable: true,
        iframeUrl: window.contextJsParameters.contextPath + '/cms/adminframe/default/en/settings.manageModules.html?redirect=false'
    });

After the type and key, the arguments will be combined using a special function called composeServices. Basically, this function will “merge” objects into a single object using the following rules:

  • Any different properties will simply be added

  • If a property has the same name in both objects, the following will happen depending on the property type:

    • If it’s a single property value, the last value of the last object in the arguments will be the one that is kept; all the others are discarded

    • If it’s an array, they will be concatenated in the order of the arguments

    • If it’s a function, the function in the second object will receive the first function as an argument to call it in its body. Here is an example:

registry.add('init', 'functionExample', {
            onInit: context => {
                context.tata = 45;
            }
        }, {
            onInit: (context, previous) => {
                previous(context);
                context.toto = 42;
            }
        });
const context = {};
registry.get('init', 'functionExample').onInit(context);
expect(context).toEqual({toto: 42, tata: 45});
  • Get all the components for a type and key

registry.get('action', 'menuAction')
  • Remove components using a type and key or just a type (which will remove all the entries of that type

registry.remove('action', 'menuAction')
registry.remove('action')
  • And find components using filters

registry.find({target: 'editHeaderTabsActions'});

Accessing the registry

You can access the registry by adding the following dependency in your Javascript NPM package.json file:

    "dependencies": {
        "@jahia/ui-extender": "^1.0.6",

And then, you can simply access the registry using the following example:

import {registry} from '@jahia/ui-extender';
registry.find({target: 'editHeaderTabsActions'});

Or you can access it directly from the shared instance in the window global variable:

window.jahia.uiExtender.registry.find({target: 'editHeaderTabsActions'});

This second method, however, is riskier and will require developers plug in into the initialization code before the render phase (this can be tricky and not detailed here). Also modifying the registry after rendering will not trigger UI re-render so this should be mainly used for accessing components or in pre-render code. This is why the first method is preferred in most cases.

Adding entries in the registry

As registry entries must be added before rendering the UI, it is important that they are done at the right time. To do this, Jahia provides an “init” entry point in the WebPack configuration that looks like this:

        plugins: [
            new ModuleFederationPlugin({
...
                exposes: {
                    '.': './src/javascript/shared',
                    './init': './src/javascript/init',
                },
...
            }),

The init.js file should look something like this:

import {registry} from '@jahia/ui-extender';
import register from './myApp.register';
export default function () {
    registry.add('callback', 'myApp', {
        targets: ['jahiaApp-init:50'],
        callback: register
    });
}

You could wonder why a callback is registered instead of directly registering the component in the init.js function. This is because the init functions are all called in parallel for performance reasons. When completed, the callbacks are called in order of priority on the jahiaApp-init target. Using callbacks ensures that the callbacks are called in a predictable and orderly fashion. The register function can then look like this:

    registry.add('primary-nav-item', 'jcontentGroupItem', {
        targets: ['nav-root-top:2'],
        requiredPermission: 'jContentAccess',
        render: () => <MyAppNavItem/>
    });

Registry targets

For a given type, it might be good to organize components into collections, which are here called targets. These collections are often used in menus, and in that case, some control over the order of the elements in the collection is desired. Targets can therefore be appended with a number that will indicate at which position we want to insert the element in the collection.

Targets also make it possible to insert the same component into multiple targets, making it easy to use a component such as an action in multiple UI elements.

In the following example, we illustrate a createContent action component that is of type action and registered in the createMenuActions target at position 3, in the contentActions target at position 3 and in the headerPrimaryActions at position 1.

registry.addOrReplace('action', 'createContent', createContentAction, {
   buttonIcon: <AddCircle/>,
   buttonLabel:
       'content-editor:label.contentEditor.CMMActions.createNewContent.menu',
   targets: ['createMenuActions:3', 'contentActions:3', 'headerPrimaryActions:1'],
   showOnNodeTypes: ['jnt:contentFolder', 'jnt:content'],
   hideOnNodeTypes: ['jnt:navMenuText', 'jnt:page'],
   requiredPermission: ['jcr:addChildNodes'],
   isModal: true
});

Identifying the type and target

Starting with Jahia 8.1.6, when adding a component, or an action, in the registry, you can easily identify the type and the target by looking at the html source of a similar entry: the <li> tag contains the data-registry-key (containing the type)  and data-registry-target (containing the target and position) attributes.

Based on the previous example of the createContent action, which corresponds to the “New content” entry in the jContent context menu, we can easily identify the type and target of the action by looking at the html code:

NewContentAction.png

  • data-registry-key="action:edit"
    • The first value of the data-registry-key corresponds to the type, in this example it is action
    • The second value is the key.
  • data-registry-target="contentActions:2"
    • The first value is the target, in this case contentActions
    • The second value is the position. The position is a float value, therefore you can use decimals if needed.
       

Registry find filters

The registry.find function can take filters to retrieve registry entries. Filters are simply an object with the keys being the property to match and the value is the value to match. Here is an example:

registry.find({type: 'comp', foo: 'bar'});

In this example, we will retrieve all the components that are of type comp and have a property foo that has the value bar.

The target property is handled a bit specifically. If a find by target is specified, the results will be sorted according to the priorities specified on the targets. 

test1 = registry.add('comp', 'test1', {targets: ['bar:1']});
test2 = registry.add('comp', 'test2', {targets: ['bar:3']});
test3 = registry.add('comp', 'test3', {targets: ['bar:2']});
res = registry.find({type: 'comp', target: 'bar'});

In the above example, the components will be in the following order in the res variable: test1, test3, test2

Common registry types

Jahia’s UI uses internal types to build its functionality, and we present some of the most important and common ones here:

  • action: Adds an UI action in the Content editor. Registering an action will make it available in the UI in all the targets it was declared in (and in the specified order). 

  • selectorType : Adds a selector type in the Content editor. Selector types are UIs used to input or select content to be added inside a field. They can range from simple input fields to custom UIs that may be more or less complex. For example, Pickers are used inside selector types to select existing content.

  • adminRoute : add a route and a screen in the administration. The benefit of using this type over the regular ‘route’ type is that it will directly register the component as part of the Jahia administration UI and will not require that you provide a LayoutModule including secondary navigation on the left. The render of this component will be inserted directly in the right panel to build a custom UI. This is usually what most users will want to use to provide custom screens for configuration and/or extension setup.

  • route : add a route for a custom panel. This route will be a primary-level route and require providing the complete application layout using a LayoutModule and a secondary navigation component. For most usages, the adminRoute should be preferred as it requires a lot less setup, and this type should only be used for advanced cases where the complete top-level rendering control is needed.

  • primary-nav-item : add an entry in the primary level navigation, mostly used alongside the route registration

  • callback : adds a function to be called when the app shell initializes. Callbacks can specify a priority to control the order in which they will be called. Callback functions are also guaranteed to be called sequentially; they are not executed in parallel (unlike the init.js scripts, which are all loaded and called in parallel)

iFrame Navigation URLs

For easy integration of existing or legacy UIs, the navigation UI components have the capability of rendering URLs inside an iFrame. In order to use this, you can simply register a navigation entry of adminRoute type with an iframeUrl property that will be used when a user clicks the navigation entry.

    registry.add('adminRoute', 'manageUsers', {
        targets: ['administration-server-usersAndRoles:10'],
        requiredPermission: 'adminUsers',
        icon: null,
        label: 'siteSettings:users.label',
        isSelectable: true,
        iframeUrl: window.contextJsParameters.contextPath + '/cms/adminframe/default/$lang/settings.manageUsers.html'
    });

As you can see in the above example, it is even possible to use parameters such as the site-key and the lang or ui-lang in the URL. 

Component registry debugging & inspection

The best way to inspect the component registry is to use the registered global window property. You may access it through:

window.jahia.uiExtender.registry

You could then perform a find to retrieve all the components of a specific type, as in the following example, to check if all the selectorTypes are correctly registered and available.

ui-debugging-registry-dump.png