Extending Jahia UI with new modules

  Written by The Jahia Team
   Estimated reading time:

To declare a new module, you have two possibilities:

  • Simple module with very little to no javascript (e.g: a legacy site settings)
  • More complex modules with react and using Jahia moonstone design system

Common step

Declare your module

Create a jahia.json file under src/main/resources/javascript/apps/jahia.json with the following content:

{
  "jahia": {
    "apps": {
      "jahia": "javascript/apps/register.js"
    }
  }
}

Let's decrypt the most important part "jahia": "javascript/apps/register.js" the key here is the app you want to extend and the value is the path to the file that will load/register your app. This file will be the entry point that will contain the initialization part of your module.

Load/Register your app

You will need to create the JS file you declared in the jahia.json file in this example register.js, and add the logic you need inside. The module can register different extensions in the application. In simple JS module this file can be written manually, in complex modules in will be generated by a JS bundler like webpack. This file should remain small, as the application won't start until it's fully loaded and executed.

Registrations

All extensions are added into the registry by calling the registry.add function. The registry object can be found by importing registry from @jahia/ui-extender. The add function taks at least 3 parameters : the type of extension to register, its unique id, and an object describing the extension.

To ease this process when doing simple JS, we expose a few things in the window.jahia object. As of today in the jahia object you can find 3 things:

  • uiExtender, which contains the registry
  • moonstone, which contains React components and utility functions from the moonstone design system
  • i18n, which contains all the necessary function to handle translations

Routes

React routes can be added in the application as extension. For example the following can be used to declare a new route /linkchecker (path) that will display a linkchecker (render) in the main area (target) :

window.jahia.uiExtender.registry.add('route', 'route-jcontent', {
      targets: ['main:2'],
      path: `/linkchecker`, // Catch /linkchecker urls
      render: function () {           // The render function for my route in this example we want to display an iframe which contains our legacy site settings, to do that we pass the URL to the `getIframeRenderer` function
        return window.jahia.uiExtender.getIframeRenderer(window.contextJsParameters.contextPath + '/cms/editframe/default/sites/$site-key.linkChecker.html');
      }
  });

Primary nav items

Item in the primary nav bar can be added as primary-nav-item - Here, add a link to /linkchecker (path) into the main primary nav (target) with a label and an icon.

// Call the add method from the registry which is in uIExtender to add a menu entry to point to my module
// `primary-nav-item` is the type I want to register and `linkcheckerRoute` is the key (must be unique), the last parameter is an object with the necessary options
window.jahia.uiExtender.registry.add('primary-nav-item', 'linkcheckerRoute', {
    targets: ['nav-root-top:21'],       // Which menu I want to extend, it can take multiple values, each value can be ordered `target:position`
    path: '/linkchecker',               // Path to call when clicking on my link
    label: 'linkchecker:label.title',   // RB to use to display the name of my link `namespace:key`
    icon: window.jahia.moonstone.toIconComponent('Feather')  // Icon to use with my link, we must use the `toIconComponent` function to make sure we return an Icon Component
});

Admin panels

Administration panels can be registered with a single entry, that will add an item in the tree and the appropriate route at the same time.

window.jahia.uiExtender.registry.add('adminRoute', 'linkchecker', {
    targets: ['jcontent:10'],
    label: 'linkchecker:label.title',
    isSelectable: true,
    requiredPermission: 'siteAdminLinkChecker',
    requireModuleInstalledOnSite: 'linkchecker',
    iframeUrl: window.contextJsParameters.contextPath + '/cms/editframe/default/$lang/sites/$site-key.linkChecker.html'
});

See more in settings documentation.

Actions

You can add actions in the application anywhere buttons or menu entries are displayed.

window.jahia.uiExtender.registry.add('action', 'downloadFile', {
    buttonIcon: window.jahia.moonstone.toIconComponent('CloudDownload'),
    buttonLabel: 'jcontent:label.contentManager.contentPreview.download',
    targets: ['contentActions:10'],
    onClick: context => {
        window.open(context.path);
    }
});

See more in actions documentation.

Translation namespaces

If you want to use translated label in your module, you need to declare your module namespace, which must be your module name :

// Declare the namespace to find the translations for my module, put the name of your module and reuse it in your labels
window.jahia.i18n.loadNamespaces('linkchecker');

This will download the translations from JSON files under the following path src/main/resources/javascript/locales/<lang>.json:

{
  "label" : {
    "title": "Link Checker"
  }
}

More complex modules

Maven

add a maven dependency on app-shell to your project with the classifier manifest:

<dependency>
    <groupId>org.jahia.modules</groupId>
    <artifactId>app-shell</artifactId>
    <version>2.0.0</version>
    <type>json</type>
    <classifier>manifest</classifier>
</dependency>

And use copy plugin:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-dependency-plugin</artifactId>
   <executions>
       <execution>
           <id>copy</id>
           <phase>initialize</phase>
           <goals>
               <goal>copy</goal>
           </goals>
       </execution>
   </executions>
   <configuration>
       <artifactItems>
           <artifactItem>
               <groupId>org.jahia.modules</groupId>
               <artifactId>app-shell</artifactId>
               <type>json</type>
               <classifier>manifest</classifier>
           </artifactItem>
       </artifactItems>
   </configuration>
</plugin>

This provides the manifest that contains all shared packages while building the application or the extension.

Set webpack configuration to use DLL and CopyWebpackPlugin to provide package.json

The output exposed of the extension must be a single file named *.bundle.js

{
    output: {
        path: path.resolve(__dirname, 'src/main/resources/javascript/apps/'),
        filename: 'content-editor-ext.bundle.js',
        chunkFilename: '[name].content-editor.[chunkhash:6].js'
    }
}

And the plugin configuration:

{
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: require('./target/dependency/app-shell-1.0.0-SNAPSHOT-manifest')
        }),
        new CopyWebpackPlugin([{ from: './package.json', to: '' }])
    ]
}