Johannes Wachter
Johannes Wachter
Core Developer – Sulu GmbH
Sulu Core Developer and Open-Source Enthusiast.

How We Extended Sulu’s Admin UI for Allianz Cinema and Why We Did it That Way

Although Sulu’s aim is to let you build without getting in the way, sometimes people ask us for real-world orientations. We’ve just published a new guide to Sulu’s powerful admin UI, so here’s an example of how we used it for a client project: the Allianz Cinema website.

Save yourself the work of customization, if you can

As described in the guide, you have three options when bringing your project into the Sulu admin UI:

  • Reuse complete existing views and configure them on the server side in PHP.
  • Reuse existing views and use extension points like form fields to sprinkle in custom components.
  • Build an entire view of your own, which is super flexible but more work.

Which option you choose depends on how much flexibility your project requires. Reusing existing views is less time consuming, but building your own allows for greater flexibility.

How we implemented the Allianz Cinema website

The better a dev knows what Sulu can do, the better they can advise the concept designer at the wireframe stage. Sometimes there are compromises to be made: out-of-the-box features are less flexible, but save time. A Sulu developer with a few projects under their belt can advise on the implications before building features. And because our developers are Sulu experts, the early stages of the Allianz Cinema relaunch began (as most of our projects do) as a negotiation between the concept designer and our developers.

Get up and running faster with our Consulting, Training, and Development services

The Allianz Cinema website is a great example of a multifunctional application with many entities that interact in complex ways—if you’ve read the case study, you know what we mean. Most content management systems wouldn’t be suited to the job because they’re focused on content entities. When we brought Allianz Cinema’s custom entities within the Sulu interface, we were able to write complex, custom business logic in PHP and still leverage the intuitive Sulu UI without rewriting everything.

Most of Allianz Cinema’s admin UI uses standard Sulu views. Views with lists of events, shows, etc., as well as forms for CRUD operations (adding or modifying a location, for example) didn’t require reinventing the wheel. Yes, there is complex business logic in the backend, but once the admin UI is hooked up, little to no coding is required. 

In Sulu, there are three main elements you can reuse and extend:

  • Views are what the user sees in the main part of the window, where data entry and manipulation takes place. A view is assembled from existing components (such as form fields), and reusable containers. Containers themselves are collections of components. Components are stateless, whereas containers and views are not.
  • Form fields are components for interacting with a form. You can use existing ones provided by Sulu (which you should, if possible), extend them, or create your own.
  • Toolbar actions give the content manager options that affect the view. For example, you can include a toggle control that hides or reveals certain fields or other parts of the form. They are rendered in the globally available toolbar, which guarantees a consistent design.

We’re going to show you one example where we used a custom view.

Example: custom view for an order form

Allianz Cinema’s order form doesn’t resemble any of the standard views. It’s partly a form, partly a list, and combines both elements in a way no other existing view does, so we chose to make it a custom view. Our decision process was as follows:

  1. We made some mockups, doing everything we could to use the existing components, containers and views.
  2. We compared the mockup to existing views, and found that it was vastly different from anything that already existed.
  3. At this point, we decided we couldn’t avoid making a custom view.

Looking at the screenshot, you can see that the layout is unique. It contains:

  • An address field that enables the content manager to link the order to a person, organization or even a unique address,
  • Fields such as “ordered at” and “UID” to the right of this area,
  • A list element under “Items” (which is a container element we reused),
  • An overview pane on the right.

Let’s go through the steps needed to create this view.

First, we use an XML configuration to define which form fields are needed for the view (see screenshot)

<form xmlns="http://schemas.sulu.io/template/template"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/form-1.0.xsd">

    <key>order_details</key>

    <properties>
        <property name="address" type="order_address" colspan="6" spaceAfter="3">
            <meta>
                <title>avodi_event.address</title>
            </meta>
            <params>
                <param name="formKey" value="order_address" />
            </params>
        </property>

        <section name="right" colspan="3">
            <properties>
                <property name="orderDate" type="date">
                    <meta>
                        <title>app.order_date</title>
                    </meta>
                </property>

                <property name="uid" type="text_line">
                    <meta>
                        <title>sulu_contact.uid</title>
                    </meta>
                </property>

                <property name="lastDatatransUppTransactionId" type="text_line" disabledCondition="true">
                    <meta>
                        <title>app.datatrans_transaction_id</title>
                    </meta>
                </property>
            </properties>
        </section>

        <property name="items" type="order_items">
            <meta>
                <title>app.order_items</title>
            </meta>
        </property>

        <property name="note" type="text_area">
            <meta>
                <title>app.note</title>
            </meta>
        </property>
    </properties>
</form>

Note that the “order_address” and “order_items” types do not come with Sulu by default, which means they have to be registered by the application itself. Before we can do that we have to create a React component for these fields (see an example for the “order_address” here).

// @flow
import React from 'react';
import {computed, observable, toJS} from 'mobx';
import userStore from 'sulu-admin-bundle/stores/userStore';
import ResourceFormStore from 'sulu-admin-bundle/containers/Form/stores/ResourceFormStore';
import type {FieldTypeProps} from 'sulu-admin-bundle/types';
import OrderAddressComponent from '../../OrderAddress/OrderAddress';

type Props = FieldTypeProps<?Object>;

export default class OrderAddress extends React.Component<Props> {
    @observable loading: boolean = false;

    @computed get orderId(): string {
        return (this.props.formInspector.id: any);
    }

    @computed get formKey(): string {
        const {schemaOptions} = this.props;
        if (!schemaOptions || !schemaOptions.formKey || !schemaOptions.formKey.value) {
            throw new Error('The OrderAddress field-type expects a "formKey" schema-option.');
        }

        return (schemaOptions.formKey.value: any);
    }

    handleChange = (value: ?Object) => {
        const {onChange, onFinish} = this.props;

        onChange(toJS(value));
        onFinish();
    };

    render() {
        const {value, formInspector} = this.props;
        const locale = formInspector.locale ? formInspector.locale : observable.box(userStore.contentLocale);

        return (
            <OrderAddressComponent
                formKey={this.formKey}
                loading={this.loading}
                locale={locale}
                onChange={this.handleChange}
                orderId={this.orderId}
                value={value}
            />
        );
    }
}

The “schemaOptions” variable contains the values from the “params” tag of the XML above and the “formInspector” allows access to information about the form.

Afterwards we register this field type in the JavaScript entry point of our application.

fieldRegistry.add('order_address', OrderAddress);

Next, we have to create our own view, because the default form does not come with a sidebar attached. Fortunately, the default form can still be reused by wrapping it in a higher-order component for showing the sidebar.

// @flow
import Form from 'sulu-admin-bundle/views/Form';
import {withSidebar} from 'sulu-admin-bundle/containers/Sidebar';

export default withSidebar(Form, function() {
    return {
        view: 'order_summary',
        sizes: ['small'],
        defaultSize: 'small',
        props: {
            formStore: this.resourceFormStore,
        },
    };
});

The “order_summary” refers to a React component that displays the sidebar in the screenshot above. It receives the FormStore as a prop so it can read from the form. 

Once the component is implemented, we register it by using a one-liner with the alias we have used for the “view” property in the withSidebar high-order component.

sidebarRegistry.add('order_summary', OrderSummary);

We also have to register the newly created order form component with the sidebar as a view.

viewRegistry.add('order_form', OrderForm);

Finally we configure this view to be displayed on a certain URL in our Admin class:

class OrderAdmin extends Admin
{
    private $viewBuilderFactory;

    public function configureViews(ViewCollection $viewCollection): void
    {
        $viewCollection->add(
            $this->viewBuilderFactory
                ->createResourceTabViewBuilder('app.order_form, '/orders/:id', 'order_form')
                ->setResourceKey('orders')
                ->setParent('app.order_form');
        );

        $viewCollection->add(
            $this->viewBuilderFactory
                ->createViewBuilder('app.order_form.details', '/details, 'order_form')
                ->setParent('app.order_form');
        );
    }
}

That’s it! Next: implement a REST API with which this form can talk, though how that gets implemented is highly dependent on your use case.

You can contribute too

While we’re on the subject of custom components: whenever we modify or improve part of Sulu in a way that other projects can benefit from, we issue a pull request and integrate it into Sulu as a whole so it’s available subsequently. Why not issue a pull request and contribute to Sulu if you make your own improvements?

As you’ve seen, Sulu allows you to bring in content entities, and much more. It’s capable of offering a tidy interface that shields users from baffling complexity. Allianz Cinema is a great example of how you can leverage Sulu’s standard functionality and make use of its extensibility where needed.

The better you know what’s built into Sulu, the better your chance of avoiding unnecessary development and, later on, maintenance. If you need a hand, the community is here to answer your questions in our Slack channel. Or you can choose the fast lane and book our professional services.

Check out our services and support