Building a custom admin view in Sulu
22 januari 2024
5 minutes
One of the lesser-known features in the sulu admin, is that you can build custom admin views inside your Sulu Admin interface. This can come in handy in cases where you want to create a page that looks different than the standard lists and forms. In this blog we'll show you the basic steps from which you can start your exploration.
Deciding where you want to place the custom view
There are 2 places where you can place a custom view:
As a page that can be accessed directly from the navigation.
As an additional tab inside the edit views.
The starting point for registering the custom views is the Admin class. You can configure the views like this:
final class SomeAdmin extends Admin
{
private const MY_VIEW_TYPE = 'app.my_custom_view_name';
public function __construct(
private readonly ViewBuilderFactoryInterface $viewBuilderFactory,
) {
}
public function configureViews(ViewCollection $viewCollection): void
{
// As a page:
$viewCollection->add(
$this->viewBuilderFactory->createViewBuilder(
name: self::MY_VIEW_TYPE,
path: '/my-view',
type: self::MY_VIEW_TYPE
)
->setOption('hello', 'world')
);
// OR as an extra tab on an edit view:
$viewCollection->add(
$this->viewBuilderFactory->createTabViewBuilder(name: 'My view', path: '/my-view')
->setTabPriority(-100)
->setParent(self::EDIT_FORM_VIEW)
->setType(self::MY_VIEW_TYPE)
->setOption('hello', 'world')
);
}
}
There are 2 important things to note from the configuration:
The view type will be used to identify the React component you want to link. It is a unique identifier that is linking the PHP part of the admin with the React part of the admin.
You can pass down any option you want into the custom React view. These options will be made available in your custom React view. A common use-case for these options is passing down the admin resource identifiers that can be used to load data. Another use-case could be a very specific setting from your PHP configuration that you want to leak to your frontend.
Building your first admin view
In previous configuration, we've specified a unique admin type that will be used as an identifier of the custom view. To link the identifier with a react component, you can register it in sulu's view registry this way:
// assets/admin/app.ts
import {viewRegistry} from 'sulu-admin-bundle/containers';
viewRegistry.add('app.my_custom_view_name', MyCustomViewComponent);
The React component that is being linked here, looks like this:
import React, {Component} from 'react';
import {ViewProps} from 'sulu-admin-bundle/containers';
import View from 'sulu-admin-bundle/components/View';
export interface RouteOptions {
readonly hello: string;
}
interface Props extends ViewProps<RouteOptions> {
}
class MyCustomViewComponent extends Component<Props> {
render() {
return <View>
<div>Hello {this.props.route.options.hello}</div>
</View>
}
}
The RouteOptions
represent the options that are being passed down from the PHP Admin configuration into the React properties. In this case, it can be used to render out Hello world
, because we passed down world
as the value for the hello
option. The View component is provided by sulu and is solely there to provide some basic styling for the view. From there on, you can do anything you want with it.
Making it more interactive
You might have noticed that in our little example we've used an old-school class component instead of a functional component. It is perfectly possible to use a functional component as well, but sulu comes shipped with an older version of mobx that is not fully compatible with the functional components yet.
Mobx is a framework that can be used on top of React to manage state. It works based on observers that listen to changes on observable values. If the observed value changes, the observer will rerender the component that is watching for any changes.
Let's create a little example:
import {observable} from "mobx";
import {observer} from 'mobx-react';
@observer
class MyCustomViewComponent extends Component<Props> {
@observable messageOfTheDay : MessageOfTheDay;
constructor(props: Props) {
super(props);
this.messageOfTheDay = new MessageOfTheDay();
}
render() {
return <View>
<div>Hello {this.props.route.options.hello}</div>
{!this.messageOfTheDay.loading && <div>{this.messageOfTheDay.value}</div>}
</View>
}
}
A decorator is used to make the custom view component listen for changes on the messageOfTheDay
property. When the component is constructed, the message of the day will be fetched from an asynchronous source. Once the data is loaded, the observer will notice that the loading stopped, and the data is there. The component will be rerendered and the message of the day will be displayed.
To make this all work, the MessageOfTheDay
component must contain observable values as well.
An implementation could look like this:
import {action, observable, runInAction} from "mobx";
import {observer} from 'mobx-react';
class MessageOfTheDay {
@observable value : string|undefined = undefined;
@observable loading : boolean = true;
public constructor() {
this.fetch();
}
@action
public async fetch() {
this.loading = true;
this.value = undefined;
try {
const messageOfTheDay = await loadMessageOfTheDayFromServer()
runInAction(() => {
this.loading = false;
this.value = messageOfTheDay;
})
} catch (error) {
runInAction(() => {
this.loading = false
})
}
}
}
Adding a toolbar
Sulu provides a higher order component that makes it possible to configure a custom toolbar for your custom view as well. Imagine in our example above, that we want to add a synchronize button that fetches another random message of the day from the server. This could be done by adding a refresh action onto the toolbar like this:
import { withToolbar} from 'sulu-admin-bundle/containers';
export default withToolbar(MyCustomViewComponent, function (this: MyCustomViewComponent): ToolbarConfig {
return {
items: [
{
type: 'button',
label: 'Refresh',
icon: 'su-sync',
disabled: this.messageOfTheDay.loading,
onClick: async () => {
return await this.messageOfTheDay.fetch();
},
}
]
};
})
The example above shows you that you can decorate your custom view component to provide toolbar actions. Your custom component will also be included in the toolbar configurator. That way, you can use the properties of your custom component as input for a disabled property or as the target of an action like reloading the message.