Creating an advanced block for Gutenberg

In April, WordPress 5.0 will be launching. With this comes the new and improved editor, Gutenberg.

Share

The new editor experience is based on blocks, making a modular site possible without the need of plugins.

Built with React, Gutenberg gives the user instant visual feedback. When integrated, you can make the editor look closer to the front-end of the site reducing the need for the preview feature.

There are already some helpful articles and resources available about the basics of using Gutenberg. Ahmad Awaid wrote about creating your first block with his boilerplate and additionally wrote the NPM package create-guten-block which we’ll use in this tutorial.

Although there are helpful resources out there, I am yet to find anything which goes into detail around inbuilt, and more complex, components. This article should fill this gap and provide a useful companion to the Gutenberg content that is already available.

Let’s get started on making our block; a post selector that renders out a list of articles and includes support for search and custom post types.

Table of Contents

Getting set up

Firstly, if you haven’t already, we need to install create guten block. You can install it via npm by running:

npm install create-guten-block --global

Once installed navigate to your WordPress plugins directory via terminal and run:

create-guten-block guten-post-list

This will take a few minutes to install the dependencies, once complete it will create a folder called guten-post-list this is where our new block will live.

cd into the newly created folder and start the watch script by running npm start in the terminal.

Now we are set up its time to open this up in our favourite editor and start working on building these blocks.

To the editor!

First let’s look at our directory structure and see whats there:

├── dist
│   ├── blocks.build.js
│   ├── blocks.editor.build.css
│   └── blocks.style.build.css
├── src
│   ├── block
│   │   ├── block.js
│   │   ├── editor.scss
│   │   └── style.scss
│   ├── blocks.js
│   ├── common.scss
│   └── init.php
├── README.md
├── package-lock.json
├── package.json
└── plugin.php

As a quick run down, dist is where all of our compiled files go and we shouldn’t have to make any changes here. Now the src folder, is where we initialise the block. For now our main focus will be the block folder within, this is where we keep all of the styles/scripts for the block itself.

Open src/block/block.js this is where our block is registered, as you can see its full of comments saying what everything does. The first step is to modify the block type title, name and keywords.

First the name; on line 28 of block.js you will see the registerBlockType function, the first parameter is the block name; you should see something similar to cgb/block-guten-post-list. Block names must be a string that contains a namespace prefix, for example I will change this to bigbite/postlist but you can change this to what you require. One thing to note, when you change the block name, the block class changes too; so what was originally .wp-block-cgb-block-guten-post-list is now .wp-block-bigbite-postlist. This will need changing in src/block/style.scss and src/block/editor.scss

Now for actually building the block, in block.js we need to remove the edit function, we are going to replace this with a component. To do this we first need to pull out Component from wp.element, so at the top of the file add:

const { Component } = wp.element;

Back to the edit function, we will define our class like so:

// ...
edit: class extends Component {
  constructor(props) {
      super(...arguments);
      this.props = props;
  }

  render() {
        const { className } = this.props;

      return (
          <div className={className}>
              Our Postlist Block
          </div>
      );
  }
},
// ...

In preperation for what is needed I have created a wireframe on how I want it to look:

Block Wireframe

From the wire frame we have a two base components, Block Title and Post Selection, from there we have a few sub components.

Component Hierarchy

├── Block Title
├── Post Selection
│   ├── PostList
│   │   └── Post
│   ├── PostType Select
│   └── Search

Creating the Title

To create our block title we are going to utilise an inbuilt component provided by the Gutenberg Plugin, the PlainText component. Before we can use the component, we have to include the PlainText component. We need to add this to the top of the block.js by editing line:

const { registerBlockType } = wp.blocks;

To be:

const { registerBlockType, PlainText } = wp.blocks;

View on GitHub

Editable requires 2 properties, value, and onChange. For now we can set the value to '' and set the onChange to () => {} just so it doesn’t error.

So what we have so far in our render() function:

return (
  <div className={className}>
        <PlainText
            value={''}
            onChange={ () => {} }
        />
  </div>
);

If we add our block in the panel we should now have the PlainText component in there. For stylistic purposes, I am going to wrap the editable component in a div with a class of title-wrapper. Rather than go through styles in this post, they are available on the Github repository here.

If you enter anything into the component and hit save, then refresh, you will notice it doesn’t save, this is because we never told it how to save. To allow this to happen we first need to declare an attribute; first we create a new declaration inside the registerBlockType arguments, and above the edit class add this:

// ...
attributes: {
    blockTitle: {
        type: 'string'
    }
}
// ...

View on GitHub

This registers your attribute, the attributes can have a few more options, such as a default value, but for the title this is all we need.

Now we need to write a function that handles the onChange event that’s dispatched by PlainText. All we need this function to do is call setAttributes, a function that is passed in as a property. So in the edit component create a function call onTitleChange, or something similar, this function receives one argument which is the value. In our function we want to call setAttributes, this function takes in one parameter which is an object with the attributes as an object. The finished function will look something like this:

onTitleChange(blockTitle) {
    this.props.setAttributes({ blockTitle });
}

View on GitHub

Now we want to bind the event to this within the constructor function to stop us having any context issues. So in our constructor we need to add this line:

this.onTitleChange = this.onTitleChange.bind(this);

View on GitHub

Now thats done it’s time to attach the event to the component. This is can be achieved like so:

// ...
onChange={this.onTitleChange}
// ...

View on GitHub

We then need to apply the value of the blockTitle attribute to the component. Attributes are passed down in the props too, so to grab the block title we can use es6. So in the render function change this line:

const { className } = this.props;

To:

const { className, attributes: { blockTitle = '' } = {} } = this.props;

View on GitHub

This may seem a little confusing but, this is multi level destructuring and setting defaults to prevent errors.

To explain we are taxing the attributes object from this.props then setting the default to {} then inside we are grabbing blockTitle and setting its default to an empty string. This means even if attributes isn’t passed down, it will always have a value.

We can now set the value of the component by changing the value property to:

// ...
value={blockTitle}
// ...

View on GitHub

So let’s test this by creating a new post and adding our block, once we do that we can add some text to our newly created title field, update the post and refresh the page. If the text avaulable on refresh, then everything is working correctly.

Building the Post selection component

The time has come to start building our component, the real meat and potatoes of our block, the post selector. This is where most of our components will live and where our main application state will be.

The first step is to create a new folder to store our components, in src/block create a folder called components. Within this folder create a file called PostSelector.js

At the top we need to import WordPress’s Component class:

const { Component } = wp.element;

View on GitHub

Now let’s create our class, we are going to be exporting it straight from the file rather than exporting it as the default.

So let’s create our class:

export class PostSelector extends Component {
    constructor(props) {
        super(...arguments);
        this.props = props;
    }

    render() {
        return (
            <p>PostSelector Block.</p>
        );
    }
}

At the top of the block.js file we should include the component:

import { PostSelector } from './components/PostSelector';

View on GitHub

Underneath the plain text component we should include our block:

// ...
<PostSelector />
// ...

View on GitHub

It’s time to start building up our post selector. First we should set up our markup, and once this is done it will give us an idea on what state we will need and what we need to split into components.

In our render function return:

<div className="post-selector">
    <div className="post-selectorHeader">
        <div className="searchbox">
            <label htmlFor="searchinput">
                <input id="searchinput" type="search" placeholder={"Please enter your search query..."} />
            </label>
        </div>
        <div className="filter">
            <label htmlFor="options">Post Type: </label>
            <select name="options" id="options">
                <option>Loading Post types</option>
            </select>
        </div>
    </div>
    <div className="post-selectorContainer">
        <div className="post-list">
            <article className="post">
                <figure className="post-figure" style={{ backgroundImage: `//satyr.io/150` }}>
                </figure>
                <div className="post-body">
                    <h3 className="post-title">Post 1</h3>
                </div>
                <button>+/-</button>
            </article>
        </div>
        <div className="post-list">
            <article className="post">
                <figure className="post-figure" style={{ backgroundImage: `//satyr.io/150` }}>
                </figure>
                <div className="post-body">
                    <h3 className="post-title">Post 2</h3>
                </div>
                <button>+/-</button>
            </article>
        </div>
    </div>
</div>

From here we should rip out each post list into a component, which will contain the post component. So let’s create two new files in the components directory, Post.js and PostList.js.

Start with creating the PostList component, for this we won’t be using a class as the post list won’t be using its own state, that will be handled by the PostSelector component. In our PostList we are going to export an arrow function like so:

export const PostList = props => {
    return (
        <div className="post-list">
            <article className="post">
                <figure className="post-figure" style={{ backgroundImage: `//satyr.io/150` }}>
                </figure>
                <div className="post-body">
                    <h3 className="post-title">Post 2</h3>
                </div>
                <button>+/-</button>
            </article>
        </div>
    );
}

View on GitHub

In our PostSelector we need to import the component:

import { PostList } from './PostList'; 

View on GitHub

We can now replace the two divs with a class of post-list with the Post List components. The post-selectorContainer will now look like this:

<div className="post-selectorContainer">
    <PostList />
    <PostList />
</div>

We need to think of what properties the post list is going to take. The first property will be posts which is an array of objects containing post data. With loading we want to be able to display a message whilst the posts are loading. filtering will be handy if we are searching, we can show different messages dependant on what is happening.

On the post list we will be passing down an action property which will either call addPostEvent or removePostEvent, dependant on whether it is the selected list or just the selection list.

Let’s start declaring the initial state on the PostSelector, so we can pass down the properties. In the constructor method we will complete:

this.state = {
};

Initially, we need to declare, posts, loading, type and types. This will get more complex as we start to build the post selector out but is suffice for this example.

this.state = {
    posts: [],
    loading: false,
    type: 'post',
    types: [],
};

View on GitHub

Let’s start passing data down to the PostList:

{/* Post List (Selection List) */}
<PostList 
    posts={this.state.posts}
    loading={this.state.loading}
/>

{/* Post List (Selected List) */}
<PostList 
    posts={[]}   /* Will change this to be the generated list. */
    loading={this.state.loading}
/>

View on GitHub

Nothing has changed so far, we first need to fetch the post types, then the posts, and handle it in the PostList. We now need to start handling the API; and the following needs to happen:

  1. On component mount fetch the post types
  2. Once complete fetch selected posts
  3. Then fetch the latest posts

For now we will skip step 2 as we haven’t implemented the selected posts feature. Rather than keeping all of our API logic in the PostSelector component, we will pull it out into its own file. Make a new directory called utils in the block folder, this is where all of our utility functions will be stored.

Within there create a file called api.js. Instead of using the fetch API we will be using axios for our API calls. Now cancel our build task and run this in terminal:

npm install axios --save

Once that is installed let’s start our build task again:

npm run start

Going back to api.js, at the top of our file, we will import axios like so:

import axios from 'axios';

View on GitHub

To grab out post types we need to fire a GET request to /wp-json/wp/v2/types.

In our API file let’s export that call:

export const getPostTypes = () => axios.get('/wp-json/wp/v2/types');

View on GitHub

The above function returns a promise which we will handle within the component. Heading back to our PostSelector component, we need to create the componentDidMount function and import the API functions.

The API functions at the top of the file let’s import all of our functions from the API file:

import * as api from '../utils/api';

View on GitHub

In our PostSelector class create the componentDidMount function that will call the get post types and set loading to true:

componentDidMount() {
    this.setState({ loading: true });

    api.getPostTypes()
        .then(({ data = {} } = {}) => {
            this.setState({
                types: data
            })
        });
}

View on GitHub

As you can see I used object destructuring to pull the data property out directly; before we set the state. However, we need to remove two types from the list, wp_block and attachment. We can do this by using delete

// just after the then
delete data.attachment;
delete data.wp_block;
// before the set state

View on GitHub

We now have our post types 💪! Let’s display them on our component within the select element:

// select
{ this.state.types.length < 1 ? (<option value="">loading</option>) : Object.keys(this.state.types).map(key => <option key={key} value={key}>{this.state.types[key].name}</option>) }
// end select

View on GitHub

Now we have got the post types we can now fetch the posts, in our api.js file create a new function:

export const getPosts = () => {
}

Our function will take a single argument, an object, that will be destructured to grab the rest base and then collect the remain argumments:

export const getPosts = ({ restBase = false, ...args }) => {

Now it’s time to build the query string from the arguments, we can do it like this:

// ...
const queryString = Object.keys(args).map(arg => `${arg}=${args[arg]}`).join('&');

Then we can return the API call:

return axios.get(`/wp-json/wp/v2/${restBase}?${queryString}&_embed`);

View on GitHub

Back in the PostSelector component we are going create a wrapper for calling the API, this will handle the building of the query using state.

Let’s create our function; it will take one parameter, an object and will return a promise:

// ...
getPosts(args = {}) {
}
// ...

View on GitHub

We are going to set up some default arguments to prevent repetition, for now we will set the per page and the post type:

const defaultArgs = {
    per_page: 10,
    type: this.state.type
};

View on GitHub

We need to merge the two arguments objects together, and add the ‘rest_base’ property so our API knows where to fire:

const requestArguments = {
    ...defaultArgs,
    ...args
};

requestArguments.restBase = this.state.types[requestArguments.type].rest_base;

View on GitHub

With our arguments now ready we can call our API, set the posts state and return the promise:

return api.getPosts(requestArguments)
    .then(response => {
        this.setState({
            posts: [...this.state.posts, ...response.data],
        });

        // return response to continue the chain
        return response;
    })

View on GitHub

Great! We can now call the API to fetch posts, hopping back to our componentDidMount function we need to fetch the posts after we fetch/set the post types. Since setState accepts a callback we will do our post fetch here. Once the posts are fetched we want to set loading to false:

// ...
this.setState({
    types: data
}, () => {
    this.getPosts()
          .then(() => this.setState({ loading: false }));
});

View on GitHub Part 1 & Part 2

We now have some posts in the list and can start working on echoing out the list. The first step is to create a Post component, if you didn’t create the Post.js file in the components folder do so now.

For now, we will just grab the markup from the post list and include it.

Post.js

export const Post = (props) => (
    <article className="post">
        <figure className="post-figure" style={{ backgroundImage: `//satyr.io/150` }}>
        </figure>
        <div className="post-body">
            <h3 className="post-title">Post 2</h3>
        </div>
        <button>+/-</button>
    </article>
);

View on GitHub

In PostList.js we need to import the post component:

import { Post } from './Post';

View on GitHub

Now we are going to start handling the properties passed down such as loading, and posts. At the top of the PostList function:

const { loading = false, posts = [] } = props;

if (loading) {
    return <p>Loading posts...</p>;
} 

View on GitHub

This destructures the props object and pulls out loading and posts, after we check if the component is loading posts – if it is then we return a loading message. For the posts, let’s change the return to be:

<div class="post-list">
    {posts.map((post) => <Post key={post.id} {...post} />)}
</div>

View on GitHub

What if everything has loaded but there is still no posts? Between the loading check and final return, add:

if ( ! posts || posts.length < 1 ) {
    return <p>No posts.</p>
}

View on GitHub

We can now link up the post component with the post contents, but first let’s find out what fields are returned. We can do this by console logging the value out:

(...args) => console.log(...args) || true

This would log all of the components arguments then return the component contents. This can be implemented into our Post component, like so:

export const Post = props => console.log(props) || (
// ...

Let’s start by adding the post title to each post:

// ...
<h3 className="post-title">{props.title.rendered}</h3>
// ...

View on GitHub

For now thats all we need, we will work on implementing the featured image later. It’s time for us to start adding and removing posts from the selected posts list.

First of all we need to add the attribute to the ‘attributes’ property of the main block (block.js). We want the type to be an array and we can declare this like so:

attributes: {
    // ...
    selectedPosts: {
        type: 'array',
        default: []
    },
},

View on GitHub

We need to create a method so we can update the selectPosts attribute, this is exactly the same as the update title method:

// ...
updateSelectedPosts( selectedPosts ) { 
    this.props.setAttributes({ selectedPosts });
}
// ...

View on GitHub

In the constructor function we will bind this to the function, to prevent any context changes:

// ...
this.updateSelectedPosts = this.updateSelectedPosts.bind(this);

View on GitHub

Let’s pass that down, along with the attribute itself, to our PostSelector component:

<PostSelector
    selectedPosts={this.props.attributes.selectedPosts}
    updateSelectedPosts={this.updateSelectedPosts}
/>

View on GitHub

Now that we have attributes, and the update method made, we can start working on the add and remove functionality. In our PostSelector component we need to create two functions, addPost() and removePost() both of these accept one argument, the post id, which is an integer. So let’s declare them:

// ...
addPost(post_id) {

}

removePost(post_id) {

}
// ...

First the addPost function, this will be fairly straight forward, just spread the array into a new array and append the new post ID:

addPost(post_id) {
    this.props.updateSelectedPosts([
        ...this.props.selectedPosts,
        post_id
    ]);
}

View on GitHub

For removePost we just need to filter the array and leave out that specific post id. This can be achieved using the inbuilt array function .filter.

removePost(post_id) {
    this.props.updateSelectedPosts([
        ...this.props.selectedPosts
    ].filter(id => id !== post_id));
}

View on GitHub

We now need to bind these functions to themselves to prevent context issues, so up in the constructor function:

this.addPost = this.addPost.bind(this);
this.removePost = this.removePost.bind(this);

View on GitHub

Our events are now ready for the adding/removing of posts, but when we fire those events the result won’t become apparent right away, so we should pass the selected posts to the post list. To do this we will make a function that accepts no arguments and returns an array. Within this function we will filter through the posts array only returning ones that have their ID present in the selectedPosts array. We then sort the posts by the position in the selected posts array like so:

getSelectedPosts() {
    const { selectedPosts } = this.props;
    return this.state.posts
        .filter(({ id }) => selectedPosts.indexOf(id) !== -1)
        .sort((a, b) => {
                const aIndex = this.props.selectedPosts.indexOf(a.id);
                const bIndex = this.props.selectedPosts.indexOf(b.id);

                if (aIndex > bIndex) {
                    return 1;
                }

                if (aIndex < bIndex) {
                    return -1;
                }

                return 0;
        });
}

View on GitHub

We now have our function to grab the selected posts, let’s pass them down to the component:

Change:

posts={[]} /* Will change this to be the generated list. */

To:

posts={this.getSelectedPosts()}

View on GitHub

We need to pass addPost and removePost to their respective PostList as action so for the standard post list we want to pass addPost, and then for the selected post list we want to pass removePost. Once you’ve done that it should looks something like this:

<PostList
    posts={this.state.posts}
    loading={this.state.loading}
    action={this.addPost}
/>
<PostList
    posts={this.getSelectedPosts()}
    loading={this.state.loading}
    action={this.removePost}
/>

In our PostList Component we need to pass the action to the post component, so when we click the +/- button it adds/removes it.

Let’s grab it from, the props like so:

const { loading = false, posts = [], action = () => {} } = props;

View on GitHub

Now add it to the post:

{posts.map((post) => <Post key={post.id} {...post} clickHandler={action} />)}

View on GitHub

In the Post component we need to attach it to the click handler, I have also changed the props var to be destructured and pull out each variable to give it a cleaner look. Here is the updated code:

export const Post = ({ title: { rendered: postTitle } = {}, clickHandler, id: postId }) => (
    <article className="post">
        <figure className="post-figure" style={{ backgroundImage: `//satyr.io/150` }}>
        </figure>
        <div className="post-body">
            <h3 className="post-title">{postTitle}</h3>
        </div>
        <button onClick={() => clickHandler(postId)}>+/-</button>
    </article>
);

Posts are now showing and we can add/remove posts. Let’s get on to switching post types; for this we need a function to handle the on change (on the PostSelector component), like so:

handlePostTypeChange(event) {

}

View on GitHub

This function needs to set the state for the new post type, then call out getPosts function. Then in the in our render we will pass the posts down filtered by post type.

Let’s handle the filtered posts in the render function, change this component from:

<PostList
    posts={this.state.posts}
    loading={this.state.loading}
    action={this.addPost}
/>

To:

<PostList
    posts={this.state.posts.filter(post => post.type === this.state.type)}
    loading={this.state.loading}
    action={this.addPost}
/>

Now we will be able to see the changes immediately when switching. We now need to set state and do the API call. In our handlePostTypeChange function we are going to destructure the parameter to just grab the value:

handlePostTypeChange({ target: { value:type = '' } = {} } = {})

Inside the function we will set state and add a callback function, this will be used to fire our get posts function:

this.setState({ type, loading: true }, () => {
    // fetch posts, then set loading = false    
    this.getPosts()
        .then(() => this.setState({ loading: false }));
})

View on GitHub

Let’s attach an onchange event to the select box like so:

<select
    //...
    onChange={this.handlePostTypeChange}
/>

View on GitHub

Then up in the constructor:

this.handlePostTypeChange = this.handlePostTypeChange.bind(this);

View on GitHub

If you start testing by switching post types, you will now notice how you end up getting duplicate posts, so we need to add a function to filter unique posts when an API responds. We will store this function in a new file in the utils folder, called useful-funcs.js. Within here we will store a few helpful functions, but for now we will just add the unique function like so:

export const uniqueBy = (arr, key) => {
    let keys = [];
    return arr.filter(item => {
        if (keys.indexOf(item[key]) !== -1) {
            return false;
        }

        return keys.push(item[key]);
    });
}

View on GitHub

To save a little time we will create a helper function to call the uniqueBy passing the key already:

export const uniqueById = arr => uniqueBy(arr, 'id');

View on GitHub

Once that’s complete we need to import it into our PostSelector component and utilise it in the get posts function.

Top Of Post Selector

import { uniqueById } from '../utils/useful-funcs';

View on GitHub

getPosts function

return api.getPosts(requestArguments)
            .then(response => {
                this.setState({
                    posts: uniqueById([...this.state.posts, ...response.data]),
                });

                // return response to continue the chain
                return response;
            })

View on GitHub

On to another complex section of this component, integrating search into our component. The steps in this section will be, setting up state, handling the input event, handling loading and correct rendering

Setting up state

In our state we need to add three new properties, one for storing the current filter, storing the filtered posts, then finally storing whether it is loading or not. In our initial state declaration add:

this.state = {
    //...
    filter: '',
    filterLoading: false,
    filterPosts: [],
}

Handling input

Let’s outline what we need this function to do; it will need to save the input value to state, then call a debounced function that will handle the API requests. Create two functions handleInputFilterChange and doPostFilter.

handleInputFilterChange({ target: { value:filter = '' } = {} } = {}) {

}

doPostFilter() {

}

Within the handleInputFilterChange function we need to set the state for the input, check if it’s empty and then if permitted call doPostFilter.

handleInputFilterChange({ target: { value:filter = '' } = {} } = {}) {
    this.setState({
        filter
    }, () => {
        if (!filter) {
            // remove filtered posts
            return this.setState({ filteredPosts: [], filtering: false });
        }

        this.doPostFilter();
    })
}

View on GitHub

In the constructor we will bind out handle function to this

this.handleInputFilterChange = this.handleInputFilterChange.bind(this);

View on GitHub

Now we can attach our function and state to the search input:

<input 
    id="searchinput" 
    type="search" 
    placeholder={"Please enter your search query..."}
    value={this.state.filter}
    onChange={this.handleInputFilterChange}
/>

View on GitHub

Calling the API

Since we have the input event handled we now need to run the getPosts query to fetch our posts:

doPostFilter() {
    const { filter = '' } = this.state;

    if (!filter) {
        return;
    }

    this.setState({
        filtering: true,
        filterLoading: true
    });

    this.getPosts()
        .then(() => {
            this.setState({ 
                filterLoading: false
            });
        })
}

View on GitHub

We also need to handle the filter for if the post type is changed, so we need to add filter into the default arguments:

const defaultArgs = {
    per_page: 10,
    type: this.state.type,
    search: this.state.filter
};

Then if search is set we set the state:

return api.getPosts(requestArguments)
            .then(response => {
                if (requestArguments.search) {
                    this.setState({
                        filterPosts: response.data
                    });

                    // return here to stop our post list getting filled with unused posts.
                    return response;
                }

                // ...
            }

View on GitHub

Rendering the results.

Let’s render the results, this will be as simple as switching out our post list with the ‘filtered’ list.

In the render function we will use a ternary to decide to use posts or filtered posts:

const isFiltered = this.state.filtering;
const postList = isFiltered && !this.state.filterLoading ? this.state.filterPosts : this.state.posts.filter(post => post.type === this.state.type);

View on GitHub

Then add the postList var to our post list:

<PostList
    posts={postList}
    loading={this.state.loading}
    action={this.addPost}
/>

We can then pass our loading variable, and pass down is filtered:

<PostList
    posts={postList}
    loading={this.state.loading || this.state.filterLoading}
    filtered={isFiltered}
    action={this.addPost}
/>

View on GitHub

In our PostList let’s add a message for if we are filtering and it returns no results. To do this we need to grab the filtered variable from props and then check to see if any posts were returned:

const { filtered = false, loading = false, posts = [], action = () => {} } = props;

View on GitHub

After the if (loading)

if (filtered && posts.length < 1) {
    return (
        <div className="post-list">
            <p>Your query yielded no results, please try again.</p>
        </div>
    );
}

View on GitHub

Finishing up

To finish up the search functionality we need to debounce the doFilterPosts, as the function will be called far too often and will hinder performance, so we need to modify our add posts function.

In our useful-funcs file we need to create the debounce function:

export const debounce = (func, wait) => {
    let timeout = null;

    return function () {
        const context = this;
        const args = arguments;

        const later = () => {
            func.apply(context, args);
        };

        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    }
};

View on GitHub

Back to PostSelector, import the debounce function:

import { uniqueById, debounce } from '../utils/useful-funcs';

View on GitHub

In our constructor we will overwrite our function with the debounced version like so:

this.doPostFilter = debounce(this.doPostFilter.bind(this), 300);

View on GitHub

At the top of the addPost function we need to add the selected item to our post list from our filterPosts array:

if (this.state.filter) {
    const post = this.state.filterPosts.filter(p => p.id === post_id);
    const posts = uniqueById([
        ...this.state.posts,
        ...post
    ]);

    this.setState({
        posts
    });
}

View on GitHub

Thats the search feature complete. 🙌

Pagination

It took me a little while to think of an efficient way to handle the pagination, which would be a catch-all for most cases. We want to retain which page you were on for each post type and the search results, then use the correct page number. However, with the search, the page number needs to change every time the query changes.

Setting up state

In the constructor, add our state for holding the current pages, the ‘loading next page’ variable, and finally the pages left:

// ...
pages: {},
pagesTotal: {},
paging: false,
// ...

View on GitHub

Modifying the query

In the getPosts function, we need to add a new default argument page. We will set the Page Number for the article like so:

// Sets page key top false if its a filter.
// Saves resetting the filter page numerous times.
const pageKey = this.state.filter ? false : this.state.type;

const defaultArgs = {
    //...
    page: this.state.pages[pageKey] || 1,
}

View on GitHub

In our return statement we want to set our pages and pages total state for the filter object, and the normal post type pagination data:

return api.getPosts(requestArguments)
            .then(response => {
                if (requestArguments.search) {
                    this.setState({
                        filterPosts: requestArguments.page > 1 ? uniqueById([...this.state.filterPosts, ...response.data]) : response.data,
                        pages: {
                            ...this.state.pages,
                            filter: requestArguments.page
                        },
                        pagesTotal: {
                            ...this.state.pagesTotal,
                            filter: response.headers['x-wp-totalpages'],
                        }
                    });

                    return response;
                }

                this.setState({
                    posts: uniqueById([...this.state.posts, ...response.data]),
                    pages: {
                        ...this.state.pages,
                        [pageKey]: requestArguments.page
                    },
                    pagesTotal: {
                        ...this.state.pagesTotal,
                        [pageKey]: response.headers['x-wp-totalpages'],
                    }
                });

                // return response to continue the chain
                return response;
            })

View on GitHub

Managing props for the Post List

We need to pass props down to the post list that:

  1. Tells us if we can paginate.
  2. Gives us the function to call.
  3. Tells us if we are currently changing page.

In our render function we need to check if the list can paginate, and we can do this by grabbing the current page and checking if it is less than the total pages like so:

const pageKey = this.state.filter ? 'filter' : this.state.type;
const canPaginate = (this.state.pages[pageKey] || 1) < this.state.pagesTotal[pageKey];

View on GitHub

We also need to create a function for handling the page change, for now we will just define it and I will go through its contents further down:

doPagination() {

}

In the constructor bind it to this:

this.doPagination = this.doPagination.bind(this);

Great, so our function is declared and can be passed down and maintain its context. Everything we need to pass has been declared so its time to attach props to our main post list:

<PostList
    // ...
    paging={this.state.paging}
    canPaginate={canPaginate}
    doPagination={this.doPagination}
/>

View on GitHub

Use the pagination props in the postList

It is time to link up everything in post list. We need to show a button when canPaginate is true but if paging is true the button text will change and the button will be disabled. This is how our return block should look in the PostList component:

return (
    <div class="post-list">
        {posts.map((post) => <Post key={post.id} {...post} clickHandler={action} />)}
        {props.canPaginate ? (<button onClick={props.doPagination} disabled={props.paging}>{props.paging ? 'Loading...' : 'Load More'}</button>) : null}
    </div>
)

View on GitHub

Loading the posts

Everything is now set up, so when we finish our doPagination function everything will just work. In the function we need to set paging to true, call getPosts passing an object with the page number we want,then once thats done setting paging to false.

Our function body should look like this:

this.setState({
    paging: true
});

const pageKey = this.state.filter ? 'filter' : this.state.type;
const page = parseInt(this.state.pages[pageKey], 10) + 1 || 2;

this.getPosts({ page })
    .then(() => this.setState({
        paging: false,
    }));

View on GitHub

Fixing up the component loading for selected posts

So you may have noticed that if you selected a paginated post or a object of a different post type, they wouldn’t show unless they appeared in the posts object. This is an easy fix, first we are going to add a new property to our state object initialLoading, this property will default to false just like our other loaders.

this.state = {
 // ...
 initialLoading: false,
}

In our render function let’s change the selected post lists loading property to be our new initialLoading property and the other post list argument to accept it as an option, they should now look like this:

<PostList
    posts={postList}
    loading={this.state.initialLoading||this.state.loading||this.state.filterLoading}
    filtered={isFiltered}
    action={this.addPost}
    paging={this.state.paging}
    canPaginate={canPaginate}
    doPagination={this.doPagination}
/>
<PostList
    posts={this.getSelectedPosts()}
    loading={this.state.initialLoading}
    action={this.removePost}
/>

View on GitHub

In our componentDidMount function we need to set our initial loading var to true, loop through all post types with our selected ids, then do our first fetch of posts.

To do this we are going to create a new function retrieveSelectedPosts, this will return a promise:

retrieveSelectedPosts() {
    const selected = this.props.selectedPosts;
    const { types } = this.state;

    if (!selected.length > 0) {
        // return a fake promise that auto resolves.
        return new Promise((resolve) => resolve());
    }

    return Promise.all(Object.keys(types).map(type => this.getPosts({
            include: this.props.selectedPosts.join(','),
            per_page: 100,
            type
        })
    ));
}

View on GitHub

Inside of our componentDidMount function we will set initialLoading to true:

this.setState({
    loading: true,
    initialLoading: true,
});

Once we have fetched all of the post types we need to call our retrieveSelectedPosts function that will then set initial loading to false and call our standard getPosts function:

this.setState({
    types: data
}, () => {
    this.retrieveSelectedPosts()
        .then(() => {
            this.setState({
                initialLoading: false,
            });
            this.getPosts()
                .then(() => {
                    this.setState({ loading: false })
                } );
        })
})

View on GitHub

To grab the featured image you can make another API call but the API has another way to give you the media without requiring the extra call. To do this we need to append &_embed onto our query, something that we actually did earlier when first setting up our query.

However, we need to make some modifications to the getPosts response to include the media url. So in the getPosts function just after return api.getPosts(requestArguments) let’s start by creating a then(() => {}) callback:

return api.getPosts(requestArguments)
    .then(response => {

        // prevent things from breaking
        return response;
    })
    // the other then function that sets state

In our function we need to loop through the posts checking if it has a featured_media property is greater than 0. If so we need to dive into the _embedded property to pull out our featured media. We can do this like so:

// ...
const { data } = response;
const posts = data.map(p => {
    if (!p.featured_media || p.featured_media < 1) {
        return {
            ...p,
            featured_image: false
        };
    }

    return {
        ...p,
        featured_image: p._embedded['wp:featuredmedia'][0].source_url || false
    }
})

return {
    ...response,
    data: posts
};
// ...

View on GitHub

Now in our Post component let’s echo out our image:

export const Post = ({ title: { rendered: postTitle } = {}, clickHandler, id: postId, featured_image = false }) => (
    <article className="post">
        <figure className="post-figure" style={{ backgroundImage: `url(${featured_image})` }}>
        </figure>
        <div className="post-body">
            <h3 className="post-title">{postTitle}</h3>
        </div>
        <button onClick={() => clickHandler(postId)}>+/-</button>
    </article>
);

View on GitHub

Adding dash icons to the component

It’s time to add those finishing touches to the component, we are going to add some icons for the search and +/- button.

To do this we need to first import BlockIcon from wp.blocks; we will do this at the top of the PostSelector component:

const { BlockIcon } = wp.blocks;

View on GitHub

Let’s add the search icon next to the search input; in the render function above the search input we will add:

<BlockIcon icon="search" />

View on GitHub

Moving on, let’s add and remove buttons on the posts, we need to pass down the icons dependant on which list it is, and we can do it by creating to variables, addIcon and removeIcon:

const addIcon = <BlockIcon icon="plus" />;
const removeIcon = <BlockIcon icon="minus" />;

View on GitHub

Now pass the addIcon to the first post list and then pass the removeIcon to the selected posts list:

<PostList
    posts={postList}
    loading={this.state.initialLoading||this.state.loading||this.state.filterLoading}
    filtered={isFiltered}
    action={this.addPost}
    paging={this.state.paging}
    canPaginate={canPaginate}
    doPagination={this.doPagination}
    icon={addIcon}
/>
<PostList
    posts={this.getSelectedPosts()}
    loading={this.state.initialLoading}
    action={this.removePost}
    icon={removeIcon}
/>

View on GitHub

In our PostList component we need to pass the icon to the Post compost like so:

const { filtered = false, loading = false, posts = [], action = () => {}, icon = null } = props;

// ...

{posts.map((post) => <Post key={post.id} {...post} clickHandler={action} icon={icon} />)}

View on GitHub

Then in Post.js we just need to echo it out:

export const Post = ({ title: { rendered: postTitle } = {}, clickHandler, id: postId, featured_image = false, icon }) => (
    <article className="post">
        <figure className="post-figure" style={{ backgroundImage: `url(${featured_image})` }}>
        </figure>
        <div className="post-body">
            <h3 className="post-title">{postTitle}</h3>
        </div>
        <button onClick={() => clickHandler(postId)}>{icon}</button>
    </article>
);

Server-side rendering

Since we are creating a post list we need to render this server side, or post changes won’t render within our block. To do this first we need to make sure our save function in block.js returns null.

save() {
    return null;
}

View on GitHub

Now our javascript won’t be rendering anything, so we need to tell it to use php. The way we do that is by registering the block in php. First we are going to modify the directory/file structure for the php.

In our src folder create a new folder called inc and in there create two new files, scripts-and-styles.php and render.php

In our init.php file we need to grab everything after:

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

View on GitHub

And then put it into inc/scripts-and-styles.php, replacing each occurrence of

dirname( __FILE__ )

To:

dirname( __DIR__ )

View on GitHub

After that we need to require the scripts and styles, and also include the render file too:

require_once 'inc/scripts-and-styles.php';
require_once 'inc/render.php';

View on GitHub

We need to register our block via php, we will do this in the init file. The first parameter of register_block_type is the name of your block that you used in the block.js file.

The name of the render_callback is up to you, and the editor script can be grabbed from scripts-and-styles.php in the *_cgb_editor_assets function where there is a call to wp_enqueue_script, the first parameter of this function call is what our editor_script will be:

function bb_guten_post_list_init_block() {
    register_block_type( 'bigbite/postlist', array(
        'render_callback' => 'bb_render_post_list_block',
        'editor_script'   => 'guten_post_list-cgb-block-js',
    ) );
}

add_action( 'init', 'bb_guten_post_list_init_block' );

View on GitHub

With the setup completed, the rest of the work will happen within the render.php file.

Let’s create our function; it accepts one parameter, an array, containing all of our attributes previously declared in the Gutenberg block:

function bb_render_post_list_block( $attributes ) {

}

View on GitHub

So to start what we need to get the attributes out of the array and return them:

function bb_render_post_list_block( $attributes ) {
    $block_title    = isset( $attributes['blockTitle'] ) ? $attributes['blockTitle'] : false;
    $selected_posts = isset( $attributes['selectedPosts'] ) ? $attributes['selectedPosts'] : false;
}

View on GitHub

The way we are going to do this is by grabbing the posts via WP_Query then using filters to modify the return content. This leaves us with a way to allow for custom templates from the theme; if you wanted to take it one step further you could create a way for users to select what type of list they want to create and then choose to echo it out.

Let’s grab the posts:

//...
$object_query = new WP_Query([
    'post__in'  => $selected_posts,
    'post_type' => get_post_types(),
    'order_by'  => 'posts__in'
]);

View on GitHub

Now we can return the filtered content:

return apply_filters( 'gutenberg_post_list_render_filter', $object_query, $block_title );

So currently this won’t do anything as we haven’t registered a filter, we should do this inside of the init.php file; Inside the bb_post_list_init_block add this:

add_filter('gutenberg_post_list_render_filter', 'render_guten_post_list_filter', 10, 2);

View on GitHub

We can now go back to our render.php and create our render_guten_post_list_filter function, this function takes 2 parameters, the object query and the title:

function render_guten_post_list_filter( $query, $title ) {

}

View on GitHub

First step is to check if there is any posts, if not then we return nothing:

if ( ! $query->have_posts() ) {
    return;
}

Then we need to open an output buffer, and return it’s contents like so:

ob_start();

return ob_get_clean();

Between these two statements we will loop through each object and echo out the title like so:

while( $query->have_posts() ):
    $query->the_post();
?>
    <article>
        <h1><?php the_title(); ?></h1>
    </article>
<?php
endwhile;

View on GitHub

From here you can start customising your post list, by adding your own styles, classes and layout. Have fun!

Summary

Creating a Gutenberg block is simple, creating a more feature rich Block can be difficult. However, since Gutenberg runs on react you can build anything!

If you found this post useful or if you have any issues leave a comment below.

The source code can be found here


Interested in learning more about enterprise WordPress?
Get in touch with the team today