WordPress.org

WordPress Developer Blog

Extending plugins using custom SlotFills

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/componentspackage 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.

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 SlotFills
  • webpack.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.

Callout image for the How to extend WordPress via the SlotFill system article

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 start 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 the start 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.

2 responses to “Extending plugins using custom SlotFills”

  1. Michelle B. Avatar

    Really great deep dive on SlotFills. Thank you for posting this!

    I’m wondering, though, what exactly are the additional benefits from using SlotFills vs basic JavaScript hooks to dynamically insert content from internal and external scripts?

    I’ve been using a simpler approach which simply exposes an array that other scripts can filter to push in additional components. Since React already supports rendering arrays of elements and components, I’m wondering what there is to be gained from the more complicated SlotFills approach.

    Here’s an example of what I mean (hopefully this will render properly 🤞):


    // MyAwesomePluginComponent.jsx
    const maybeContentAfterDescription = window.Completionist.hooks.applyFilters(
    'content_after_description',
    [],
    someOtherContextVariable
    );
    // render
    return (

    Here is the description. You can use the 'content_after_description' hook to add more content below this!
    { maybeContentAfterDescription }

    );


    // YourCoolExtension.jsx
    import YourCoolContent from './path/to/YourCoolContent.jsx';
    window.MyAwesomePlugin.hooks.addFilter(
    'content_after_description',
    'your-cool-extension',
    ( content, someOtherContextVariable ) => {

    if ( 'specific_context' === someOtherContextVariable ) {
    content.push();
    }

    return content;
    }
    );

    This approach takes less code and is more reminiscent of using hooks in PHP which we all know and love. So what does the SlotFills approach have to offer differently?

    1. David Gwyer Avatar
      David Gwyer

      I use this approach too so would like to understand what benefits using SlotFills would bring too. 🙂

Leave a Reply

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