Working with Gutenberg and the WordPress REST API

In this article I take the Select2 component that I previously added to my Gutenberg plugin, and make it dynamic by taking advantage of the WordPress REST API. This code borrows heavily from the Secure Blocks for Gutenberg plugin.

In this example we will be making the ‘Select Roles’ Inspector use dynamic data from the REST API.

Use the 'Select Roles' dropdown to restrict content by user role
Use the ‘Select Roles’ dropdown to restrict content by user role

Oh no, withAPIData is depreciated!

Adding REST API data to your Gutenberg block used to be very easy to do, with withAPIData and my solution originally looked a lot like this:

...
withAPIData( props => {
    return {
        roles: '/matt-watson/secure-blocks/v1/user-roles/'
    };
} )( props => {
...

This approach worked great, but in Gutenberg 3.7, withAPIData is deprecated, and the recommended replacement is withSelect.

Don’t Worry, withSelect to the Rescue

By default withSelect works great with core REST API functions, and there are a bunch of methods already available for getting things like posts, taxonomies etc… However at the time of writing this article I struggled to find guidance about using a custom REST API endpoint with withSelect

I initially wrote a Stack Exchange question asking how to do this, but, as my fellow colleagues will testify, I don’t give in until I find an answer, and a couple days later I answered my own question.

Here is a step by step guide on how to use the REST API in your Gutenberg block. For those of you that like to follow through the code, you can get my complete solution by looking at the Secure Blocks for Gutenberg plugin’s codebase in the Secure Blocks for Gutenberg GitHub repository,

Build Your Gutenberg Block

Before you start to make things dynamic it is a good idea to build your Gutenberg Block with registerBlockType. Here is one I made earlier (the main index file of the Secure Blocks plugin). For more information see my blog post about Select2 as this uses the external react-select component that is used here.

/**
 * Import Assets
 */
import '../scss/style.scss';
import '../scss/editor.scss';
import '../scss/admin.scss';

/**
 * Block Dependencies
 */
import icons from './icons';
import classnames from 'classnames';
import Select from 'react-select';

/**
 * Internal Block Libraries
 */
const { __ }                = wp.i18n;
const { registerBlockType } = wp.blocks;
const {
	InnerBlocks,
	InspectorControls,
} = wp.editor;
const {
	PanelBody,
	PanelRow,
	Spinner,
} = wp.components;


/**
 * Register secure block
 */
export default registerBlockType(
	'matt-watson/secure-block',
	{
		title:       __( 'Secure Block', 'secure-blocks-for-gutenberg' ),
		description: __( 'By default the secure content is only shown if a user is logged in. You can also restrict the block to be visible to users within certain roles.', 'secure-blocks-for-gutenberg' ),
		category:   'layout',
		icon:       'lock',
		keywords:   [
			__( 'Secure Block' ),
			__( 'Permissions' ),
			__( 'Password Protected' ),
		],
		attributes: {
			role: {
				type:    'string',
				default: null,
			},
		},
		edit: ( props => {
			const { attributes: { role }, className, setAttributes } = props;
			const handleRoleChange = ( role ) => setAttributes( { role: JSON.stringify( role ) } );
			let rolesToString = '';
			let selectedRoles = [];
			if ( null !== role ) {
				selectedRoles = JSON.parse( role );
			}

			return [
				<InspectorControls>
					<PanelBody title={ __( 'Select Roles', 'secure-blocks-for-gutenberg' ) } className="secure-block-inspector">
						<PanelRow>
							<label htmlFor="secure-block-roles" className="secure-block-inspector__label">
								{ __( 'Secure content is presented to users that are logged-in and in the following roles:', 'secure-blocks-for-gutenberg' ) }
							</label>
						</PanelRow>
						<PanelRow>
							<Select
								className="secure-block-inspector__control"
								name='secure-block-roles'
								value={ selectedRoles }
								onChange={ handleRoleChange }
								options={[
									{ value: 'health', label: 'Health' },
									{ value: 'wealth', label: 'Wealth' },
									{ value: 'code', label: 'Code' },
								]}
								isMulti='true'
							 />
						</PanelRow>
						<PanelRow>
							<em className="muted">{ __( 'No selected roles mean that secure content will be presented to all logged-in users.', 'secure-blocks-for-gutenberg' ) }</em>
						</PanelRow>
					</PanelBody>
				</InspectorControls>,
				<div className={ classnames( props.className ) }>
					<header className={ classnames( props.className ) + '__handle' }>
						<span className={ classnames( props.className ) + '__icon' }>
							{ icons.lock }
						</span>
						<span className={classnames( props.className ) + '__description'}>
							<span>{ __( 'Content shown to users that are ', 'secure-blocks-for-gutenberg' ) }</span>
							<strong>{ __( 'logged-in', 'secure-blocks-for-gutenberg' ) }</strong>
							{ selectedRoles.length === 0  ?
								<span>.</span>
							:
								<span>
									{ 1 === selectedRoles.length ?
										<span>
											{ __( ' and in the following role: ', 'secure-blocks-for-gutenberg' ) }
										</span>
									:
										<span>
											{ __( ' and in any of the following roles: ', 'secure-blocks-for-gutenberg' ) }
										</span>
									}
									<span className={classnames( props.className ) + '__roles'}>
									{ Object( selectedRoles ).map( ( value, key ) =>
										<span className="role">
											<span className="role__name">
												{ value['label'] }
											</span>
										</span>
									)}
									</span>
								</span>
							}
						</span>
					</header>
					<InnerBlocks
						template={ [
							[ 'matt-watson/secure-block-inner-secure' ],
							[ 'matt-watson/secure-block-inner-unsecure' ],
						] }
						templateLock="all"
						allowedBlocksExample={ [
							[ 'matt-watson/secure-block-inner-secure' ],
							[ 'matt-watson/secure-block-inner-unsecure' ],
						] }
						/>
					<footer className={ classnames( props.className ) + '__footer' }>
						{ __( 'End: Secure Blocks', 'secure-blocks-for-gutenberg' ) }
					</footer>
				</div>
			];
		} ),
		save: props => {
			return (
				<div>
					<InnerBlocks.Content />
				</div>
			);
		},
	},
);

There is a lot of code in there specific to the Secure Blocks plugin, but lets focus in on the dynamic areas.

Create the REST API Endpoint

In PHP, using the action rest_api_init to register a REST API endpoint with register_rest_route, I created a custom endpoint to get all of the user roles on my WordPress site and expose them via the REST API.

I ensure that they are securely accessed by making sure the user requesting the endpoint has the permission ‘edit_posts‘ so that only people that are logged in with a role that can edit posts can view the endpoint.

The code for creating the endpoint looks a lot like this:

<?php
namespace matt_watson\secure_blocks_for_gutenberg;

// Abort if this file is called directly.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class API
 *
 * WP REST API Custom Methods
 *
 * @package matt_watson\secure_blocks_for_gutenberg
 */
class API {

	private $version;
	private $namespace;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		$this->version   = '1';
		$this->namespace = 'matt-watson/secure-blocks/v' . $this->version;
	}

	/**
	 * Run all of the plugin functions.
	 *
	 * @since 1.0.0
	 */
	public function run() {
		add_action( 'rest_api_init', array( $this, 'user_roles' ) );
	}

	/**
	 * Register REST API
	 */
	public function user_roles() {

		// Council
		register_rest_route(
			$this->namespace,
			'/user-roles',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_user_roles' ),
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			)
		);
	}

	/**
	 * Get the user roles
	 *
	 * @return $roles JSON feed of returned objects
	 */
	public function get_user_roles() {
		global $wp_roles;

		$roles      = array();
		$user_roles = $wp_roles->roles;

		foreach ( $user_roles as $key => $role ) {
			$roles[] = array(
				'value' => $key,
				'label' => $role['name'],
			);
		}

		return $roles;
	}
}

Getting the REST API Data into the Gutenberg Block

Since withAPIData was removed, you now have to create a data ‘store’ with registerStore. To do this you will first need to import a few things into your Gutenberg Block like so:

const { apiFetch }          = wp;
const {
	registerStore,
	withSelect,
} = wp.data;

Now you can build the store, like this:

const actions = {
	setUserRoles( userRoles ) {
		return {
			type: 'SET_USER_ROLES',
			userRoles,
		};
	},
	receiveUserRoles( path ) {
		return {
			type: 'RECEIVE_USER_ROLES',
			path,
		};
	},
};

const store = registerStore( 'matt-watson/secure-block', {
	reducer( state = { userRoles: {} }, action ) {

		switch ( action.type ) {
			case 'SET_USER_ROLES':
				return {
					...state,
					userRoles: action.userRoles,
				};
			case 'RECEIVE_USER_ROLES':
				return action.userRoles;
		}

		return state;
	},

	actions,

	selectors: {
		receiveUserRoles( state ) {
			const { userRoles } = state;
			return userRoles;
		},
	},

	resolvers: {
		* receiveUserRoles( state ) {
			const userRoles = apiFetch( { path: '/matt-watson/secure-blocks/v1/user-roles/' } )
				.then( userRoles => {
					return actions.setUserRoles( userRoles );
				} )
			yield userRoles;
		},
	},
} );

This creates a way to resolve the JavaScript promise that is returned by apiFetch and passes it into the state that is stored in the data store.

Honestly, that final * receiveUserRoles function and how to return the promise was the main pain of this whole discovery!

If none of that means anything to you, then don’t worry, using and altering the method above should work for you and your custom endpoint.

Accessing the Data Store Using withSelect

Next up, we need to get that data into our function. We do that with withSelect like so:

...
edit: withSelect( ( select ) => {
				return {
					userRoles: select('matt-watson/secure-block').receiveUserRoles(),
				};
			} )( props => {
...

This adds userRoles to our props, that we then pass into the function like so:

const { attributes: { role }, userRoles, className, setAttributes } = props;

So now we have some dynamic data! But what if it returns before the promise has been fulfilled? Well before our main return we can have a conditional return that checks if it is empty. Here I just load the spinner and the word ‘Loading Data’ as per the dynamic blocks section in Zac Gordons Gutenberg Course

if ( ! userRoles.length ) {
				return (
					<p className={className} >
						<Spinner />
						{ __( 'Loading Data', 'secure-blocks-for-gutenberg' ) }
					</p>
				);
			}

Note that for this example I have returned the userRoles data in the exact format that I need, you may need to do some data manipulation first.

Finally, lets update our Select2 component to use the REST API data.

<Select
	className="secure-block-inspector__control"
	name='secure-block-roles'
	value={ selectedRoles }
	onChange={ handleRoleChange }
	options={ userRoles }
	isMulti='true'
	/>

Putting it Together

When we put it all together this is what we get:

/**
 * Import Assets
 */
import '../scss/style.scss';
import '../scss/editor.scss';
import '../scss/admin.scss';

/**
 * Block Dependencies
 */
import icons from './icons';
import classnames from 'classnames';
import Select from 'react-select';

/**
 * Internal Block Libraries
 */
const { __ }                = wp.i18n;
const { registerBlockType } = wp.blocks;
const { apiFetch }          = wp;
const {
	registerStore,
	withSelect,
} = wp.data;
const {
	InnerBlocks,
	InspectorControls,
} = wp.editor;
const {
	PanelBody,
	PanelRow,
	Spinner,
} = wp.components;

const actions = {
	setUserRoles( userRoles ) {
		return {
			type: 'SET_USER_ROLES',
			userRoles,
		};
	},
	receiveUserRoles( path ) {
		return {
			type: 'RECEIVE_USER_ROLES',
			path,
		};
	},
};

const store = registerStore( 'matt-watson/secure-block', {
	reducer( state = { userRoles: {} }, action ) {

		switch ( action.type ) {
			case 'SET_USER_ROLES':
				return {
					...state,
					userRoles: action.userRoles,
				};
			case 'RECEIVE_USER_ROLES':
				return action.userRoles;
		}

		return state;
	},

	actions,

	selectors: {
		receiveUserRoles( state ) {
			const { userRoles } = state;
			return userRoles;
		},
	},

	resolvers: {
		* receiveUserRoles( state ) {
			const userRoles = apiFetch( { path: '/matt-watson/secure-blocks/v1/user-roles/' } )
				.then( userRoles => {
					return actions.setUserRoles( userRoles );
				} )
			yield userRoles;
		},
	},
} );

/**
 * Register secure block
 */
export default registerBlockType(
	'matt-watson/secure-block',
	{
		title:       __( 'Secure Block', 'secure-blocks-for-gutenberg' ),
		description: __( 'By default the secure content is only shown if a user is logged in. You can also restrict the block to be visible to users within certain roles.', 'secure-blocks-for-gutenberg' ),
		category:   'layout',
		icon:       'lock',
		keywords:   [
			__( 'Secure Block' ),
			__( 'Permissions' ),
			__( 'Password Protected' ),
		],
		attributes: {
			role: {
				type:    'string',
				default: null,
			},
		},
		edit: withSelect( ( select ) => {
				return {
					userRoles: select('matt-watson/secure-block').receiveUserRoles(),
				};
			} )( props => {
			const { attributes: { role }, userRoles, className, setAttributes } = props;
			const handleRoleChange = ( role ) => setAttributes( { role: JSON.stringify( role ) } );
			let rolesToString = '';
			let selectedRoles = [];
			if ( null !== role ) {
				selectedRoles = JSON.parse( role );
			}

			if ( ! userRoles.length ) {
				return (
					<p className={className} >
						<Spinner />
						{ __( 'Loading Data', 'secure-blocks-for-gutenberg' ) }
					</p>
				);
			}
			return [
				<InspectorControls>
					<PanelBody title={ __( 'Select Roles', 'secure-blocks-for-gutenberg' ) } className="secure-block-inspector">
						<PanelRow>
							<label htmlFor="secure-block-roles" className="secure-block-inspector__label">
								{ __( 'Secure content is presented to users that are logged-in and in the following roles:', 'secure-blocks-for-gutenberg' ) }
							</label>
						</PanelRow>
						<PanelRow>
							<Select
								className="secure-block-inspector__control"
								name='secure-block-roles'
								value={ selectedRoles }
								onChange={ handleRoleChange }
								options={ userRoles }
								isMulti='true'
							 />
						</PanelRow>
						<PanelRow>
							<em className="muted">{ __( 'No selected roles mean that secure content will be presented to all logged-in users.', 'secure-blocks-for-gutenberg' ) }</em>
						</PanelRow>
					</PanelBody>
				</InspectorControls>,
				<div className={ classnames( props.className ) }>
					<header className={ classnames( props.className ) + '__handle' }>
						<span className={ classnames( props.className ) + '__icon' }>
							{ icons.lock }
						</span>
						<span className={classnames( props.className ) + '__description'}>
							<span>{ __( 'Content shown to users that are ', 'secure-blocks-for-gutenberg' ) }</span>
							<strong>{ __( 'logged-in', 'secure-blocks-for-gutenberg' ) }</strong>
							{ selectedRoles.length === 0  ?
								<span>.</span>
							:
								<span>
									{ 1 === selectedRoles.length ?
										<span>
											{ __( ' and in the following role: ', 'secure-blocks-for-gutenberg' ) }
										</span>
									:
										<span>
											{ __( ' and in any of the following roles: ', 'secure-blocks-for-gutenberg' ) }
										</span>
									}
									<span className={classnames( props.className ) + '__roles'}>
									{ Object( selectedRoles ).map( ( value, key ) =>
										<span className="role">
											<span className="role__name">
												{ value['label'] }
											</span>
										</span>
									)}
									</span>
								</span>
							}
						</span>
					</header>
					<InnerBlocks
						template={ [
							[ 'matt-watson/secure-block-inner-secure' ],
							[ 'matt-watson/secure-block-inner-unsecure' ],
						] }
						templateLock="all"
						allowedBlocksExample={ [
							[ 'matt-watson/secure-block-inner-secure' ],
							[ 'matt-watson/secure-block-inner-unsecure' ],
						] }
						/>
					<footer className={ classnames( props.className ) + '__footer' }>
						{ __( 'End: Secure Blocks', 'secure-blocks-for-gutenberg' ) }
					</footer>
				</div>
			];
		} ),
		save: props => {
			return (
				<div>
					<InnerBlocks.Content />
				</div>
			);
		},
	},
);

That wasn’t so hard. Lets see what the final component looks like.

Restricting content by user role
Restricting content by user role

There we go, a dynamically REST API populated dropdown InspectorControl in the Secure Blocks for Gutenberg plugin.

Posted by Matt Watson

Matt Watson is co-founder of the WordPress agency Make Do. Matt loves learning about personal, professional and web development. Learn more about Matt.

One Reply to “Working with Gutenberg and the WordPress REST API”

  1. Brilliant! Thanks for the share. I’m building a block which requires me to pull a list of all top level pages. Inspector Control will be hosting a dropdown for me. I can easily achieve this with an ajax call. But I think a better solution would be to use withSelect. There are couple of examples out there which isn’t sufficient enough. But your post really helps understanding the concept and application of withSelect feature.

    Reply

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.