One of WordPress’ most powerful features is that it can be extended to do almost anything. Developers have been using the Hooks API to extend core WordPress since version 2.0, and with version 5.0, the SlotFill system was introduced to allow extending the block and site editor screens.
If you are unfamiliar with the SlotFill system and how you can use it to extend the block editor and site editor, take a moment to review How to extend WordPress via the SlotFill system.
The SlotFill system can not only be used to extend existing UIs but also your custom implementations – which is the focus of this article.
Use cases
You might have any number of reasons. But the most prominent examples come from the plugin ecosystem, where these sorts of extensions have put real money in the hands of their developers.
You can classify the uses as internal or external.
An internal use example is the “freemium” pricing model, where you offer a plugin with a set of basic features. Then, if your users buy a license, you can unlock a more advanced set.
In an external extension, you set up a set of extension points that other developers can use to add features to your base plugin.
If you were only using PHP and the Hooks API to create these extension points, then this difference becomes moot as the PHP-based hooks and filters are available as soon as the relevant source code is loaded.
However, in the case of SlotFill, this is a little more complicated. The difference between these approaches is whether or not the custom SlotFills are exposed externally or if they are only available internally to the plugin’s codebase.
So let’s build some custom SlotFills and expose them.
Creating a custom Slot and Fill
A SlotFill contains two components: a Slot and a Fill. The Slot governs where the Fills will be rendered and both are connected with a common name
property.
You can import the Slot and Fill components directly from the @wordpress/components
package and add the name manually, but a helper function, createSlotFill
will do all that for you – just pass the name of your SlotFill.
import { createSlotFill } from '@wordpress/components';
const { Fill, Slot } = createSlotFill( 'BasicCreateSlotFill' );
Now you need a custom component they can live in. That will help you identify your SlotFill, sidestep naming conflicts, and work the same way WordPress core’s existing SlotFills do.
Let’s walk through an example.
Start by making a new component called BasicCreateSlotFill
(best practice: name your component after the value you pass to createSlotFill). Your new component should contain the Fill component and wrap and children components that get passed to it. Then assign the Slot component to a Slot Property of the component, and export the whole thing for use.
import { createSlotFill } from '@wordpress/components';
const { Fill, Slot } = createSlotFill( 'BasicCreateSlotFill' );
const BasicCreateSlotFill = ( { children } ) => {
return <Fill>{ children }</Fill>;
};
BasicCreateSlotFill.Slot = Slot;
export default BasicCreateSlotFill;
Now can now use your custom SlotFill in your code! The first step is to expose the Slot
property.
const SettingsScreen = () => (
<Panel>
<PanelBody title="Basic" initialOpen={ false }>
<PanelRow>
<BasicCreateSlotFill.Slot />
</PanelRow>
</PanelBody>
</Panel>
);
Next, register a plugin that will target the Slot.
The registerPlugin function is not related to WordPress plugins found on https://wordpress.org/plugins/. It is used to register JavaScript plugins containing Fills. You can more about it in How to extend WordPress via the SlotFill system
registerPlugin( 'custom-slot-fills', {
render: () => (
<BasicCreateSlotFill>
<p>{ `This appears where <BasicCreateSlotFill.Slot/> is rendered` }</p>
</BasicCreateSlotFill>
),
} );
Remember how you wrapped children
in the Fill
component when you built the BasicSlotFill
component? The children
are any elements that you wrapped in the BasicSlotFill component in the registerPlugin
call. In the example above, the p tag and its text content are the children and will render where <BasicCreateSlotFill.Slot />
is.
Reference Codebase
An accompanying code repository for this article available on GitHub. It provides some examples of both of the use cases outlined above. Take a moment now to follow the set up instructions to get the example working on your computer.
Internal example: “Freemium” features
With the example code in place, navigate to the plugins/freemium-inc
folder. This folder contains all of the code for this example and is a standard WordPress plugin containing a single block that was scaffolded using the @wordpress/create-block
package.
The block itself does nothing of value beyond outputting a default message. There are two new files that have been added to the plugin:
src/slotfills.js
: used to store our custom SlotFillswebpack.config.js
: updates the built-in build process to create a separate file that contains our premium features.
The theory
The approach here is to expose a custom SlotFill in the InspectorControls
for the block and then conditionally enqueue a separate file that uses registerPlugin
to add more controls if the “premium” features have been unlocked.
Implementation
Open the src/slotfills.js
file and create a custom SlotFill called <PremiumFeatures />
.
/**
* WordPress dependencies
*/
import { createSlotFill } from '@wordpress/components';
/**
* Create our Slot and Fill components
*/
const { Fill, Slot } = createSlotFill( 'PremiumFeatures' );
const PremiumFeatures = ( { children } ) => <Fill>{ children }</Fill>;
PremiumFeatures.Slot = ( { fillProps } ) => (
<Slot fillProps={ fillProps }>
{ ( fills ) => {
return fills.length ? fills : null;
} }
</Slot>
);
export default PremiumFeatures;
Next, expose PremiumFeatures.Slot
in the block’s InspectorControls by adding in src/edit.js
export default function Edit( props ) {
const {
attributes: { makeItFaster },
setAttributes,
} = props;
return (
<>
<InspectorControls>
<PanelBody
title={ __( 'Freemium Inc. Settings', 'developer-blog' ) }
>
<CheckboxControl
checked={ makeItFaster }
label={ __(
'Make my site a little faster',
'developer-blog'
) }
onChange={ () =>
setAttributes( { makeItFaster: ! makeItFaster } )
}
/>
<PremiumFeatures.Slot fillProps={ { ...props } } />
</PanelBody>
</InspectorControls>
<p { ...useBlockProps() }>
{ __( 'Freemium INC Example', 'developer-blog' ) }
</p>
</>
);
}
Notice that you are passing all of the block props
to the Slot via the fillProps
property. This allows your extension to access everything that the block has access to such as its attributes
, setAttributes
function, etc. This is very important if your extension needs to update block attributes or respond to changes in those properties.
Next, create a separate file that holds the premium items for your plugins called premium/index.js
.
/**
* WordPress dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { CheckboxControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import PremiumFeatures from '../src/slotfills';
registerPlugin( 'freemium-inc-premium-items', {
render: () => {
return (
<PremiumFeatures>
{ ( { attributes, setAttributes } ) => {
const { tenXMode } = attributes;
return (
<>
<h2>
{ __( 'Premium Features', 'developer-blog' ) }
</h2>
<CheckboxControl
label={ __(
'🔥🔥Enable 10x mode🔥🔥',
'developer-blog'
) }
help={ __(
'10x mode will make your site 10x faster.',
'developer-blog'
) }
checked={ tenXMode }
onChange={ () =>
setAttributes( { tenXMode: ! tenXMode } )
}
/>
</>
);
} }
</PremiumFeatures>
);
},
} );
Finally, update the build process to include premium/index.js
and output a separate file that we can enqueue. The build process, provided by the @wordpress/scripts
package, can be extended by adding a webpack.config.js
file to the root directory of the plugin and extending the default configuration.
In this case, you need to add a new entry
to tell Webpack that it should process a new file and output it separately
// Import the original config from the @wordpress/scripts package.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
// Import the helper to find and generate the entry points in the src directory
const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );
// Add any a new entry point by extending the webpack config.
module.exports = {
...defaultConfig,
entry: {
...getWebpackEntryPoints(),
premium: './premium/index.js',
},
};
The new entry
tells Webpack that it should look for a new file in ./premium/index.js
and output it (and any related files) with a base filename of premium. Now restart the build process using npm run star
t or npm run build
to have Webpack recognize the changes to the configuration file.
Looking in the build
directory, you should see some new files added. The exact files added will depend on which build command you ran.
premium.asset.php
premium.js
premium.js.map
( only added when using thestart
command )
Now that the build is updated and working, the final step is to conditionally enqueue the premium.js
file. This is where things could get very complicated, but for the purposes of this article, the code is using variable set to false.
/**
* Determine if the plugin has been upgraded and enqueue the assets if so.
*/
function maybe_add_premium_features() {
// This can be done any number of ways.
$user_has_upgraded = false;
$premium_assets_file = plugin_dir_path( __FILE__ ) . 'build/premium.asset.php';
if ( $user_has_upgraded && file_exists( $premium_assets_file ) ) {
$assets = include $premium_assets_file;
wp_enqueue_script(
'freemium-inc-premium',
plugin_dir_url( __FILE__ ) . 'build/premium.js',
$assets['dependencies'],
$assets['version'],
true
);
}
}
add_action( 'enqueue_block_editor_assets', 'maybe_add_premium_features' );
Insert the block and then in the code set the $user_has_upgraded
to true
to see the Premiums settings.
Screenshots
External example: Extending Advanced Query Loop
Creating an external example requires an existing codebase that exposes SlotFills for use. For this tutorial, you will be extending my Advanced Query Loop (AQL) plugin. You can see all of the code for AQL, including documentation for the SlotFills, in the GitHub repository.
The theory
External codebases are not available to extenders and as such, extra steps must be taken to expose the Slots for use. Luckily, this can be done using the @wordpress/scripts
package with a custom webpack.config.js
file.
Using the example below taken from AQL, you can create a new JavaScript object attached to the window
object that will store the Slots (or any items as needed) by adding the library
to the output
property.
// Import the original config from the @wordpress/scripts package.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
// Import the helper to find and generate the entry points in the src directory
const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );
// Add any a new entry point by extending the webpack config.
module.exports = {
...defaultConfig,
entry: {
...getWebpackEntryPoints(),
variations: './src/variations/index.js',
},
output: {
...defaultConfig.output,
library: [ 'aql' ],
},
};
Now any items exported from /src/variations/index.js
will be exposed in a global aql
object.
/**
* WordPress dependencies
*/
import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './controls';
import AQLIcon from '../components/icons';
import AQLControls from '../slots/aql-controls';
import AQLControlsInheritedQuery from '../slots/aql-controls-inherited-query';
const AQL = 'advanced-query-loop';
registerBlockVariation( 'core/query', {
name: AQL,
title: __( 'Advanced Query Loop', 'advanced-query-loop' ),
description: __( 'Create advanced queries', 'advanced-query-loop' ),
icon: AQLIcon,
isActive: [ 'namespace' ],
attributes: {
namespace: AQL,
},
scope: [ 'inserter', 'transform' ],
} );
export { AQL, AQLControls, AQLControlsInheritedQuery };
The implementation
Since the majority of the work is being done in the external codebase, you only need to import and use the Slots as you would any other.
In the example repository, navigate to the plugins/aql-extension
directory to see all of the code for this example. This again is a simple WordPress plugin with a custom webpack.config.js
that is used to build out a single file stored in aql-extension/slotfills/index.js
where the custom Fills are registered using registerPlugin
:
/**
* WordPress dependencies
*/
const { AQLControls, AQLControlsInheritedQuery } = window.aql;
import { registerPlugin } from '@wordpress/plugins';
import { ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
const LoggedInUserControl = ( { attributes, setAttributes } ) => {
const { query: { authorContent = false } = {} } = attributes;
return (
<>
<ToggleControl
label={ __( 'Show content for logged in user only' ) }
checked={ authorContent === true }
onChange={ () => {
setAttributes( {
query: {
...attributes.query,
authorContent: ! authorContent,
},
} );
} }
/>
</>
);
};
registerPlugin( 'aql-extension', {
render: () => {
return (
<>
<AQLControls>
{ ( props ) => <LoggedInUserControl { ...props } /> }
</AQLControls>
<AQLControlsInheritedQuery>
{ ( props ) => <LoggedInUserControl { ...props } /> }
</AQLControlsInheritedQuery>
</>
);
},
} );
The Advanced Query Loop plugin exposes two Slots; one for when the Query is being inherited and one for when it is not. This code adds the new control to both.
From a SlotFill perspective, this is a complete example and you will now have a new control available under the Advanced Query Settings tab to only show posts for logged in users.
There is some additional PHP code in this example to make it function with AQL. While explaining that code is beyond the scope of this article, you are most welcome to explore it!
Screenshots
Props to @greenshady, @marybaum, and @juanmaguitar for reviewing this article.
Leave a Reply