After completing Zac Gordon’s wonderful Gutenberg Development Course, I wanted to try building my own block. It was more difficult than I expected, but a great learning experience. For comparison, see my article on building custom blocks with Advanced Custom Fields.
I added a “List Subpages” block to my BE Subpages Widget plugin (code here). This lets you select a page and dynamically lists all its subpages.
The biggest hurdle is my lack of knowledge of modern JavaScript development. I’m a theme and plugin developer, so I’m comfortable with PHP and frontend markup. I use a bit of jQuery in theme development, but nothing special.
There was a lot of copy/pasting pieces from existing blocks while I tried to figure out the proper syntax to implement what I want. I’m sure this code could be written better if I knew what I was doing.
Getting Started
I followed the example code from the Gutenberg Development Course to setup the plugin initially. You could also use the create-guten-block tool or the wp cli command wp scaffold block.
In terminal, run npm install
in the plugin’s directory. Then run npm run dev
, which will monitor the files for changes and recompile the resulting CSS/JS into the assets directory.
In the main plugin file I’m enqueuing the editor-specific Javascript using the enqueue_block_editor_assets
hook. If my block required its own styling or JS, I’d also enqueue block-specific CSS and JS using enqueue_block_assets
hook, which applies to both the editor and frontend, ensuring the block looks the same in both places.
Starting the block
In the block’s index.js file, use registerBlockType()
to create a block (example). You specify a unique name, some basic parameters, and two functions: edit and save.
The edit function is what’s rendered in the Gutenberg editor. When the block is in focus, I’m displaying a dropdown menu, and when the block is out of focus I’m displaying the actual subpages list.
The save function is saved into the post content and is shown on the frontend. Since my block is outputting dynamic content, my save function is empty and I created a PHP callback function to handle the output, similar to a shortcode.
This is one complaint I have about Gutenberg. All dynamic elements need to be implemented twice: once in JS for the edit()
function so it’s visible in editor, and again in PHP for frontend render. You’ll need to be careful to keep your WP_Query
returning the same results as your JavaScript REST API Query.
Gutenberg is readable and reusable
Gutenberg’s code is very clean and logical. Everything is broken down into small, single-purpose files. Reusable components are accessible from global variables.
In my plugin I wanted a dropdown menu with a hierarchical page listing. I found an existing area of Gutenberg that already did this – the “page parent” dropdown in the Page Attributes section.
In the parent.js file in /packages/editor/src/components/page-attributes, they’re using <TreeSelect>
to create this hierarchical select menu (see here). At the top of the file you’ll see they are importing that component from another file:
import { TreeSelect } from '@wordpress/components';
You can’t copy this line directly, but you can use something similar:
const { TreeSelect } = wp.components;
I used this approach to find all the pieces I needed and import them all. I also kept my code simple and readable by breaking my functionality into separate files.
Querying Posts
You can use withAPIData()
to query for data using the REST API. The examples I found in Gutenberg used a constant like applyWithAPIData
to build the query, then applies it to your component on export. You can then access the JSON result of that query by using whatever variable you returned in the constant (in this case, pages
).
See the REST API Handbook for more information on querying.
Lots to learn
It truly is time to “Learn Javascript Deeply” to keep up with future WordPress development. To that end, I’m:
- Reading Eloquent Javascript
- Taking Wes Bos’ courses Javascript30 (free), ES6 for Everyone, and React for Beginners.
- Keeping up with Gutenberg development. I’m reading all the issues and pull requests to follow what’s changing and improve my understanding of the underlying code.
- Contribute where I can. Here’s my first bug report and patch.
- While building client sites, find elements to build as Gutenberg blocks in my free time. Implementing these real-world examples will help prepare me for when I’m actually building client sites with Gutenberg.
We’re still many months away from Gutenberg merging into core. But if you’re like me, it’s going to take that long to become technically competent in modern JavaScript and comfortable building websites using Gutenberg.
Now is the perfect time to start learning.
Marjorie Ray says
Great post! Thanks so much.
robert says
Hi bill thanks for your article.
I just have one question
I wonder how stringify works with array. I mean when I have
const query = stringify( {
context: ‘edit’,
per_page: 100,
orderby: ‘menu_order’,
order: ‘asc’,
_fields: [ ‘id’, ‘parent’, ‘title’ ],
} );
I get context=edit&per_page=100&orderby=menu_order&order=asc but then how do it work with _fields: [ ‘id’, ‘parent’, ‘title’ ] ?
Bill Erickson says
I
console.log
‘d the resulting string and it is: “/wp/v2/pages?per_page=100&parent=80&orderby=menu_order&order=asc&_fields=id%2Cparent%2Ctitle”It looks like the array items are simply separated with commas (%2C = comma).
Ahmad Awais says
Yay! Thanks for trying and mentioning create-guten-block! 🙌 — keep up the #GutenExperiments
Hendrik says
Thank you for this interesting tutorial.
Question: How do you get the rest of the sub pages if there are more than 100?
Bill Erickson says
For the purposes of this example I assumed I would never want to list more than 100 pages.
Hendrik says
Looking since two weeks now, all similar block examples I can find only list maximum 100 results.
Do you know how to change const pages = get( this.props.pages, ‘data’, [] ); to get the number of real results and also the remaining results?
Thank you if you can enhance your tutorial. I like your writing, it is much clearer and understandable as other tutorial.
Bill Erickson says
You would likely need to write a custom REST API endpoint to get that data. The current endpoints only provide the posts.
Here’s an example of requesting the most recent post on my site (just the id and title): https://www.billerickson.net/wp-json/wp/v2/posts?per_page=1&_fields=id,title
Hendrik says
Thank you, but I am looking for posts 1-100 and 101-200 together in one result variable. Will keep looking..
Bill Erickson says
Again, you’ll need to write your own custom REST API endpoint. The built-in endpoints are limited to 100 results. This is what happens if you request 200: https://www.billerickson.net/wp-json/wp/v2/pages?per_page=200&_fields=id%2Ctitle
Hendrik says
Thank you. There is a parameter ‘page’ as well, maybe the request can be called more times after each other but I dont know how to put this in the get() function and how to find how many pages.
There must be a way, so many use cases for blocks have more than 100 results and writing own endpoint for every block sounds overkill.
Bill Erickson says
You could just run two API queries, and on the second one add
&offset=100
. That will get you posts 101 – 200.As I mentioned in the post, I’m not very experienced with JavaScript so I’m not sure the best way to write that. You could probably duplicate the whole `applyWithAPIData` constant (ex: `applyWithAPIData2`), return a different property (ex: `pages_2`), then in the render function combine the two arrays. But I’m sure there’s a better way.
Anh Tran says
Hey Bill, I’ve completed the same course from Zac Gordon and very exciting to work on Gutenberg for my plugins and themes. Thanks a lot for sharing your experience. It’s really inspiring.
theo says
Hi Bill
Thanks for your article.
I have one question about: const pageItems = get( pages, ‘data’, [] );
What are you trying to do here? What is ‘data’ and the empty array
Bill Erickson says
At the bottom,
applyWithAPIData
builds the WP REST API query and returns it aspages
(see here).We then pass that API query url to
SelectSubpages
here.So inside
SelectSubpages
, when I callget( pages )
, I’m doing the actual API query using the “pages” query URL generated byapplyWithAPIData
.As for the ‘data’ and empty array part, I don’t recall exactly. I think I copied the general structure from an existing block in Gutenberg and it probably had that (example). I’m guessing the second parameter says what we want (give us the data), and the third parameter is a default value to use if it’s empty or an error.
Alan Fuller says
I have just spent the morning catching up with Gutenberg, initially just to check compatibility with a custom plugin I’m building for a site.
I have to admit I’m struggling, I thought things would get easier, but I’m glad you also found it more difficult than expected. I thought it would be a simple process to just replace a metabox on a CPT with a block of structured data (name, address, phone, email) and then use that data in the same way as post_meta.
For the moment, I’m dropping back and reverting to traditional meta to get the job done, a sits going to take a while for this to sink in. It is a significant change of approach. I guess many devs are in the same boat.
Bill Erickson says
I agree, it’s a lot more difficult than I had expected, especially since everything is JavaScript based.
On a recent interview with Matt Mullenweg about Gutenberg, Matt was asked if (a) he thought creating a block was difficult, and (b) if there would be a PHP-only approach for block creation in the future. He answered “No, it isn’t hard to make a block” and just left it there.
I believe ACF will provide a way to create block-based metaboxes, and maybe through that process they’ll create a whole “block builder” so you don’t actually need to hand code blocks yourself.
For now, I’m adding Gutenberg support in my themes for simple pages that just use the main content editor. I’m still using metaboxes for complex pages. If a need arises where I build a shortcode, I’ll probably also build a Gutenblock for that as well.
It will probably be 6-12 months before I can consider building everything in the Gutenberg editor. Gutenberg itself needs to mature, and then the WordPress developer community needs to define best practices for building new blocks.
Phil says
I’m also experiencing the same steep learning curve as I dive into Gutenberg today. It has been a great relief to find this blog post and comments from people with with a similar experiences. I was starting to wonder if I’m just not that smart!
Jordan Smith says
So do you think this will replace the concept of the ACF Flex Content field? I learned about this from you and have an entire Genesis page builder theme based around the concept. Will you be replacing your Flex Content setup with Gutenberg blocks?
Bill Erickson says
At some point in time, yes. As mentioned in the above comment, I think ACF or someone similar will simplify how to build a block. But for the foreseeable future, I’ll be using post-meta based flexible content pages (I call them disabling the editor on those specific templates.
Drew Jaynes says
Hey Bill, great post!
I found your post and code pretty enlightening – especially not yet having dug in too much to Gutenberg. I have to say that while I applaud the Gutenberg team for already having a handbook in progress, it leaves a lot to be desired for what I would consider to be the dominant WordPress develop demographic: strong basis in PHP and just learning modern JS via React/ES6 etc.
There’s kind of an assumption and tone in the handbook that you are already familiar with React/Gutenberg terminology. To really be an effective resource, I think they’re going to have to do a better job of breaking it down.
In terms of some of the limitations you highlighted, I too am a little concerned about how Gutenberg is reshaping the “WordPress way”. In theory it might seem like a good idea to require creating the equivalent of a render preview in JSX, but in practice I find it to be shortsighted and annoying. I hope they come up with a more efficient, intuitive way to address that problem.
One would think that it should be possible to simply defer to the PHP-defined rendering callback instead of having to reinvent the PHP wheel in JSX for the benefit of rendering the edit preview. Good stuff, thanks for the post 🙂
Bill Erickson says
Thank you for the kind comment!
I agree, at a minimum you should be able to register a PHP callback for the preview. If core doesn’t add it, I bet someone will build it and plugin developers will bundle it.