In this chapter, I’ll cover how to add a few Gutenberg blocks to the Voting Tally plugin we’ve been creating over the last few chapters.
You’re welcome to follow along at wpajax.pro/gutenberg.
This chapter will take a nose-dive into Node, React, and command line. If you are unfamiliar with these terms, you can skip this chapter and work your way into the examples.
The first step is installing Node and NPM. From there, follow the installation instructions. When you’re all set, make sure you have Node and NPM installed by typing node -v
and npm -v
. You should receive version numbers if properly installed.
node -v v11.15.0
npm -v 6.7.0
Let’s navigate to your plugin via command line. In my case, since I’m using MAMP, I’ll run the following:
cd /applications/mamp/htdocs/yourinstall/wp-content/plugins/votingtally_gutenberg
If you’re using a different local setup, your command will be different. For example, if using a popular tool like Local by Flywheel, you’d run the following:
cd ~/"Local Sites"/test-site/app/public/wp-content/plugins/votingtally_gutenberg
Inside the directory via command line, type npm init
. It’ll allow you to customize your project. In our case, we’ll be using Voting Tally as the package name.
npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (votingtally_gutenberg)
And you can fill out the details like I have below.
Press ^C at any time to quit. package name: (votingtally_gutenberg) votingtally version: (1.0.0) description: A WordPress plugin that allows voting on posts. entry point: (index.js) test command: git repository: (https://github.com/wpajax/votingtally_gutenberg.git) keywords: vote, votingtally author: Ronald Huereca license: (ISC) GPL-3.0-or-later About to write to /Applications/MAMP/htdocs/beaverbuilder/wp-content/plugins/votingtally_gutenberg/package.json: { "name": "votingtally", "version": "1.0.0", "description": "A WordPress plugin that allows voting on posts.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/wpajax/votingtally_gutenberg.git" }, "keywords": [ "vote", "votingtally" ], "author": "Ronald Huereca", "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/wpajax/votingtally_gutenberg/issues" }, "homepage": "https://github.com/wpajax/votingtally_gutenberg#readme" }
If all is well, type ‘yes’ and a file called package.json
will be generated. I received a warning that my NPM is out of date, so I’ll go ahead and run npm install -g npm
.
npm install -g npm
In my case, NPM didn’t have the right permissions for updating, so I ran:
sudo npm install -g npm
That got me to the latest version.
Create Guten Block is an amazing utility for creating Gutenberg blocks. It does the hard parts for you. There’s no need to configure things like Webpack or Babel; the tool does the hard work. All you have to worry about is the JavaScript and PHP to register the blocks.
Let’s go ahead and install it by running this command in our plugin directory.
npm install cgb-scripts --save-dev
This will install Create Guten Block and create a node_modules
folder in our plugin. Now, we have to create the files and folders it expects.
Create Guten Block expects the following directories and files:
Let’s go ahead and create those directories and files.
Finally, we need to modify our package.json file and modify the scripts portion. Here’s what our package.json file looks like right now without modifying the scripts.
{ "name": "votingtally", "version": "1.0.0", "description": "A WordPress plugin that allows voting on posts.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/wpajax/votingtally_gutenberg.git" }, "keywords": [ "vote", "votingtally" ], "author": "Ronald Huereca", "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/wpajax/votingtally_gutenberg/issues" }, "homepage": "https://github.com/wpajax/votingtally_gutenberg#readme", "devDependencies": { "cgb-scripts": "^1.22.0" } }
We’ll be stripping out the scripts portion and adding this instead.
"scripts": { "start": "cgb-scripts start", "build": "cgb-scripts build", "eject": "cgb-scripts eject" },
And here’s what our final package.json file looks like:
{ "name": "votingtally", "version": "1.0.0", "description": "A WordPress plugin that allows voting on posts.", "main": "index.js", "scripts": { "start": "cgb-scripts start", "build": "cgb-scripts build", "eject": "cgb-scripts eject" }, "repository": { "type": "git", "url": "git+https://github.com/wpajax/votingtally_gutenberg.git" }, "keywords": [ "vote", "votingtally" ], "author": "Ronald Huereca", "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/wpajax/votingtally_gutenberg/issues" }, "homepage": "https://github.com/wpajax/votingtally_gutenberg#readme", "devDependencies": { "cgb-scripts": "^1.22.0" } }
From there we can run npm run start
and see the following:
This will create a dist
folder in your plugin install. Right now the only thing in the dist folder is a file called blocks.build.js
. It’s not picking up our stylesheets quite yet.
Inside src/blocks.js
, let’s add the following:
import './block/style.scss'; import './block/editor.scss';
With npm run start
still running, it should now create the following in the dist
folder.
Now that we have Create Guten Block working, let’s begin adding a block to replace (supplement?) one of our shortcodes.
Let’s begin by creating a popular-posts.js
JavaScript file inside the src/block
folder. Inside the src/blocks.js
file, we’ll add an import statement to pull in our new JavaScript.
import './block/style.scss'; import './block/editor.scss'; import './block/popular-posts';
Let’s create a popular-posts-edit.js
file inside the src/block
folder. We’ll be using popular-posts-edit.js
to do the bulk of our block logic and use popular-posts.js
to initialize the block. Let’s begin with initializing the block.
const { __ } = wp.i18n; // Import __() from wp.i18n const { registerBlockType } = wp.blocks; // Import registerBlockType() from wp.blocks import edit from './popular-posts-edit'; registerBlockType( 'votingtally/popular-posts', { title: __( 'Popular Posts', 'votingtally' ), // Block title. icon: <svg width="72" height="72" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" fill="none" rx="0" ry="0"></rect><path fill-rule="evenodd" clip-rule="evenodd" d="M9 2C7.89543 2 7 2.89543 7 4V5H6C4.89543 5 4 5.89543 4 7V20C4 21.1046 4.89543 22 6 22H15C16.1046 22 17 21.1046 17 20V19H18C19.1046 19 20 18.1046 20 17V4C20 2.89543 19.1046 2 18 2H9ZM17 7V17.8H17.8C18.3523 17.8 18.8 17.3523 18.8 16.8V4.2C18.8 3.64772 18.3523 3.2 17.8 3.2H9.2C8.64772 3.2 8.2 3.64771 8.2 4.2V5H15C16.1046 5 17 5.89543 17 7ZM6.2 6.2C5.64772 6.2 5.2 6.64771 5.2 7.2V19.8C5.2 20.3523 5.64771 20.8 6.2 20.8H14.8C15.3523 20.8 15.8 20.3523 15.8 19.8V7.2C15.8 6.64772 15.3523 6.2 14.8 6.2H6.2Z" fill="#ffffff"></path></svg>, category: 'widgets', keywords: [ __( 'popular', 'votingtally' ), __( 'posts', 'votingtally' ), __( 'voting', 'votingtally' ), ], supports: { align: true }, edit: edit, save() { return null } } );
Let’s start with what we import.
__
from wp.i18n
in order to provide translatable strings.registerBlockType
is imported from wp.blocks
. We use this to register our block.edit
is a variable that is assigned our file popular-posts-edit.js
file. We’ll use this in place of putting code inside the edit
option for cleaner separation.And the contents of registerBlockType
:
votingtally
and what the block will be called (in this case, popular-posts
).Popular Posts
.edit
object our edit
variable (the output of popular-posts-edit.js
).save
to null
, which means we’ll be using PHP to output the contents of the block.Now that our block is initialized via JavaScript, we also need to initialize it in PHP.
Let’s create a blocks
folder in our plugin. Inside this blocks folder, create a file called class-popular-posts.php
.
<?php /** * Initialize and output the Popular Posts block. * * @package votingtally */ namespace VotingTally\Blocks; /** * Class Popular_Posts */ class Popular_Posts { }
We’ll use the init
action to point to our block register function, pass an attribute or two, and provide a render_callback
callback function for our front-end output.
Here’s a barebones example of our class.
<?php /** * Initialize and output the Popular Posts block. * * @package votingtally */ namespace VotingTally\Blocks; /** * Class Popular_Posts */ class Popular_Posts { /** * Class Constructor */ public function __construct() { add_action( 'init', array( $this, 'register_block' ) ); } /** * Register the Popular Posts Block in PHP. */ public function register_block() { if ( ! function_exists( 'register_block_type' ) ) { return; } register_block_type( 'votingtally/popular-posts', array( 'attributes' => array( 'align' => array( 'type' => 'string', 'default' => 'center', ), ), 'render_callback' => array( $this, 'frontend' ), ) ); } /** * Front-end output for our block. * * @param array $attributes Attributes passed from Gutenberg. */ public function frontend( $attributes ) { } }
We utilize a WordPress function called register_block_type
, which takes the block name and an array of options. Attributes are initialized, which will be passed to the Gutenberg block. A render_callback
argument is used to point to our frontend output.
What’s next is enqueuing our JavaScript and CSS files.
Let’s create a new class inside our blocks folder. We’re going to use the unoriginal name of class-enqueue.php
.
This will be used as our base code.
<?php /** * Initialize and output the Popular Posts block. * * @package votingtally */ namespace VotingTally\Blocks; /** * Class Popular_Posts */ class Enqueue { /** * Class Constructor. */ public function __construct() { } }
We’ll use the action enqueue_block_editor_assets
to register our scripts and styles. Registering is necessary so we can tell WordPress which scripts and styles belong to our block.
class Enqueue { /** * Class Constructor. */ public function __construct() { add_action( 'enqueue_block_editor_assets', array( $this, 'block_scripts_and_styles' ) ); } /** * Enqueue block assets. */ public function block_scripts_and_styles() { // Register block scripts and set translations. wp_register_script( 'votingtally_block', VOTINGTALLY_URL . 'dist/blocks.build.js', array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ), VOTINGTALLY_VERSION, true ); wp_set_script_translations( 'votingtally_block', 'votingtally' ); // Register our block stylesheet. wp_register_style( 'votingtally_block_css', VOTINGTALLY_URL . 'dist/blocks.editor.build.css', array(), VOTINGTALLY_VERSION, 'all' ); } }
The handles are important here: votingtally_block
for the JavaScript file and votingtally_block_css
for the CSS file.
We’re now going to travel back to our block PHP file, which is the blocks folder. It’s named class-popular-posts.php
in case you’ve already forgotten (I don’t blame you). Let’s use two arguments to attach the JavaScript and CSS files we registered in the previous section.
/** * Register the Popular Posts Block in PHP. */ public function register_block() { if ( ! function_exists( 'register_block_type' ) ) { return; } register_block_type( 'votingtally/popular-posts', array( 'attributes' => array( 'align' => array( 'type' => 'string', 'default' => 'center', ), ), 'editor_script' => 'votingtally_block', 'editor_style' => 'votingtally_block_css', 'render_callback' => array( $this, 'frontend' ), ) ); }
Finally, we need to fill out our file popular posts edit file. It’s located in /src/block/popular-posts-edit.js
.
WordPress does a lot of the heavy lifting when it comes to React. All we really need to do is import the components and helpers we need, and render our output.
const { Component, Fragment } = wp.element; const { __, _x } = wp.i18n; const { PanelBody, Placeholder, Toolbar, Spinner, } = wp.components; class Popular_Posts extends Component { constructor() { super( ...arguments ); } render() { const { setAttributes } = this.props; const { align } = this.props.attributes; return ( <Fragment> <Placeholder> <div> {__( 'Popular Posts', 'votingtally' )} </div> </Placeholder> </Fragment> ) } } export default Popular_Posts;
Finally, we should be able to see our block in the block editor.
Right now, essentially, we have just a simple “Hello World” block. I know, this journey has been tedious just for one block that does almost nothing yet, but we’ll get to the Ajax portion soon.
We’ll need to journey back to the command line and import a library called axios. We’ll install it via NPM. It’ll help us make the Ajax call to retrieve the popular posts.
npm install axios --save-dev
Once that’s done, we’ll need to import it into our block.
import axios from "axios"; const { Component, Fragment } = wp.element; const { __, _x } = wp.i18n;
Let’s set a state in our block for loading, which will display a spinner.
import axios from "axios"; const { Component, Fragment } = wp.element; const { __, _x } = wp.i18n; const { PanelBody, Placeholder, Toolbar, Spinner, } = wp.components; class Popular_Posts extends Component { constructor() { super( ...arguments ); this.state = { loading: true, }; } render() { const { setAttributes } = this.props; const { align } = this.props.attributes; return ( <Fragment> { this.state.loading && <Fragment> <Placeholder icon="admin-post" label={ __( 'Popular Posts', 'votingtally' ) } > <Spinner /> </Placeholder> </Fragment> } </Fragment> ) } } export default Popular_Posts;
Next, we’ll need something to load in the popular posts. This is where axios
comes into play.
We’ll use the componentDidMount
React lifecycle method to gather the popular posts via our REST API endpoint that we created in the previous chapter. It’ll use axios
to make our Ajax request to the REST endpoint.
componentDidMount = () => { axios.get( votingtally_admin.rest_url + `votingtally/v1/get_posts/post/10/DESC`, { 'headers': { 'X-WP-Nonce': votingtally_admin.nonce } } ).then( ( response ) => { console.log( response ); } ); }
We’re accessing two variables that do not exist. We’ll have to go back to our class-enqueue.php
in the blocks directory and add the variables we need. We’ll be making use of wp_localize_script
.
public function block_scripts_and_styles() { // Register block scripts and set translations. wp_register_script( 'votingtally_block', VOTINGTALLY_URL . 'dist/blocks.build.js', array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ), VOTINGTALLY_VERSION, true ); wp_localize_script( 'votingtally_block', 'votingtally_admin', array( 'rest_url' => rest_url(), 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); wp_set_script_translations( 'votingtally_block', 'votingtally' ); // Register our block stylesheet. wp_register_style( 'votingtally_block_css', VOTINGTALLY_URL . 'dist/blocks.editor.build.css', array(), VOTINGTALLY_VERSION, 'all' ); }
Let’s output the contents of the block using our componentDidMount
React method.
componentDidMount = () => { axios.get( votingtally_admin.rest_url + `votingtally/v1/get_posts/post/10/DESC`, { 'headers': { 'X-WP-Nonce': votingtally_admin.nonce } } ).then( ( response ) => { this.setState( { loading: false, html: this.getListHtml( response.data ), } ) } ); }
We set the state loading to false. And HTML is returned courtesy of a helper function called getListHtml
.
getListHtml = (data) => { const listItems = data.map( ( item ) => <li key={item.id}> {item.title} </li> ); return ( <ul>{listItems}</ul> ); }
We now need to tell our render
method to output the HTML.
render() { const { setAttributes } = this.props; const { align } = this.props.attributes; return ( <Fragment> { this.state.loading && <Fragment> <Placeholder icon="admin-post" label={ __( 'Popular Posts', 'votingtally' ) } > <Spinner /> </Placeholder> </Fragment> } { ! this.state.loading && <Fragment> {this.state.html} </Fragment> } </Fragment> ) }
What’s left is outputting the contents of the block on the front-end. Let’s just simply echo out the shortcode content for the popular posts.
We’ll output the content in class-popular-posts
located in the blocks
directory.
/** * Front-end output for our block. * * @param array $attributes Attributes passed from Gutenberg. */ public function frontend( $attributes ) { return wp_kses_post( do_shortcode( '[votingtally]' ) ); }
That’s it! We’re done!
Obviously we want our shortcode to run faster, so we’d modify it to just output the posts instead of requiring an HTTP request to do the hard work.
We’re also not taking into account other post types. Ideally, we’d modify our block to have a list of post types to display instead of just posts. Likewise, we’d need to add an option for the number of posts to retrieve and the order. Lastly, we’re not outputting any styles. Some basic styling can be added to our popular posts block both inside and outside the editor.
Feel free to fork and modify the code at wpajax.pro/gutenberg.
In this chapter, we painfully slogged through how to create a custom Gutenberg block to output our popular posts. There are a bunch of refinements we can make, but I’ll leave that as an exercise for the reader.
Up next, I will show through several examples of how to use Ajax to get the results you need and make for a positive user experience.