Build a Block Series – Part 7: Placeholder

In this seventh part, I do away with screencasting and work on improving the editor interaction with the QRCode block. Specifically, showing in the editor how the QRCode will look when published.

Previous screencasts:

  1. Development Environment
  2. WordPress Plugin
  3. Anatomy of a Block
  4. Block Attributes
  5. ES6+ Syntax
  6. QRCode

Intro

One of the primary tenets of Gutenberg is as a WYSIWYG editor, what you see in the editor, should be as close to what you get when published. Keep this in mind when building blocks.

A meta-comment, I’m not going to continue screencasting as part of this series. I don’t think there is enough value in hearing my voice and watching me type to offset the time and effort it takes. The explanations and code are where the true value lies, and it is easier for me to continue with just posts.

So on with the show.

Plan

The QRCode block, as we left it after Part 6, only shows a URL TextField in the editor. You don’t see the actual QRCode until the block is published. In this part, our goal is to show the URL field if none is entered, and the QRCode after the URL is entered.

There is a special property of a block called isSelected this is a boolean and is set to true when the block is selected, or being edited, and set to false when the author moves to another block in the editor.

We can use this property to control if the block is selected then show the URL field, otherwise show the QRCode, assuming a URL is set.

Code

If you have been following the series and look at the code, you will see I broke up the edit and save functions into their own file. I did this to make development a little easier isolating each, see the Import section of Part 5 for an explanation.

Placeholder

The state when a block has been inserted, but no data has been entered yet, is called a placeholder. There is a Placeholder component built that gives us a standard look. You can see example placeholders in use with the image and embed blocks.

To use the Placeholder, wrap the <TextControl> component so it becomes a child element of the <Placeholder> component. Try it out in your code. After updating, you might have something like:

<Placeholder
  label="QRCode Block"
  instructions="Add URL for QRCode"
>
  <TextControl
    value={ attributes.url }
    onChange={ ( val ) => setAttributes( { url: val } ) }
  />
</Placeholder>

Ternary Function

A ternary function is an inline if-else statement, using the syntax:

  ( clause ) ? ( doIfTrue ) : ( doIfFalse )

This can be used inside a block to control what shows when a parameter is set or not. A simple case that checks if the URL is set might look like:

  return (
    <div>
      { url ?
        <div>URL: { attributes.url }</div> :
        <div> No URL <TextField/> </div>
      }
  );

We need to add another check for isSelected since we want to show the QRCode field if the URL is set and !isSelected, meaning we are not editing the block, the focus is elsewhere.

The logic case is: attributes.url && ! isSelected

Combined together: we want to show the same div from the save function, this holds our QRCode; otherwise, if no URL or the block has the focus, show the Placeholder. Go ahead and attempt an implementation.

Here’s what my edit function looks like this:

const edit = ( { attributes, isSelected, setAttributes } ) => {
    return (
        <div>
            { attributes.url && ! isSelected ?
                <div
                    id="qrcode"
                    data-url={ attributes.url }
                ></div> :
                <Placeholder
                    label="QRCode Block"
                    instructions="Add URL for QRCode"
                >
                    <TextControl
                        value={ attributes.url }
                        onChange={ ( val ) => setAttributes( { url: val } ) }
                    />
                </Placeholder>
            }
        </div>
    );
}

Show QRCode in Editor

The above function will work, but the QRCode will not show. It only inserts the div which as-is renders nothing visible. This is because we only load the library and run the trigger on the front-end.

We need to update qrcode-block.php and load the library on the back-end, in WordPress these are called the admin screens. Use admin_enqueue_scripts hook to enqueue a script there.

add_action( 'admin_enqueue_scripts', function() {
  wp_enqueue_script( 'mkaz-qrcode-qrcodejs' );
} );

Trigger

The next part is to add the trigger to run each time the URL is updated and the <div> is rendered. There is a mechanism already in place to do this, it is the React Hook useEffect. To use this hook, you define a function that React will call after each render.

In our case, the function we want to call is the same trigger code used on the front-end. I just duplicate the code, for now, it probably could be refactored later so both use the same function.

Putting it all together, here is the complete edit function. When editing or no URL is set it renders a Placeholder, otherwise, it shows the QRCode when the URL is set.

import {
    Placeholder,
    TextControl,
} from '@wordpress/components';

import { useEffect } from '@wordpress/element';

const edit = ( { attributes, isSelected, setAttributes } ) => {
    useEffect( () => {
        if ( ! isSelected ) {
            const elem = document.getElementById( 'qrcode' );
            if ( elem ) {
                const url = elem.dataset.url;
                new QRCode( elem, {
                    text: url,
                    width: 256,
                    height: 256,
                    colorDark: '#000000',
                    colorLight: '#ffffff',
                    correctLevel: QRCode.CorrectLevel.H,
                } );
            }
        }
    } );

    return (
        <div>
            { attributes.url && ! isSelected ?
                <div
                    id="qrcode"
                    data-url={ attributes.url }
                ></div> :
                <Placeholder
                    label="QRCode Block"
                    instructions="Add URL for QRCode"
                >
                    <TextControl
                        value={ attributes.url }
                        onChange={ ( val ) => setAttributes( { url: val } ) }
                    />
                </Placeholder>
            }
        </div>
    );
};

export default edit;

The updated code is available at github.com/mkaz/qrcode-block

If you want to see the changes I made just for this part, see PR #1