Extending eZ Platform 2.x on the frontend side

Extending eZ Platform 2.x on the frontend side

In this article I'm going to focus on extending eZ Platform 2.x editorial interface (so called back-office) from the frontend perspective. On the frontend side we're using ReactJS for rich interactive UI modules Universal Discovery and Sub Items List.

A quick look into the past

In the past, to extend the editorial interface developers had to know JavaScript and, more importantly, YUI3 JavaScript framework. It forced a specific developer workflow, like defining module dependencies in the yui.yml file and wrapping JS code in YUI3 modules. That could be painful, because you had to know what dependencies are required by a given module and where a specified module should be loaded using the dependencyOf keyword in yui.yml.

For the templates we used Handlebars templating system which was another piece of code developers had to be familiar with. In the end, the app performance wasn't at the top level. Editors and UI developers had to wait quite some time to load everything (approx. 10 seconds of watching the loading spinner).

In general, for our community, writing JS code to extend the user interface was difficult. Using YUI3 JS framework didn't help them either, instead it complicated everything.

Seeking a right solution

When attending meetups, conferences and meeting with customers and partners we found out that the technology choices we made when developing eZ Platform 1.x were leading to a dead end. It was difficult even for us to find right developers to join our development team. And it was even more difficult for our community and customers as they needed JS/YUI3 devs as well. Our community background comes from PHP and that was one of our main goals while developing eZ Platform 2.x, to re-enable development of new features in the new version of eZ Platform.

Our goals for eZ Platform 2.x were to:

  • Increase app performance,
  • Eliminate the loading spinner headache,
  • Make it easy to extend.

We tried many approaches, like mixing up server-side rendering with Web Components, but this lead us to the same point we had been in before. It was even worse, because of Web Components, which didn't fit our requirements. Fortunately, we changed the direction at the very last moment.

eZ Platform 2.x tech stack

We decided to use the following tech stack:

  • PHP 7.1 (to take advantage of latests features, be able to use Doctrine DBAL 2.7, and be prepared for Symfony 4),
  • Symfony3,
  • Bootstrap 4,
  • ReactJS 16.x,
  • vanilla JS (with ES6 support).

With this technology stack we are sure we're following the best modern app development trends enabling us and our community, partners and customers to build superb features without being hindered by the lack of experts in a given technology.

As I mentioned earlier, internally we were playing with Web Components, but it turned out the software using it is hardly extensible and customizable. What's more important, there aren't many Web Components developers on the job market.

We decided to use ReactJS to build highly interactive UI modules that can be extended using predefined extension points. ReactJS library is very popular amongst frontend developers and it brings many features like:

  • quick UI updates, which is very important in terms of UI responsiveness,
  • easy way to manage module's state and data,
  • easy way to implement event handlers in UI,
  • the module is split into smaller ReactJS components, so it's very easy to compose them into a bigger module - no need to specify dependencies in files like yui.yml,
  • no need to use template engines, like Handlebars, anymore,
  • easy to combine with other libraries, even with Web Components (in a distant future).

With usage of ReactJS and the fact that eZ Platform 2.x always supports the latest version of modern browsers like:

  • Google Chrome,
  • Mozilla Firefox,
  • Opera,
  • Safari,
  • Microsoft Edge.

We are able to use:

  • the latest native JavaScript features (ES6+ features),
  • the newest CSS features (like CSS grid),
  • and HTML5 APIs.

Currently, we're not targeting mobile browsers or mobile devices at all. It's because of the Page Builder feature which will come with 2.2 release in June this year. Using it, to create content that contains the Page fieldtype, on smaller screens will be ineffective. Of course, our app is prepared in some way to be RWD, but it depends on settings coming from Bootstrap 4.

Extensible eZ Platform UI parts

When building the new generation of eZ Platform we had extensibility in mind. Everything in the back-office can be extended with PHP (in most cases) and with JavaScript/ReactJS in some cases. In this article I'll focus on extending the UI with custom JS code.

But, before we start extending the back-office UI with new features, there's a couple of thing worth mentioning:

  1. The only ReactJS module loaded on every page is Universal Discovery module. The rest of them: Sub Items List module and Multi File Upload (which is a part of Sub Items module) are loaded on demand, when needed. It will be up to you to decide when you need it, and load it.
  2. To load any CSS/JS file from your bundle, you will have to define them in the services.yml file. This way all your scripts will be available in one of 11 existing extension points: 2 for CSS files and 9 for JS files and additional configuration to custom JS features. These places are called:
  • stylesheet-head,
  • script-head,
  • custom-admin-ui-modules,
  • custom-admin-ui-config,
  • stylesheet-body,
  • script-body
  • content-edit-form-before
  • content-edit-form-after
  • content-create-form-before
  • content-create-form-after
  • dashboard-blocks

Extending Universal Discovery module

The Universal Discovery module is a key element of Admin UI. By using it you can find content, navigate to a content location, even create a new content item.

It's almighty, so it has to have extension points to allow developers to extend its functionality by adding new tabs with an additional custom interface.

To extend Universal Discovery module with a custom tab you have to create a file with a code that follows the structure:

(function (global, eZ) {
    eZ = eZ || {};
    eZ.adminUiConfig = eZ.adminUiConfig || {};
    eZ.adminUiConfig.universalDiscoveryWidget = eZ.adminUiConfig.universalDiscoveryWidget || {};
    eZ.adminUiConfig.universalDiscoveryWidget.extraTabs = eZ.adminUiConfig.universalDiscoveryWidget.extraTabs || [];

    eZ.adminUiConfig.universalDiscoveryWidget.extraTabs.push({
        id: 'tab-identifier',
        title: 'tab-title',
        iconIdentifier: 'tab-icon-identifier',
        panel: CustomTabContentPanel,
        attrs: {}
    });

})(window, window.eZ);

In the code above the first thing you have to do is to check whether the eZ object exists in the global scope (in the window).

Then you can start adding any number of custom tabs to the extraTabs property, by pushing them into an array.

Each custom tab definition should have the following properties:

  • id - a tab identifier, which has to be unique,
  • title - a tab title,
  • iconIdentifier - a tab icon. It should come from ez-icons.svg file, otherwise no icon will be displayed.
  • panel - it's a ReactJS component that will be displayed as a tab content after clicking on a tab nav item,
  • attrs - any additional props (as an object) required by a component defined in panel property.

Your custom tab should be loaded in custom-admin-ui-modules group and the file where you add a new tab should be loaded in custom-admin-ui-config group.

More information about extending the Universal Discovery module can be found here: Creating a custom UDW tab

Extending Sub Items List module

Extending Sub Items List module is similar to extending Universal Discovery module. When you extend Sub Items List module it means you're adding extra actions in the list's header section. The extra actions don't manipulate the Sub Items List module output.

An example of extra action in the current version of Admin UI is the Multi File Upload module. The extra action declared in the Sub Items List module opens a file upload popup, where an editor can see the progress of file uploads and can drop new files while uploading other files. After closing the popup, the page will reload showing new files on the list.

To extend the module you have to pass this prop while initializing the module instance:

extraActions: [{
    component: MultiFileUpload,
    attrs: Object.assign({}, mfuAttrs, {
        onPopupClose: doSomethingOnClose,
    })
}]

Let's explain what it contains. The extraActions prop is an array of objects containing:

  • component - which is a reference to ReactJS module class,
  • attrs - configuration hash required by a given module class when instantiating it.

The full example of the Sub Items List module instance can looks like the following code:

ReactDOM.render(React.createElement(SubItems, {
    handleEditItem,
    generateLink,
    parentLocationId: container.dataset.location,
    sortClauses: {[sortField]: sortOrder},
    restInfo: {token, siteaccess},
    extraActions: [{
        component: MultiFileUpload,
        attrs: Object.assign({}, mfuAttrs, {
            onPopupClose: doSomethingOnClose,
        })
    }],
    items,
    contentTypesMap,
    limit: parseInt(container.dataset.limit, 10),
    totalCount: subItemsList.ChildrenCount
}), container);

Extending Field Type validation features

Field Types are playing very important role in eZ Platform. Each Content Type is built with a set of them. Each system Field Type has a predefined frontend validator class.

If you created a new, custom Field Type on the backend side, you'd likely want to have some sort of frontend validation for your Field Type.

There are a couple of ways of implementing the frontend validators. The first approach, the recommened one, is to create a new JS class that extends the eZ.BaseFieldValidator class.

class CustomFieldTypeValidator extends global.eZ.BaseFieldValidator {
    validateValue(event) {
        return {
            isError: false,
            errorMessage: ''
        };
    }
}

Then you have to apply your validator to any Field that requires validation on the frontend side:

const validator = new CustomFieldTypeValidator({
    classInvalid: 'is-invalid',
    fieldSelector: SELECTOR_FIELD,
    eventsMap: [
        {
            selector: '.ez-field-edit--custom-field-type input',
            eventName: 'blur',
            callback: 'validateValue',
            errorNodeSelectors: ['.ez-field-edit__label-wrapper'],
        },
    ],
});

validator.init();

When creating a new instance of your validator you have to provide some sort of config with a minimal set of required values:

  • classInvalid - a name of CSS class, to be added to your invalid field; for styling purposes,
  • fieldSelector - it's a CSS field selector,
  • eventsMap - an array of objects containing information about every form field that requires validating its input. Each input config should contain:
    • selector - the input selector,
    • eventName - the JS event name; it can be any event, even custom ones.
    • callback - a callback name to be run for validation of a specific input field,
    • errorNodeSelectors - the selectors of any element that should have the CSS error class applied to.

Please, notice the usage of validator.init() - it starts validation feature on your custom input field.

At the end you have to add the newly created validator instance to global list of running validators. This will help refreshing validators when some conditions change.

global.eZ.fieldTypeValidators = global.eZ.fieldTypeValidators ?
    [...global.eZ.fieldTypeValidators, validator] :
    [validator];

The link to eZ Conference 2018 presentation: Extending eZ Platform 2.x with Symfony and React

eZ Platform is now Ibexa DXP

Ibexa DXP was announced in October 2020. It replaces the eZ Platform brand name, but behind the scenes it is an evolution of the technology. Read the Ibexa DXP v3.2 announcement blog post to learn all about our new product family: Ibexa Content, Ibexa Experience and Ibexa Commerce

Introducing Ibexa DXP 3.2

Insights and News