edit description

Extending eZ Platform UI

Back-end interface

The back-end interface is produced by the ezplatform-admin-ui Bundle together with ezplatform-admin-ui-modules, which contains React modules that handle specific parts of the application. This interface is accessible in your browser at http://[uri_of_platform]/admin.

General extensibility

eZ Platform's back-end interface uses React-based modules that make each part of the UI easily extensible. The interface uses Bootstrap, which facilitates adapting and styling the interface to your needs.

Available extensibility points:

Menus in eZ Platform are based on the KnpMenuBundle and are easily extensible. For a general idea on how to use MenuBuilder, refer to the official documentation.

Menus are extensible using event subscribers/listeners. You can hook into the following events:

  • ConfigureMenuEvent::MAIN_MENU
  • ConfigureMenuEvent::USER_MENU
  • ConfigureMenuEvent::CONTENT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::CONTENT_EDIT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::CONTENT_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::CONTENT_SIDEBAR_LEFT
  • ConfigureMenuEvent::TRASH_SIDEBAR_RIGHT
  • ConfigureMenuEvent::SECTION_EDIT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::SECTION_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::POLICY_EDIT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::POLICY_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::ROLE_EDIT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::ROLE_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::USER_EDIT_SIDEBAR_RIGHT
  • ConfigureMenuEvent::USER_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::ROLE_ASSIGNMENT_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::LANGUAGE_CREATE_SIDEBAR_RIGHT
  • ConfigureMenuEvent::LANGUAGE_EDIT_SIDEBAR_RIGHT

An event subscriber can be implemented as follows:

<?php
namespace EzSystems\EzPlatformAdminUi\EventListener;

use EzSystems\EzPlatformAdminUi\Menu\Event\ConfigureMenuEvent;
use EzSystems\EzPlatformAdminUi\Menu\MainMenuBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class MenuListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            ConfigureMenuEvent::MAIN_MENU => ['onMenuConfigure', 0],
        ];
    }

    public function onMenuConfigure(ConfigureMenuEvent $event)
    {
        $menu = $event->getMenu();
        $factory = $event->getFactory();
        $options = $event->getOptions(); // options passed from the context (i.e. Content item in Content View)

        // your customizations
    }
}

Extending menu examples

Add a new menu item under "Content" with custom attributes

$menu[MainMenuBuilder::ITEM_CONTENT]->addChild(
    'form_manager',
    [
        'route' => '_ezpublishLocation',
        'routeParameters' => ['locationId' => 2],
        [
            'linkAttributes' => [
                'class' => 'test_class another_class',
                'data-property' => 'value',
            ],
        ],
    ]
);

Remove the "Media" menu item from the Content tab

$menu[MainMenuBuilder::ITEM_CONTENT]->removeChild(
    MainMenuBuilder::ITEM_CONTENT__MEDIA
);

Add a top-level menu item with a child

$menu->addChild(
    'menu_item_1',
    ['label' => 'Menu Item 1', 'extras' => ['icon' => 'file']]
);
$menu['menu_item_1']->addChild(
    '2nd_level_menu_item',
    ['label' => '2nd level menu item', 'uri' => 'http://example.com']
);

Add an item depending on a condition

$condition = true;
if ($condition) {
    $menu->addChild(
        'menu_item_2',
        ['label' => 'Menu Item 2', 'extras' => ['icon' => 'article']]
    );
}

Add a top-level menu item with URL redirection

$menu->addChild(
    'menu_item_3',
    [
        'label' => 'Menu Item 3',
        'uri' => 'http://example.com',
        'extras' => ['icon' => 'article'],
    ]
);

Translatable labels

To have translatable labels, use translation.key from the messages domain:

$menu->addChild(
    'menu_item_3',
    [
        'label' => 'translation.key',
        'uri' => 'http://example.com',
        'extras' => ['icon' => 'article'],
        'translation_domain' => 'messages',
    ]
);

Reorder menu items, i.e. reverse the order

$menu->reorderChildren(
    array_reverse(array_keys($menu->getChildren()))
);

Dashboard

To extend the Dashboard, make use of an event subscriber.

In the following example, the DasboardEventSubscriber.php reverses the order of sections of the Dashboard (in a default installation this makes the "Everyone" block appear above the "Me" block):

namespace AppBundle\EventListener;

use EzSystems\EzPlatformAdminUi\Component\Event\RenderGroupEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class DashboardEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            RenderGroupEvent::NAME => ['onRenderGroupEvent', 20],
        ];
    }

    public function onRenderGroupEvent(RenderGroupEvent $event)
    {
        if ($event->getGroupName() !== 'dashboard-blocks') {
            return;
        }

        $components = $event->getComponents();

        $reverseOrder = array_reverse($components);

        $event->setComponents($reverseOrder);

    }
}

Tabs

Many elements of the back-office interface, such as System Info or Location View, are built using tabs.

Tabs in System Information

You can extend existing tab groups with new tabs, or create your own tab groups.

Adding a new tab group

To create a custom tab group with additional logic you need to create it at the level of compiling the Symfony container, using a CompilerPass:

// src/AppBundle/DependencyInjection/Compiler/CustomTabGroupPass.php

namespace AppBundle\DependencyInjection\Compiler;

use EzSystems\EzPlatformAdminUi\Tab\TabGroup;
use EzSystems\EzPlatformAdminUi\Tab\TabRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class CustomTabGroupPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition(TabRegistry::class)) {
            return;
        }

        $tabRegistry = $container->getDefinition(TabRegistry::class);
        $tabGroupDefinition = new Definition(
            TabGroup::class,  // or any class that extends TabGroup
            ['custom-tab-group']
        );
        $tabRegistry->addMethodCall('addTabGroup', [$tabGroupDefinition]);
    }
}

A shorter way that you can use when no custom logic is required, is to add your own tab with the new group. If a tab's group is not defined, it will be created automatically.

A new tab group can be rendered using the ez_platform_tabs Twig helper:

<div class="my-tabs">
    {{ ez_platform_tabs('custom-tab-group') }}
</div>

This will render the group and all tabs assigned to it.

Adding a new tab

Before you add a tab to a group you must create the tab's PHP class and define it as a Symfony service with the ezplatform.tab tag:

# services.yml

AppBundle\Custom\Tab:
    parent: EzSystems\EzPlatformAdminUi\Tab\AbstractTab
    tags:
        - { name: ezplatform.tab, group: 'custom-tab-group' }

This configuration also assigns the new tab to custom-tab-group.

Tip

If the custom-tab-group does not yet exist, it will be created automatically at this point.

The tab class can look like this:

// src/AppBundle/Custom/Tab.php

namespace AppBundle\Custom;

use EzSystems\EzPlatformAdminUi\Tab\AbstractTab;

class Tab extends AbstractTab
{
    public function getIdentifier(): string
    {
        return 'custom-tab';
    }

    public function getName(): string
    {
        return /** @Desc("Custom Tab") */
            $this->translator->trans('custom.tab.name', [], 'some_translation_domain');
    }

    public function renderView(array $parameters): string
    {
        // Do rendering here

        return $this->twig->render('AppBundle:my_tabs/custom.html.twig', [
            'foo' => 'Bar!',
        ]);
    }
}

Beyond the AbstractTab used in the example above you can use two specializes tab types:

  • AbstractControllerBasedTab enables you to embed the results of a controller action in the tab.
  • AbstractRouteBasedTab embeds the results of the selected routing, passing applicable parameters.

Modifying tab display

You can order the tabs by making the tab implement OrderedTabInterface. The order will then depend on the numerical value returned by the getOrder method.

class Tab extends AbstractTab implements OrderedTabInterface
{
    public function getOrder(): int
    {
        return 100;
    }
}

The tabs will be displayed according to this value in ascending order.

Tip

It is good practice to reserve some distance between these values, for example to stagger them by step of 10. It may come useful if you later need to place something between the existing tabs.

You can also influence tab display (e.g. order tabs, remove or modify them, etc.) by using Event Listeners:

class TabEvents
{
    /**
     * Happens just before rendering a tab group.
     */
    const TAB_GROUP_PRE_RENDER = 'ezplatform.tab.group.pre_render';

    /**
     * Happens just before rendering a tab.
     */
    const TAB_PRE_RENDER = 'ezplatform.tab.pre_render';
}

As an example, see how OrderedTabInterface is implemented:

namespace EzSystems\EzPlatformAdminUi\Tab\Event\Subscriber;

use EzSystems\EzPlatformAdminUi\Tab\Event\TabEvents;
use EzSystems\EzPlatformAdminUi\Tab\Event\TabGroupEvent;
use EzSystems\EzPlatformAdminUi\Tab\OrderedTabInterface;
use EzSystems\EzPlatformAdminUi\Tab\TabInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Reorders tabs according to their Order value (Tabs implementing OrderedTabInterface).
 * Tabs without order specified are pushed to the end of the group.
 *
 * @see OrderedTabInterface
 */
class OrderedTabSubscriber implements EventSubscriberInterface
{
    /**
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
        return [
            TabEvents::TAB_GROUP_PRE_RENDER => ['onTabGroupPreRender'],
        ];
    }

    /**
     * @param TabGroupEvent $tabGroupEvent
     */
    public function onTabGroupPreRender(TabGroupEvent $tabGroupEvent)
    {
        $tabGroup = $tabGroupEvent->getData();
        $tabs = $tabGroup->getTabs();

        $tabs = $this->reorderTabs($tabs);

        $tabGroup->setTabs($tabs);
        $tabGroupEvent->setData($tabGroup);
    }

    /**
     * @param TabInterface[] $tabs
     *
     * @return array
     */
    private function reorderTabs($tabs): array
    {
        $orderedTabs = [];
        foreach ($tabs as $tab) {
            if ($tab instanceof OrderedTabInterface) {
                $orderedTabs[$tab->getIdentifier()] = $tab;
                unset($tabs[$tab->getIdentifier()]);
            }
        }

        uasort($orderedTabs, [$this, 'sortTabs']);

        return array_merge($orderedTabs, $tabs);
    }

    /**
     * @param OrderedTabInterface $tab1
     * @param OrderedTabInterface $tab2
     *
     * @return int
     */
    private function sortTabs(OrderedTabInterface $tab1, OrderedTabInterface $tab2): int
    {
        return $tab1->getOrder() <=> $tab2->getOrder();
    }
}

Further extensibility using Components

Components are any sort of custom elements that you can add to the back-office interface.

There are several extensibility points in the AdminUI templates that you can use for this purpose.

The only limitation to the application of these extensibility points is that the Component must implement the Renderable interface.

The available extensibility points for Components are:

Universal Discovery module

Universal Discovery module allows you to browse the content structure and search for content using an interactive interface: the browse view and the search view. The module is highly configurable. It can be extended with new tabs.

How to use it?

With vanilla JS:

const container = document.querySelector('#react-udw');

ReactDOM.render(React.createElement(eZ.modules.UniversalDiscovery, {
    onConfirm: {Function},
    onCancel: {Function},
}), container);

With JSX:

const props = {
    onConfirm: {Function},
    onCancel: {Function}
};

<UniversalDiscoveryModule {...props} />

Adding new tabs to the Universal Discovery module

The Universal Discovery module is highly customizable. It allows you to add new tabs to the module.

const props = {
    onConfirm: {Function},
    onCancel: {Function},
    extraTabs: [{
        id: {String},
        title: {String},
        panel: {Element}, // React component that represents content of a tab
        attrs: {Object}
    }]
};

<UniversalDiscoveryModule {...props} />

Each tab definition is an object containing properties:

  • id {String} - unique tab identifier (it cannot be: browse or search)
  • title {String} - tab button title/label
  • panel {Element} - any kind of React component. A panel component will receive the following props:
    • isVisible {Boolean} - visible flag
    • onItemSelect {Function} - a callback to be invoked when content is selected
    • maxHeight {Number} - the maximum height of the panel container
    • id {String} - panel identifier
    • startingLocationId {Number} - location ID
    • findLocationsByParentLocationId {Function} - finds locations related to the parent location
    • findContentBySearchQuery {Function} - finds content matching a given text query
    • contentTypesMap {Object} - content types map with content type ids as keys
    • multiple {Boolean} - can select multiple content items flag
    • labels {Object} - a hash containing text messages to be placed across many places in a component
  • attrs {Object} - any optional list of props that should applied to the panel component. It can override the panel props listed above.

Property list

The <UniversalDiscoveryModule /> module can handle additional properties. There are 2 types of properties: required and optional.

Required properties

Without all the following properties the Universal Discovery module will not work.

onConfirm {Function} - a callback to be invoked when a user clicks on the confirm button in a Universal Discovery popup. The function takes one param: content which is an array of content items structs.

onCancel {Function} - a callback to be invoked when a user clicks on the cancel button in a Universal Discovery popup. It takes no extra params.

restInfo {Function} - a config hash containing: token ({String}) and siteaccess ({String}).

Optional props

Optionally, Universal Discovery module can take a following list of props:

  • loadContentInfo {Function} - loads content info. It takes 3 params: restInfo, contentId and callback
  • loadContentTypes {Function} - loads content types data. It takes 2 params: restInfo, callback,
  • canSelectContent {Function} - checks whether a content item can be selected. It takes one param: a data object containing an item property as the content struct and itemsCount as a number of selected items in UDW,
  • findContentBySearchQuery {Function} - finds a content using a search query. It takes 3 params: restInfo, query and callback,
  • findLocationsByParentLocationId {Function} - finds sub items of a given location. It takes 3 params: restInfo, parentLocationId and callback,
  • title {String} - the title of Universal Discovery popup. Default value: Find content,
  • multiple {Boolean} - can select multiple content items flag. Default value: true,
  • activeTab {String} - active tab identifier. Default value: browse,
  • startingLocationId {Number} - location ID. Default value: 1,
  • maxHeight {Number} - maximum height of panel container. Default value: 500,
  • searchResultsPerPage {Number} - max amount of items visible per page in the search results. Default value: 10,
  • extraTabs {Array} - optional, extra tabs. Each tab definition is an object containing the following properties (all of them are required):
    • id {String} - unique tab identifier (it cannot be: browse or search),
    • title {String} - tab button title/label,
    • panel {Element} - any kind of React component,
    • attrs {Object} - any optional list of props that should applied to the panel component. })),
  • labels {Object} - a hash containing text messages to be placed across many places in a component. It contains text labels for child components:
    • udw {Object} - a hash of text labels for Universal Discovery module, see universal.discovery.module.js for details,
    • selectedContentItem {Object} - a hash of text labels for Selected Content Item component,
    • contentMetaPreview {Object} - a hash of text labels for Content Meta Preview component,
    • search {Object} - a hash of text labels for Search component,
    • searchPagination {Object} - a hash of text labels for Search Pagination component,
    • searchResults {Object} - a hash of text labels for Search Results component,
    • searchResultsItem {Object} - a hash of text labels for Search Results Item component.
  • selectedItemsLimit {Number} - the limit of items that can be selected. Should be combined with the multiple attribute set to true. Default value is 0, which means no limit.

Sub-items List

The Sub-items List module is meant to be used as a part of the editorial interface of eZ Platform. It provides an interface for listing the sub items of any location.

How to use it?

With vanilla JS:

React.createElement(eZ.modules.SubItems, {
    parentLocationId: {Number},
    restInfo: {
        token: {String},
        siteaccess: {String}
    }
});

With JSX:

const attrs = {
    parentLocationId: {Number},
    restInfo: {
        token: {String},
        siteaccess: {String}
    }
};

<SubItemsModule {...attrs}/>

Properties list

The <SubItemsModule /> module can handle additional properties. There are 2 types of properties: required and optional. All of them are listed below.

Required props

Without all the following properties the Sub-items module will not work.

  • parentLocationId {Number} - parent location ID.
  • restInfo {Object} - backend config object:
    • token {String} - CSRF token,
    • siteaccess {String} - SiteAccess identifier.
  • handleEditItem {Function} - callback to handle edit content action.
  • generateLink {Function} - callback to handle view content action.

Optional properties

Optionally, Sub-items module can take a following list of props:

  • loadContentInfo {Function} - loads content items info. Takes 2 params:
    • contentIds {Array} - list of content IDs
    • callback {Function} - a callback invoked when content info is loaded
  • loadContentTypes {Function} - loads content types. Takes one param:
    • callback {Function} - callback invoked when content types are loaded
  • loadLocation {Function} - loads location. Takes 4 params:
    • restInfo {Object} - REST info params:
      • token {String} - the user token
      • siteaccess {String} - the current SiteAccess
    • queryConfig {Object} - query config:
      • locationId {Number} - location ID
      • limit {Number} - content item limit
      • offset {Number} - items offset
      • sortClauses {Object} - the sort clauses, e.g. {LocationPriority: 'ascending'}
    • callback {Function} - callback invoked when location is loaded
  • updateLocationPriority - updates item location priority. Takes 2 params:
    • params {Object} - parameters hash containing:
      • priority {Number} - priority value
      • location {String} - REST location id
      • token {String} - CSRF token
      • siteaccess {String} - SiteAccess identifier
    • callback {Function} - callback invoked when content location priority is updated
  • activeView {String} - active list view identifier
  • extraActions {Array} - list of extra actions. Each action is an object containing:
    • component {Element} - React component class
    • attrs {Object} - additional component properties
  • items {Array} - list of location sub items
  • limit {Number} - items limit count
  • offset {Number} - items limit offset
  • labels {Object} - list of module labels, see sub.items.module.js for details. Contains definitions for sub components:
    • subItems {Object} - list of sub items module labels
    • tableView {Object} - list of table view component labels
    • tableViewItem {Object} - list of table item view component labels
    • loadMore {Object} - list of load more component labels
    • gridViewItem {Object} - list of grid item view component labels

Multi-file Upload

The Multi-file Upload module is meant to be used as a part of editorial interface of eZ Platform. It provides an interface to publish content based on dropped files while uploading them in the interface.

How to use it?

With vanilla JS:

React.createElement(eZ.modules.MultiFileUpload, {
    onAfterUpload: {Function},
    adminUiConfig: {
        multiFileUpload: {
            defaultMappings: [{
                contentTypeIdentifier: {String},
                contentFieldIdentifier: {String},
                contentNameIdentifier: {String},
                mimeTypes: [{String}, {String}, ...]
            }],
            fallbackContentType: {
                contentTypeIdentifier: {String},
                contentFieldIdentifier: {String},
                contentNameIdentifier: {String}
            },
            locationMappings: [{Object}],
            maxFileSize: {Number}
        },
        token: {String},
        siteaccess: {String}
    },
    parentInfo: {
        contentTypeIdentifier: {String},
        contentTypeId: {Number},
        locationPath: {String},
        language: {String}
    }
});

With JSX:

const attrs = {
    onAfterUpload: {Function},
    adminUiConfig: {
        multiFileUpload: {
            defaultMappings: [{
                contentTypeIdentifier: {String},
                contentFieldIdentifier: {String},
                contentNameIdentifier: {String},
                mimeTypes: [{String}, {String}, ...]
            }],
            fallbackContentType: {
                contentTypeIdentifier: {String},
                contentFieldIdentifier: {String},
                contentNameIdentifier: {String}
            },
            locationMappings: [{Object}],
            maxFileSize: {Number}
        },
        token: {String},
        siteaccess: {String}
    },
    parentInfo: {
        contentTypeIdentifier: {String},
        contentTypeId: {Number},
        locationPath: {String},
        language: {String}
    }
};

<MultiFileUploadModule {...attrs}/>

Properties list

The <MultiFileUpload /> module can handle additional properties. There are 2 types of properties: required and optional. All of them are listed below.

Required properties

Without all the following properties the Multi-file Upload will not work.

  • onAfterUpload {Function} - a callback to be invoked just after a file has been uploaded
  • adminUiConfig {Object} - UI config object. It should keep the following structure:
    • multiFileUpload {Object} - multi file upload module config:
      • defaultMappings {Array} - a list of file type to content type mappings Sample mapping be an object and should follow the convention:
        • contentTypeIdentifier {String} - Content Type identifier
        • contentFieldIdentifier {String} - Content field identifier
        • nameFieldIdentifier {String} - name field identifier
        • mimeTypes {Array} - a list of file typers assigned to a specific content type
      • fallbackContentType {Object} - a fallback content type definition. Should contain the following info:
        • contentTypeIdentifier {String} - Content Type identifier
        • contentFieldIdentifier {String} - Content Field identifier
        • nameFieldIdentifier {String} - name Field identifier
      • locationMappings {Array} - list of file type to content type mappings based on a location identifier
      • maxFileSize {Number} - maximum file size allowed for uploading. It's a number of bytes
    • token {String} - CSRF token
    • siteaccess {String} - SiteAccess identifier
  • parentInfo {Object} - parent location meta information:
    • contentTypeIdentifier {String} - Content Type identifier
    • contentTypeId {Number} - Content Type id
    • locationPath {String} - location path string
    • language {String} - language code identifier

Optional properties

Optionally, the Multi-file Upload module can take a following list of props:

  • checkCanUpload {Function} - checks whether am uploaded file can be uploaded. The callback takes 4 params:
    • file {File} - file object,
    • parentInfo {Object} - parent location meta information,
    • config {Object} - Multi-file Upload module config,
    • callbacks {Object} - error callbacks list: fileTypeNotAllowedCallback and fileSizeNotAllowedCallback.
  • createFileStruct {Function} - a function that creates a ContentCreate struct. The function takes 2 params:
    • file {File} - file object,
    • params {Object} - params hash containing: parentInfo and adminUiConfig stored under the config key.
  • deleteFile {Function} - a function deleting Content created from a given file. It takes 3 params:
    • systemInfo {Object} - hash containing information about CSRF token and siteaccess: token and siteaccess,
    • struct {Object} - Content struct,
    • callback {Function} - content deleted callback.
  • onPopupClose {Function} - function invoked when closing a Multi-file Upload popup. It takes one param: itemsUploaded - the list of uploaded items.
  • publishFile {Function} - publishes an uploaded file-based content item. Takes 3 params:
    • data {Object} - an object containing information about:
      • struct {Object} - the ContentCreate struct (),
      • token {String} - CSRF token,
      • siteaccess {String} - SiteAccess identifier,
    • requestEventHandlers {Object} - a list of upload event handlers:
      • onloadstart {Function} - on load start callback,
      • upload {Object} - file upload events:
        • onabort {Function} - on abort callback,
        • onload {Function} - on load callback,
        • onprogress {Function} - on progress callback,
        • ontimeout {Function} - on timeout callback.
    • callback {Function} - a callback invoked when an uploaded file-based content has been published.

Enterprise

Block templates

All Landing Page blocks, both those that come out of the box and custom ones, can have multiple templates. This allows you to create different styles for each block and let the editor choose them when adding the block from the UI. The templates are defined in your configuration files like in the following example, with simplelist and special being the template names:

# app/config/block_templates.yml
blocks:
    contentlist:
        views:
            simplelist:
                template: blocks/contentlist_simple.html.twig
                name: Simple Content List
            special:
                template: blocks/contentlist_special.html.twig
                name: Special Content List

Some blocks can have slightly more complex configuration. An example is the Collection block, which requires an options key. This key defines which Content Types can be added to it. See this example from the Studio Demo:

blocks:
    collection:
        views:
            content:
                template: eZStudioDemoBundle:blocks:collection.content.html.twig
                name: Content List View
                options:
                    match: [article, blog_post]
            gallery:
                template: eZStudioDemoBundle:blocks:collection.content.html.twig
                name: Gallery View
                options:
                    match: [image]

Extending the Form Builder

Form Builder Bundle comes a number of types of fields, but it was designed to be easy to extend by adding new ones.

Field definitions

Default field definitions are available in Resources\config\default_fields.yml.

Field definition structure

Field definitions are contained under the fields key. Each definition has its own key, e.g. single_line_text. Each definition must contain two sections:

  • identifier - name of the definition used on the front end
  • displayName - name displayed in the Page mode editor in the fields tab

The definition can also contain the following optional sections:

  • validators - defines validators that the field will use. This must contain the validator's identifier and the values that will be checked during validation, for example:
        validators:
            - { identifier: required, value: 1 }
  • attributes - contains the field's attributes. You can place here custom attributes for new fields. There are also four default attributes that are used for every field: LABEL_NAME, LABEL_HELP_TEXT, LABEL_ADMIN_LABEL and LABEL_PLACEHOLDER_TEXT. If you wish, you can override them in your configuration.

  • views - provides a list of views that will be used to display the field. At least one view must be defined, with the keys name, thumbnail and template, for example:

        views:
            basic:
                name: Form Test Line Text Basic
                thumbnail: /bundles/ezsystemsformbuilder/images/thumbnails/single_line_text/basic.svg
                template: EzSystemsFormBuilderBundle:fields:single_line_text/basic.html.twig

Adding a new field definition

This procedure assumes you are creating a separate Bundle to house your new type of form field.

1. Create a YAML file with field definition

Create a YAML configuration file, e.g. Resources\config\extended_fields.yml, with the following code:

fields:
    test_text:
        identifier: testLineText
        displayName: 'Test Line Text'
        validators:
            - { identifier: required, value: 1 }
        attributes:
            name: 'test line attribute'
        views:
            basic:
                name: Form Test Line Text Basic
                thumbnail: /bundles/ezsystemsformbuilder/images/thumbnails/single_line_text/basic.svg
                template: EzSystemsFormBuilderBundle:fields:single_line_text/basic.html.twig

You can also provide additional options using the options: key. For example, you can make sure that the data entered in a field will not to be stored in the database, like for example in the built-in Captcha field.

When creating a new template for the field definition, remember to add the mandatory ezform-field class and field.id as shown below: 

{% extends 'EzSystemsFormBuilderBundle:fields:field.html.twig' %}
{% block input %}
    <input type="text" class="ezform-field ..." id="{{ field.id }}" placeholder="{{ field.placeholderText }}">
{% endblock %}
2. Modify the DependencyInjection\TestExtension.php class

The class must implement thePrependExtensionInterface interface:

class TestExtension implements PrependExtensionInterface

In the prepend method in TestExtension.php add the following lines at the end:

public function prepend(ContainerBuilder $container)
    {
    ...
        $configFile = __DIR__ . '/../Resources/config/extended_fields.yml';
        $config = Yaml::parse(file_get_contents($configFile));
        $container->loadFromExtension('ez_systems_form_builder', $config);
        $container->addResource(new FileResource($configFile));    
    }

Validators

Creating your own validators

You can create your own validators by reproducing the following configuration:

Validator configuration

A validator implements the EzSystems\FormBuilder\SPI\ValidatorInterface.php interface and extends the abstract class EzSystems\FormBuilder\Core\Validator\Validator.php.

The interface requires the implementation of the following methods:

Method Returns Parameters Description
validate bool $value Contains the logic for the validation. It returns true when validation is successful, or false when the data does not validate
getLabel string Returns a string with the name of the validator that will be used in the editor
getErrorMessage array Returns error message(s) to appear when the validate method returns false
setLimitation mixed $limitation $limitation Allows the configuration of limitations. Its default implementation is contained in the Validator abstract class
getValueType string Returns the name of the checked value type

Currently the abstract class Validator has three value types (defined in Core\Validator\Validator.php):

    const TYPE_INTEGER = 'integer';
    const TYPE_STRING = 'string';
    const TYPE_BOOL = 'bool';

The validator must be tagged as form_builder.field_validator. Due to this the Resources\config\validator_services.yml file contains two entries, one in the parameters section:

form_builder.validator.example.class: EzSystems\FormBuilder\Core\Validator\ExampleValidator

and one in the services section:

form_builder.validator.example:
        class: '%form_builder.validator.example.class%'
        tags:
            - { name: form_builder.field_validator, alias: example }

Signal slots

Whenever a form is submitted and stored in a database, lib\Core\SignalSlot\Signal\FormSubmit emits a signal in a submitForm service. You can create your own listeners, called Signal slots, to capture the FormSubmit signal.

Below you can find an example of a custom Signal slot. It saves submission to a text file. 

<?php
namespace AppBundle\SignalSlot;
use Symfony\Component\Filesystem\Filesystem;
use eZ\Publish\Core\SignalSlot\Slot;
use eZ\Publish\Core\SignalSlot\Signal;
class HandleSubmission extends Slot
{
    /**
     * Receive the given $signal and react on it.
     *
     * @param EzSystems\FormBuilder\Core\SignalSlot\Signal\FormSubmit $signal
     */
    public function receive(Signal $signal)
    {
        $fs = new Filesystem();
        $formId = $signal->formId;
        $submission = $signal->submission;
        $created = $submission->created->format("Y-m-d.H:i:s");
        $dataRows = [];
        foreach ($submission->fields as $field) {
            $dataRows[] = "{$field->label}: {$field->value}";
        }
        $fs->mkdir("forms/{$formId}");
        $fs->dumpFile("forms/{$formId}/{$created}.txt", implode("\n", $dataRows));
    }
}

It has to be registered as a tagged Symfony service, like this:

    app_bundle.handle_submission:
        class: "AppBundle\SignalSlot\HandleSubmission"
        tags:
            - { name: ezpublish.api.slot, signal: '\EzSystems\FormBuilder\Core\SignalSlot\Signal\FormSubmit' }
 

Other Form Builder fields

The following form Builder fields require additional configuration.

Date field

To make use of a datepicker in the Date field you need to add the necessary assets. The assets should be added in page head with the following code:

{% javascripts
    ...
    'bundles/ezsystemsformbuilder/js/vendors/*/*.js'
%}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
{% stylesheets filter='cssrewrite'
    ...
    'bundles/ezsystemsformbuilder/css/vendors/*/*.css'
%}
    <link rel="stylesheet" type="text/css" href="{{ asset_url }}">
{% endstylesheets %}
Adding new date format

If you wish to add a new date format, the alias in the date field config must follow this pattern:

d or D - day of the month (1-2 digits) dd or DD - day of the month (2 digits) m or M - month of the year (1-2 digits) mm or MM - month (2 digits) yy or YY - year (2 digits) yyyy or YYYY - year (4 digits)

for example:

d-m-yyyy - 16-1-2017 mm/dd/yy - 01/16/17

Read the Docs