Hey visitors! If you’re a beginner in WordPress and WP-CLI development, then you should check out my dear friend Bhargav’s Youtube channel! @BuntyWP

Building an extendible WordPress admin Settings page with Gutenberg Components

In a WordPress settings page 3rd-party developers can inject their own settings fields / HTML with action hooks, but you cannot do the same if you decide to build your plugin’s settings page in React.

This article explores the options to make our React Components extendible such that 3rd-party developers can easily inject their own components in them.

We are going to use SlotFillProvider, Slot, Fill and PluginArea Gutenberg React Components.

Part 1: Building a simple settings page

We’re going to build a very simple settings page. For the sake of simplicity of this article, we won’t complicate it by writing any server-side logic to save the fields, nor will cover managing the state of our application because our focus is just rendering our components and allowing other developers to render their own components.

So, we will create an AdminSettings Component that renders First Name, Last Name text fields and a Save button.

WordPress settings page built using Gutenberg components.

The following code will result in the image you see above:

import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    TextControl,
    Button,
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <>
            <h1>{ __( 'Settings using Gutenberg components' ) }</h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            </BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            </BaseControl>

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            </Button>
        </>
    );
};Code language: JavaScript (javascript)

Understanding Slot and Fill Components

If we were to compare the behavior with the PHP hooks, the I’d say Slot is similar to what do_action() does and Fill is similar to add_action().

As a plugin developer, you usually provide both the Slot and Fill. 3rd-party developers will inject their Components in this Slot using Fills. But we will do it differently. Instead of relying on other developers to implement Fills, we will expose a component on the global window object that implement Fills.

A Slot and a Fill is connected through the name attribute. For example:

<Slot name="additional-fields-area"/>
<Fill name="additional-fields-area">
    <TextControl label="Injected field"/>
</Fill>Code language: HTML, XML (xml)

They are only connected if both share the same value for the name attribute.

Understanding SlotFillProvider

SlotFillProvider is a Context provider for Slot and Fill. When you’re using Slot-Fill, you must ensure that both Slot and Fill are inside the SlotFillProvider, else it won’t work.

When you’re building an extendible Gutenberg block, you don’t need to use SlotFillProvider because the entire Gutenberg editor Component is already inside it. But if you’re building an extendible component which will be used outside of the Gutenberg editor, then you must use it.

Understanding how Fill works

As long as Fill is inside the SlotFillProvider, you can render it anywhere, it doesn’t matter. Whatever is between the Fill open and close tags will always be rendered where the connected Slot is.

The SlotFill mechanism is built using React Portals.

Part 2: Adding a Slot to our Settings Component

import { __ } from '@wordpress/i18n';
import { PluginArea } from '@wordpress/plugins';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }</h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            </BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            </BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <br />
            <Button variant='primary'>
                { __( 'Save' ) }
            </Button>
        </SlotFillProvider>
    );
};
Code language: JavaScript (javascript)

Great! We’re already halfway into building an extendible component.

Now remember I said in the previous point – Fill has to be inside the SlotFillProvider for this to work. You might wonder, how can 3rd-party developers add a Fill here? This will be explained next.

Revising registerPlugin

Gutenberg editor provides a number of SlotFills to allow developers to inject their Components in specific areas of the editor. You may have already used some of them already. If you remember, you may have done something like:

import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { PluginPostPublishPanel } from '@wordpress/editor';

registerPlugin( 'third-party-plugin', {
    render: InjectSomeText,
} );

function InjectSomeText() {
    return (
        <PluginPostPublishPanel>
            <p>Post Publish Panel</p>
        </PluginPostPublishPanel>
    );
}Code language: JavaScript (javascript)
The PluginPostPublishPanel Slot is highlighted in blue at the bottom.

Gutenberg provides the following SlotFills:

If InjectSomeText does not return at least one of these, then whatever the component returns won’t be rendered anywhere. So it is clear – The Components rendered by registerPlugin must return at least one of the above.

But why is that? Why does registerPlugin refuse to render a Component that does not return one of the core SlotFills?

This is because, the core SlotFills return Fill components that are connected to the pre-defined Slot areas. And these Fill components are rendered inside PluginArea components.

Understanding PluginArea

The registerPlugin function is very closely related to the PluginArea component and they work together. Let’s try to understand it with a practical example.

import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot,
    Fill
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }</h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            </BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            </BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            </Button>
        </SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { children }
            </Fill>
        </>
    );
};
Code language: JavaScript (javascript)

Similar to how core provides us with a list of SlotFills, we created a custom SlotFill Component called PluginGutenbergSettingsFields and exposed it on the global window object, so that other developers can easily use it with registerPlugin.

But you might wonder, wait! Didn’t I mention previously that Fill should be inside the same SlotFillProvider ? If other 3rd-party developers use PluginGutenbergSettingsFields to inject code, where would it be rendered? – That’s a good question!

This is where, PluginArea is important!

import { __ } from '@wordpress/i18n';
import { PluginArea } from '@wordpress/plugins';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot,
    Fill
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }</h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            </BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            </BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <PluginArea/>

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            </Button>
        </SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { children }
            </Fill>
        </>
    );
};
Code language: JavaScript (javascript)

I will explain the entire flow in the next section.

Part 3: Extending as a 3rd-party developer

As a 3rd-party developer, I would like to add a ToggleControl Component to the settings page.

Injecting a ToggleControl by extending a Component using Slot and Fills.
import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    ToggleControl
} from '@wordpress/components';

const { PluginGutenbergSettingsFields } = window;

registerPlugin( 'third-party-plugin', {
    render: InjectAdditionalFields,
    scope: PluginGutenbergSettingsFields.scope // Ignore this for now.
} );

function InjectAdditionalFields() {
    return (
        <PluginGutenbergSettingsFields>
            <BaseControl label={ __( 'Activate Account?' ) }>
                <ToggleControl/>
            </BaseControl>
        </PluginGutenbergSettingsFields>
    );
}Code language: JavaScript (javascript)

Summary:

registerPlugin renders InjectAdditionalFields where the PluginArea component is.

PluginArea renders a hidden div which renders the contents of InjectAdditionalFields.

– InjectAdditionalFields returns our custom SlotFill PluginGutenbergSettingsFields.

PluginGutenbergSettingsFields renders Fill next to the Slot and also inside the same SlotFillProvider

Slot with the help of SlotFillProvider renders content that is between the Fill tags.

Extra: Exposing application data to 3rd party components

Obviously, the Toggle Field will require access to the AdminSettings state data. For example, if you want to make the Toggle Field conditional depending on the value of some other field?

Slot component accepts a prop called as fillProps. This is how you do it:

But to access this data, AdminSettings, PluginGutenbergSettingsFields and InjectAdditionalFields needs to be updated like the following:

import { useState } from '@wordpress/element';

export const AdminSettings = () => {

    const [ appData, setAppData ] = useState( { fname: 'Siddharth', lname: 'Thevaril' } );

    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }</h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl value={ appData.fname }/>
            </BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl value={ appData.lname }/>
            </BaseControl>

            <Slot name="gutenberg-settings-additional-fields" fillProps={ { appData, setAppData } } />

            <PluginArea />

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            </Button>
        </SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { ( fillProps ) => children( fillProps ) }
            </Fill>
        </>
    );
};
Code language: JavaScript (javascript)

And for the plugin:

function InjectAdditionalFields() {
    return (
        <PluginGutenbergSettingsFields>
            {
                ( fillProps ) => {
                    return (
                        <BaseControl label={ __( 'Activate Account?' ) }>
                            <ToggleControl />
                        </BaseControl>
                    )
                }
            }
        </PluginGutenbergSettingsFields>
    );
}
Code language: JavaScript (javascript)

Tip:

If your settings page is large and the app data is complex, I would suggest to implement a separate data store instead of passing app state via fillProps.

I hope this article was helpful. I have pushed the code to a Github repository if you want to experiment with. Thank you for reading!



Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

Powered by WordPress