Building a required modal in the WordPress Block Editor - Aurooba

Building a required modal in the WordPress Block Editor Part 1

During my first Twitch stream, I worked on a feature for a project, which turned out pretty cool. Here’s how I went about building a required modal in the WordPress Block Editor, effectively interrupting the post creation experience, on purpose.

This intermediate tutorial requires some understanding of React, state in React, JSX, and how the Block Editor works. If you’re new to developing for the Block Editor, start with my free 10 day email series on Block Editor Essentials that don’t require JavaScript.

Once I started writing this tutorial, I realized how long it is. So I’ve broken it up into 3 posts. So this is part one of the tutorial on how to build a required modal in the WordPress Block Editor.

The end result

Here’s a little video of what we’re building in this tutorial:

When you add a new post, a modal immediately pops up. It asks you to confirm if you have permission to publish the work you’re about to share: “Royalties must be paid before published work is added to our website. Original work does not require payment of royalties.”

You can choose between Original work or Published work. If you choose Original, you can click the Start Creating button and go on with your post. If you choose Published work, you are additionally asked to confirm – by way of a checkbox – that you have have paid royalties for streaming this work online.

The Document Panel version of the streaming rights settings

This choice is reflected in the Document panel of the post, so it’s available for later when reviewing the post.

Admittedly, I was livestreaming while also very tired and I definitely floundered a bit and made some silly I-need-sleep mistakes. However, I think that’s often part of life. So I’m comfortable with having publicly floundered. I ain’t perfect. However, this tutorial is streamlined and demonstrates the final, complete, and cleaned up solution. 🙂

The initial idea was to build a popup that would ask the user to confirm a couple conditions (physically acknowledge they have consent to publish the work), before they would be allowed to create a post. Going into it, I thought I would use the Guide component, which is used by the default Welcome Guide, but about half hour into my livestream, I discovered the Modal component, which fit my requirements exactly.

Anyway, onto actually building a required modal in the WordPress Block Editor – wow, that’s a mouthful. 😂

Building the modal

I recommend you always do this kind of customization work in a plugin. It’s safer, more portable, and if something isn’t affecting the aesthetics of a website, it doesn’t really belong in a theme, anyways. So let’s set up our plugin, shall we?

Setting up our build tools

Actually, a lot of what I’m doing can be done without build tools. However, they make it easier to do more interesting things cleanly. Plus, WordPress has @wordpress/scripts, which makes things a whole lot quicker and easier to set up.

This is a quick and dirty setup without much explanation. I promise a complete tutorial on just setting up build tools and really understanding what is going on, is in the works.

In Terminal (or iTerm2, in my case), navigate to your plugins directory in your local WordPress installation (or wherever you sym link your plugins in from, in my case) and run the following:

npx @wordpress/create-block required-modal-block

This will create a WordPress plugin with @wordpress/scripts installed and ready to go. Then hop into the new required-modal-block directory, open it up in your preferred code editor, and run:

npm start

This will watch for changes in the file and compile on the go.

Cleaning up and adjusting the files a bit

Now, we aren’t actually creating a block, we’re creating a plugin for the editor, so we can safely delete the block.json file, remove the entire register_block section in the main php file, and fix the description in the main php file and in package.json to reflect what we’re actually doing. Go ahead and delete the contents of src/index.js as well, and delete the edit.js and save.js files, because we won’t be needing them.

Important: In the main php file, you want to change all instances of wp_register_script and wp_register_style with wp_enqueue_{script / style} so our scripts still get enqueued, even though we aren’t making a block.

The technical details of what we are building

  1. We are building a modal: it will interrupt the initial post creation experience to confirm some things. It shouldn’t appear every time the post is opened up in the Editor; we just want it to show up when it’s a brand new post. This means, this modal should only show up when the post has the auto-draft status.
  2. People shouldn’t be able to bypass the modal, they must confirm the information and make a selection. So we need to make sure the button to close the modal is only enabled when the conditions are met, and you can’t use Esc key to get rid of the modal.
  3. Once the modal is closed, it shouldn’t show up again in the session. So we need to make sure we keep track of whether the modal was shown or not, and once it’s shown once in a session, we don’t want to see it again.
  4. This information needs to be saved, so it’s available in subsequent sessions and for others (editors) to see. We can’t really serialize this, because we’re not creating a block. Custom (meta) fields were originally built for exactly this kind of use case. This means we need to attach some custom meta to this post, where this information is stored.
  5. We don’t want the modal to be the only way this information can be accessed. The modal only shows up shows up the first time. They need to share state. So we also want to add a panel in the Document Inspector that displays the saved information and updates at the same time as the Modal.

Registering our Block Editor plugin

Since this is not a block, we’re going to be registering a plugin to add this functionality to the block editor. In your index.js file, you’re going to import registerPlugin, and set up our call to it:

import { registerPlugin } from "@wordpress/plugins";

registerPlugin("aurooba-makes-modal", {
     render: streamingSectionsWithPostType,
 });

Passing our current post type to the function

I’ve called the callback streamingSectionsWithPostType because we’re actually going to wrap this constant in withSelect from @wordpress/data so we can pass the Post Type and make it available to the modal on initialization.

So let’s import withSelect and create streamingSectionsWithPostType (line 2-8):

import { registerPlugin } from "@wordpress/plugins";
import { withSelect } from "@wordpress/data";

const streamingSectionsWithPostType = withSelect((select) => {
     return {
         postType: select("core/editor").getCurrentPostType(),
     };
 })(streamingSections);

registerPlugin("aurooba-makes-modal", {
     render: streamingSectionsWithPostType,
 });

Using withSelect, we’re using getCurrentPostType() from core/editor data store, storing it in the postType variable, and passing it to the our main function where we’ll be doing our work: streamingSections.

Test this out in the console to see how it works:

Open up the block editor and then in your console, type this in and hit enter:

wp.data.select("core/editor").getCurrentPostType()

Cool, right?

Creating our main function and checking the post type

Now let’s create streamingSections, the only props we are passing to it, is postType, and if postType is not page, we don’t want to continue with this.

const streamingSections = (postType) => {
     if ("page" !== postType.postType) {
         return null;
     }
}

postType is actually an object, so that’s why we have to check postType.postType.

Using State to check if the modal has been shown and whether to open it or not

With useState we’re going to keep track of when the modal has been shown and whether it should be open or not. So we’ll import useState into index.js and set up our state constants (lines 8-11):

import { registerPlugin } from "@wordpress/plugins";
import { useState } from "@wordpress/element";

const streamingSections = (postType) => {
     if ("page" !== postType.postType) {
         return null;
     }
     // Initialize a temporary state variable to make sure the modal only shows up once per instance
     const [wasDisplayed, setDisplayed] = useState(false);
    // Set state to track open/close of modal
     const [isOpen, setOpen] = useState(true);
}

const streamingSectionsWithPostType = withSelect((select) => {
     return {
         postType: select("core/editor").getCurrentPostType(),
     };
 })(streamingSections);
 registerPlugin("aurooba-makes-modal", {
     render: streamingSectionsWithPostType,
 });

In an effort to keep things clean and organized, let’s create a new file called modal.js in our src/ folder, where we’ll tackle the actual Modal.

Now in our index.js file, we’ll import the component we’ll be building, like so:

import { StreamingModal } from "./modal";

Then we can return StreamingModal in index.js, while also passing it the state variables we just created, and we’ll wrap it in Fragment for ease later on:

    return (
        <>
            <StreamingModal
                isOpen={isOpen}
                wasDisplayed={wasDisplayed}
            />
        </>
    );

<> and </> are shortcuts for the Fragment component. In React, your return statement needs to be wrapped into one component. Since we want to return our Modal and our document panel (eventually), we can wrap them in a fragment component, which ultimately displays as nothing but stops React from yelling at us.

Then in modal.js, let’s initialize our modal:

import { Modal } from "@wordpress/components";

export const StreamingModal = (props) => {
    const {
        wasDisplayed,
        isOpen,
    } = props;

    // if modal has already been displayed, don't display it again
    if (wasDisplayed) {
        return null;
    }
// The Modal itself
    return (
        <>
            {isOpen && (
                <Modal></Modal>
            )}
        </>
    );
};

Right away, we’ve added some logic here using our state variables.

If wasDisplayed is true, we return nothing because the modal has already done its job.

Then in our return, if isOpen is set to true, we show the Modal. If you run this code now, here’s what you will see:

an empty modal that you can’t exit

This modal is empty, aside from the X button, which doesn’t work, and a separator line.

Let’s add a few more attributes that the Modal component supports to remove the X button, add a title, and stop it from being dismissable in any way:

import { Modal } from "@wordpress/components";

export const StreamingModal = (props) => {
    const {
        wasDisplayed,
        isOpen,
    } = props;

    // if modal has already been displayed, don't display it again
    if (wasDisplayed) {
        return null;
    }
// The Modal itself
    return (
        <>
            {isOpen && (
                <Modal
                    title="Streaming Royalties"
                    isDismissible={false}
                    shouldCloseOnClickOutside={false}
                    shouldCloseOnEsc={false}
                >
                </Modal>
            )}
        </>
    );
};

Streaming Royalties shows up as the title, and the X button is gone

Adding a button to close the modal

We still need a way to be able to exit this modal, so let’s add a button, which will trigger a closeModal function (which we will create).

Let’s import Button into modal.js and add it inside our Modal (note, I’ve imported the internationalization variable and am using. it in here now):

/**
 * WordPress dependencies
 */
import { __ } from "@wordpress/i18n";
import { Modal, Button } from "@wordpress/components";

export const StreamingModal = (props) => {
    const {
        wasDisplayed,
        isOpen,
        closeModal,
    } = props;

    // if the Modal was already displayed this session, don't display it again
    if (wasDisplayed) {
        return null;
    }
    // The Modal itself
    return (
        <>
            {isOpen && (
                <Modal
                    title="Streaming Royalties"
                    isDismissible={false}
                    shouldCloseOnClickOutside={false}
                    shouldCloseOnEsc={false}
                >
                    <Button isPrimary onClick={closeModal}>
                        {__("Start Creating")}
                    </Button> 
                </Modal>
            )}
        </>
    );
};

We’re going to create our closeModal function in index.js, so we’ll be importing it as part of props, so let’s add that in our props breakout. Then inside our Modal, we have the actual button. We’re using the isPrimary attribute to give it the style of the primary buttons in the block editor, and the onClick function will be closeModal.

Then let’s hop back into. our index.js file and create our closeModal function. We’ll set the isOpen state variable to false, and since we have now shown the modal, we’ll set isDisplayed to true.

    const closeModal = () => {
        setOpen(false);
        setDisplayed(true);
    };

We also want to pass the closeModal function to the Modal component, so let’s update that in our return in index.js

<StreamingModal
    isOpen={isOpen}
    wasDisplayed={wasDisplayed}
    closeModal={closeModal}
/>

Now if you refresh your block editor, you’ll see a button, and clicking on. it will close the modal.

Only showing the modal for auto drafts

We only want to show the modal for auto-drafts (when we create a brand new post), otherwise it would be super annoying. To do that, we’ll get the post status from the data store, using useSelect (lines 15-22):

/**
 * WordPress dependencies
 */
import { __ } from "@wordpress/i18n";
import { useSelect } from "@wordpress/data";
import { Modal, Button } from "@wordpress/components";

export const StreamingModal = (props) => {
    const {
        wasDisplayed,
        isOpen,
        closeModal,
    } = props;

    // Get the Post Status
    const { status } = useSelect((select) => ({
        status: select("core/editor").getEditedPostAttribute("status"),
    }));
    // If the post status isn't an auto-draft, don't show the modal
    if ("auto-draft" !== status) {
        return null;
    }
    // if the Modal was already displayed this session, don't display it again
    if (wasDisplayed) {
        return null;
    }
    // The Modal itself
    return (
        <>
            {isOpen && (
                <Modal
                    title="Streaming Royalties"
                    isDismissible={false}
                    shouldCloseOnClickOutside={false}
                    shouldCloseOnEsc={false}
                >
                    <Button isPrimary onClick={closeModal}>
                        {__("Start Creating")}
                    </Button>
                </Modal>
            )}
        </>
    );
};

Test this out in the console to see how it works:

Open up the block editor and then in your console, type this in and hit enter:

wp.data.select("core/editor").getEditedPostAttribute("status")

Cool, right?

Now if you create a new post, dismiss the modal, give the post a title and save, and refresh, you’ll find the modal doesn’t appear. That’s because the status of the post is draft not, not auto-draft.

We have a modal!


We’ve officially built a required modal in the WordPress Block Editor. It doesn’t do anything yet, though. Next week, we’ll add fields to the modal which conditionally allow you to click the Start Creating button. 🙂

Sign up below to get useful tutorials straight in your inbox.