Using WordPress JavaScript APIs: Slots and Fills
Welcome to part five of our series on using the WordPress JavaScript APIs, in which we explore the APIs that were introduced in WordPress 5.0. We’ll look at how we can use the APIs to better integrate with other plugins, in a reliable and safe way. Make sure to also check out our repository on GitHub, which contains all the code we’ll be writing in this series.
In the previous chapter, we integrated our dummy version of the core/editor
package to simulate how WordPress handles content in the new editor and how you can access this information in your own codebase for various purposes. In this chapter, we’ll be exploring Slots and Fills that are part of the @wordpress/components
package, which will allow your plugin to integrate into various parts of the UI of WordPress. Additionally, we’ll be creating a variety of dummy versions of WordPress code to display the process of registering your own sidebars!
The @wordpress/components
package
The package that we’re going to be using contains a variety of UI-related components such as Buttons, Panels, Icons, etc. We’ll be focussing on three components within this package, namely the SlotFillProvider
, Slot
and Fill
components to create the next iteration of our application.
If you’re familiar with React, you can see this package as something similar to Portals in React, which allow you to render a component elsewhere in the DOM-structure, rather than in the ‘physical’ location of the component.
Before we begin
Due to the React-based nature of the @wordpress/components
library, we won’t be modifying the jQuery version of the form in this post. We’ll address this later in the post as to why.
Additionally, we’ll first discuss the way WordPress allows developers to expand the UI by registering their own sidebar and then discuss how Slot
, Fill
and the SlotFillProvider
tie into this.
Implementing the code
Before we start, a quick note on the code that we’ll be writing: As we want to demonstrate the ease of use of the functions that are present in the WordPress APIs, we’ll be recreating some of them in our own codebase. This is similar to the ‘dummy’ core/editor
we made in the previous post, but with a few more layers. Don’t worry: We’ve simplified most of the code to keep things concise and we’ll discuss what features might be missing compared to the real-world implementation.
Allowing developers to register a plugin
The first bit of code we will be creating is a simple function that registers the passed settings as a plugin, under a specific namespace. This will later be available for the rest of the code we’ll be writing and is used to generate a sidebar.
First, let’s create a new directory named utils
in the src
directory. In this directory, create a file named api.js
and add the following code to it:
/** * Contains the registered plugins. * * @type {Object} The registered plugins. */ const plugins = {}; /** * Registers a plugin to be used in the sidebar. * * @param {string} name The name of the plugin. * @param {Object} settings The settings for the plugin. * * @returns {Object} The settings. */ export function registerPlugin( name, settings ) { // Skipping validation methods for the time being. plugins[ name ] = { name, icon: 'admin-plugins', ...settings, }; return settings; } /** * Gets a specific plugin from the list of registered plugins. * * @param {string} name The name of the plugin to retrieve. * * @returns {Object} The plugin instance. */ export function getPlugin( name ) { return plugins[ name ]; } /** * Gets all the registered plugin instances. * * @returns {Object[]} The plugin instances. */ export function getPlugins() { return Object.values( plugins ); }
The above code is rather isolated. First, we’ll create an empty object that will hold every registered plugin. Then, we’ll create and export three functions to interact with the plugins object.
The registerPlugin
function is the main driving force behind allowing other plugins to interact with the WordPress UI. In our more simplistic implementation, we just add a new entry to the plugins
object, consisting of a name (which is actually a namespace) and an object containing all relevant information pertaining to the plugin. Please note: The implementation of this function is far more expansive in WordPress and contains a bunch of validation and extra checks to ensure that no duplicate entries exist. Additionally, the above icon
property does not get used in our code and is purely to demonstrate WordPress’ implementation.
The getPlugin
and getPlugins
functions are pretty self-explanatory: They allow you to either get a single plugin from the object, or all plugins from the object.
The PluginContextProvider component
Before we utilize the code we’ve written in the previous section, let’s take a look at some of the parts further down the chain and explain what WordPress uses them for. One of these components is the PluginContextProvider. What this component essentially does, is provide a certain plugin context to an underlying Provider component, which is a concept we already used before in part two of our series.
Due to the complex nature of this component, and to ensure we don’t go down a very deep rabbit hole, we’ll be creating a far more simplistic version of this component. It consists of the following code, which will somewhat illustrate part of how WordPress handles providing data to components:
import React from "react"; import { SlotFillProvider } from "@wordpress/components"; /** * The PluginContextProvider component. */ class PluginContextProvider extends React.Component { /** * Renders the PluginContextProvider and any passed slots and fills. * * @returns {SlotFillProvider} The SlotFillProvider. */ render() { return ( <SlotFillProvider> { this.props.children } </SlotFillProvider> ); } } export default PluginContextProvider;
Instead of utilizing a Provider
component to demonstrate what WordPress is doing, we’ll use the SlotFillProvider
that gets shipped with the @wordpress/components package instead. This provider ensures that both Slots and Fills are aware of each other’s existence, but requires you to place them within this component because otherwise, it will not work. After that, we let React render any of the passed child components.
The PluginArea component
The next component we’ll introduce is, you guessed it, another simplified version of one that gets shipped with WordPress. What this component does, is take all the registered plugins, create a PluginContextProvider component and passes the plugin on to the provider. After this, it outputs the plugin in its own (hidden) section in the sidebar. Additionally, this component allows for some actions, just like in WordPress’ PHP code, to be called whenever the PluginArea is loaded. In our simplified version, we will be limiting ourselves to solely retrieving the plugins and ensuring they get placed in the sidebar. In the js/src/components
directory, add a new file named PluginArea.js
and add the following code:
import React from "react"; import { getPlugins } from "../utils/api"; import PluginContextProvider from "../components/PluginContextProvider"; /** * The PluginArea component. */ class PluginArea extends React.Component { /** * Constructs the component. * * @param {Object} props The props to pass along. */ constructor( props ) { super( props ); this.setPlugins = this.setPlugins.bind( this ); this.state = this.getCurrentPluginsState(); } /** * Gets the state of all registered plugins. * * @returns {Object} The registered plugins and their state. */ getCurrentPluginsState() { return { plugins: Array.map( getPlugins(), ( { icon, name, render } ) => { return { Plugin: render, context: { name, icon } } } ), } } /** * Sets the plugin's state in the PluginArea's state. * * @returns {void} */ setPlugins() { this.setState( this.getCurrentPluginsState ); } /** * Renders the PluginArea and any registered plugin. * * @returns {PluginArea} The PluginArea. */ render() { return ( <div> { Array.map( this.state.plugins, ( { context, Plugin } ) => ( <PluginContextProvider key={ context.name } context={ context }> <Plugin /> </PluginContextProvider> ) ) } </div> ); } } export default PluginArea;
The above code retrieves the registered plugins via the getPlugins
function we wrote earlier and ensures anything written into each registered plugin’s state, gets mapped to the PluginContextProvider
. The PluginContextProvider
then renders out the mapped plugin as a child component.
The Editor component
The Editor
component is the one that we’ll hold responsible for creating a layout of sorts for our demo application. In a WordPress-based implementation, this part could be skipped entirely.
In the js/src/components
directory, create a file named Editor.js
and add the following code to it:
import React from "react"; import SimpleForm from "./simple-form-react"; import PluginArea from "./plugin-area"; /** * The Editor component. */ class Editor extends React.Component { /** * Renders the component. * * @returns {Editor} The Editor component. */ render() { return ( <div className="flex-grid"> <section className="col main"> <SimpleForm /> </section> <aside className="col sidebar"> <PluginArea /> </aside> </div> ); } } export default Editor;
The above code doesn’t do much more than create a layout for us, apply some CSS and implements the SimpleForm
and PluginArea
component, which we wrote earlier in this post. We won’t be discussing the CSS that was used, but if you’d like to take a look, you can find it in the repository on GitHub.
Please note: On line 2 of the above code snippet, we’re importing the simple-react-form
file from the same directory. This differs from the previous chapters, where this file lived in the js/src
directory. Please make sure you move the simple-react-form.js
file into the js/src/components
directory, or else the above example won’t work!
PluginSidebar
The PluginSidebar
component is a UI-related component that is shipped by WordPress to make integrating with the sidebar that much more easier. Usually speaking, this component will ensure a proper HTML-structure for the registered plugin so that it adheres to WordPress’ styling. Additionally, this plugin will register a small toggle button in the toolbar that you can find in the top-right corner while editing a post or page. In our example, we’ll be omitting this small toggle button and solely focus on enforcing a proper HTML-structure. In the js/src/components/
directory, create a new file called PluginSidebar.js
and add the following code:
import React from "react"; /** * The PluginSideBar component. */ class PluginSidebar extends React.Component { /** * Renders the PluginSidebar. * * @returns {PluginSidebar} The PluginSidebar. */ render() { return ( <div> <div className="plugin-sidebar-header"><strong>{this.props.title || 'Untitled sidebar'}</strong></div> {this.props.children} </div> ); } }; export default PluginSidebar;
As we already mentioned, this component outputs an HTML-structure we’ve deemed correct for our example project. Additionally, the component allows you to set a title for the sidebar and ensures any nested child components are rendered within the defined structure. The above code is another oversimplified example compared to WordPress’ implementation, but it does cover the basics of how it works.
But wait: there’s more!
The PluginSidebar
is just one of the components that developers can use to extend the WordPress UI. In Yoast SEO, we also utilize the PluginSidebarMoreMenuItem
component, which allows us to register our plugin under the ‘More’ menu item (the three dots in the top-right corner of the editor). As this is just another convenience type of component, we won’t be discussing its implementation.
Creating our ‘plugin’
All the code we’ve written up until now is related to our ‘dummy’ implementation of WordPress’ APIs. Now, let’s start creating some code that is related to our ‘plugin’ to demonstrate how one could register their own sidebars and make use of Slot and Fill components. Before we begin, create a new directory within the js/src/ directory and call it plugins. Within that directory, create another directory called MyAwesomePlugin
. This is where our ‘plugin’ code will live.
Creating a Fill
Firstly, let’s create a Fill
that will render itself within a Slot
that we’ll define later on in our ‘plugin’, which upon registration will be rendered in the sidebar. The below Fill
is just one example. If you’d like to see more examples, make sure you check out the js/src/plugins/MyAwesomePlugin/components/fills
directory in the repository.
To demonstrate the usage of Fills
, we’ll be creating one that reads out the content we’ve written and provides some feedback on it. Create a file named ContentFill.js
in the js/src/plugins/MyAwesomePlugin/components/fills
directory and add the following code:
import React from "react"; import { compose } from "@wordpress/compose"; import { withSelect } from "@wordpress/data"; import { Fill } from "@wordpress/components"; import { count_words } from "../utils/strings"; /** * The ContentFill component. */ class ContentFill extends React.Component { /** * Evaluates the content and returns feedback based on the length of the content. * * @returns {string} The feedback. */ evaluateContent() { const content = this.props.content; const wordCount = count_words( content ); if ( wordCount === 0 ) { return "You haven't added any content. Please add some."; } return `Total words: ${wordCount}`; } /** * Renders the component. * * @returns {ContentFill} The component. */ render() { return ( <Fill name="MyAwesomePluginSidebar"> <p>{ this.evaluateContent() }</p> </Fill> ); } } /** * Maps the state to props. * * @param {Object} state The state to map. * * @returns {Object} The mapped props. */ const mapStateToProps = ( select ) => { const store = select( "core/editor" ); return { content: store.getEditedPostContent(), }; } export default compose( withSelect( mapStateToProps ) )( ContentFill );
The above code contains some things we might recognize from our form we’ve been working on. Especially the mapStateToProps
function should seem familiar. To give the Fill
some purpose, we’re using this function to extract information from the store and map it to a prop. This will allow us to evaluate the content and provide the user with feedback.
The evaluateContent
method takes the content property and counts the length of it. Based on this, it will display a message regarding the content. For demonstration purposes, we’ve decided to check for an empty content field. If the field isn’t empty, it’ll count the total amount of words typed.
Lastly, the render
method is what returns the Fill
component. The name property tells React what Slot
to occupy and thus when the ContentFill
component gets rendered, will be placed in whatever location the Slot
is currently at.
What’s this count_words
thing?
You might be wondering where that count_words
function is coming from. To better illustrate some of the uses of Fills, we created a separate directory in our js/src/plugins/MyAwesomePlugin/
directory called utils
which contains a helper file called strings.js
. The contents of this file are as follows:
/** * Counts the amount of words in the passed text. * * @param {string} text The text to count the words in. * * @returns {number} The amount of words. */ export function count_words( text ) { const words = text.trim().split( " " ); // Counteracts the empty string being counted as a word. if ( words[0] === "" ) { return 0; } return words.length; } /** * Converts the passed text to a slug-compatible version. * * @param {string} text The text to convert. * * @returns {string} The slug-compatible version. */ export function to_compatible_slug( text ) { return text.trim().replace( /[^a-zA-Z0-9-_/\s]+/g, '' ).replace( /\s+/g, '-' ).toLowerCase(); }
The first function in the above file does a crude splitting of words based on the usage of spaces and counts the total amount of items in the array. The way split
works, when no items have been found to split, the array will contain the original string. That’s why we’re checking that the item at index 0 isn’t an empty string.
The second function is a bit more complex. We start by filtering the characters through a regular expression, only leaving behind characters that we deem valid for a slug. After this, the last step is to replace all spaces with hyphens and convert the string to lowercase characters, to ensure consistency with how browsers show URLs. If you want to see an example of its implementation, check out the SlugFill
component.
The Sidebar
The last component we’ll add is a Sidebar
component. This is our own custom sidebar code and can contain whatever you want. Whatever you define in this file, will eventually be rendered in the Slot
we’ll be defining shortly. In the js/src/plugins/MyAwesomePlugin/components/Sidebar.js
file, add the following code:
import React, { Fragment } from "react"; import ContentFill from "./fills/ContentFill"; import PluginSidebar from "../../../components/PluginSidebar"; import { Slot } from "@wordpress/components"; /** * The Sidebar component. */ class Sidebar extends React.Component { /** * Renders the sidebar. * * @returns {Sidebar} The Sidebar component. */ render() { return ( <Fragment> <PluginSidebar name="my-awesome-plugin-sidebar" title="My Awesome Plugin"> <Slot name="MyAwesomePluginSidebar" /> </PluginSidebar> <ContentFill /> </Fragment> ); } } export default Sidebar;
The above code that lives within our ‘plugin’ does nothing more than render out a component that consists of a Fragment
which contains the PluginSidebar
component that we’ve created earlier (which usually is accessible via the wp.editPost
global) and contains the Slot
component, which we’ve given a unique name. This name might seem familiar; it’s the same name we defined earlier in our ContentFill
component and basically defines the connection between the Slot
and the Fill
. Below that, we’ve added the ContentFill
component.
Our main plugin file
The main file within our ‘plugin’ will be responsible for making sure that the registration is properly done, by utilizing the registerPlugin
method that we recreated earlier on. In your plugin directory, create the file MyAwesomePlugin.js
and add the following code:
import Sidebar from "./components/Sidebar"; import { registerPlugin } from "../../utils/api"; /** * MyAwesomePlugin. */ class MyAwesomePlugin { /** * Registers the plugin. */ constructor() { registerPlugin( "My-awesome-plugin", { render: Sidebar } ); } } export default MyAwesomePlugin;
The above code is relatively simple. On the creation of a new instance of MyAwesomePlugin
, we’ll register the Sidebar
component that we made so that WordPress is aware of its existence.
The app.js
file
Lastly, we’ll have to edit the app.js
file to tie everything together. Copy the following code and overwrite everything in js/src/app.js
:
import React from "react"; import ReactDOM from "react-dom"; import { combineReducers, registerStore } from "@wordpress/data"; import editorReducer from "./reducers/core-editor"; import * as editorActions from "./actions/core-editor"; import * as editorSelectors from "./selectors/core-editor"; import Editor from "./components/Editor"; import MyAwesomePlugin from "./plugins/MyAwesomePlugin/MyAwesomePlugin"; import MyNotSoAwesomePlugin from "./plugins/MyNotSoAwesomePlugin/MyAwesomePlugin"; registerStore( "core/editor", { reducer: combineReducers( { editor: editorReducer } ), selectors: editorSelectors, actions: editorActions, } ); /** * The App component. */ class App extends React.Component { /** * Constructs the component. * * @param {Object} props The props to pass along. */ constructor( props ) { super( props ); this.init(); } /** * Registers the plugins to be used in the application. * * @returns {void} */ init() { new MyAwesomePlugin(); new MyNotSoAwesomePlugin(); } /** * Renders the editor. * * @returns {Editor} The editor. */ render() { return ( <Editor /> ); } } ReactDOM.render( <App/>, document.getElementById( "root" ) );
First off, we’ll import some new things into this file, namely:
- The
Editor
component - The
MyAwesomePlugin
class - The
MyNotSoAwesomePlugin
class.
We didn’t cover the last class in this list. This class is just part of a copy of the MyAwesomePlugin directory, which we copied and changed the names for demonstration purposes. This secondary plugin solely consists of a Sidebar
component with its own unique slot name and a MyNotSoAwesomePlugin
class that registers a new sidebar and unique slot. Nothing will be rendered within it unless you have a Fill
that has the same name defined as the Slot
. Feel free to copy over one of the Fills
from the MyAwesomePlugin
directory, change the name of the Slot
and see what we mean.
As the last step within this init
method, we’ll register both our ‘plugins’ by instantiating them.
That’s it! Now make sure you run yarn build-dev
and test out the changes we’ve just made! If all has gone well, you should see both the sidebars on the right: One contains the ContentFill
and the other sidebar is empty (unless you copied and altered one of the other Fills
).
Please note: The App
component is a bit of a hybrid component in this demonstration. Part of it mimics WordPress’ editor and another part of it handles the registration of our ‘plugins’. This differs from the real-world implementation where you, as a plugin developer, would most likely initialize your plugin in a different manner. Possible options for this are initializing via a filter or initializing once the DOM is fully loaded.
A note on jQuery
As we briefly mentioned at the beginning of this article, we haven’t implemented a similar feature in our jQuery version of the form. The reason behind this is that WordPress has fully switched over to React. So, if you want to become as interoperable as possible, you’ll have to adhere to the approach used within the JavaScript APIs. Luckily, we’ve been decoupling our code into various layers (i.e. stores, reducers, actions, and selectors), so making the jump to React is suddenly a lot less of a hassle.
Conclusion
That’s all for chapter 5 in our series on how to implement the WordPress JavaScript APIs into your own code to become more interoperable with WordPress itself! Our next and final chapter will be dedicated to looking back at all that we’ve learned in this series and some of the situations that can’t be solved just yet, resulting in page builders not being able to fully make the switch over to a full-fledged WordPress 5.0+ integration.
Coming up next!
-
Event
WordCamp Netherlands 2024
November 29 - 30, 2024 Team Yoast is at Sponsoring WordCamp Netherlands 2024! Click through to see who will be there, what we will do, and more! See where you can find us next » -
SEO webinar
The SEO update by Yoast - October & November 2024 Edition
26 November 2024 Get expert analysis on the latest SEO news developments with Carolyn Shelby and Alex Moss. Join our upcoming update! ?️ All Yoast SEO webinars »