David Yeiser

Create Your Own Dynamic Gutenberg Block for Wordpress, Part 2

A collection of code snippets for building your own custom WordPress Gutenberg blocks.

This is “part 2” of the Create Your Own Dynamic Gutenberg Block for Wordpress tutorial that was published last year. But instead of a narrative step-by-step it’s a collection of useful code snippets you can use to build feature rich custom Gutenberg blocks for WordPress (see the 1/14/20 update at the beginning of part 1 for a little more context for this portion).

The first tutorial covers the infrastructure required to house custom blocks in WordPress, so go there first if you don’t know how to set up your own Gutenberg blocks. Then once that is complete you can add the code snippets below to further develop your custom Gutenberg blocks. The companion GitHub repository tracks the lastest code snippets added here on the master branch. The base code developed in part 1 is maintained in the tutorial/part1 branch.

Important!

Note that the code snippets below should be inserted into the appropriate locations in the file in which they go, never just copy and then replace the full file code, this will break the code. You can always refer to the GitHub repository to see the full context.


Table of Contents


Image Upload

Steps:

  • Use MediaUpload and MediaUploadCheck from wp.blockEditor.
  • Access Button from wp.components for a UI helper.

Helpful documentation:

index.js

In blocks/book-details/index.js, add the following code:

…

const { MediaUpload, MediaUploadCheck, RichText } = wp.blockEditor
const { Button } = wp.components

…

registerBlockType('davidyeiser-detailer/book-details', {
  …
  // Set up data model for custom block
  attributes: {
    image: {
      type: 'object',
      selector: 'js-book-details-image'
    },
    …
  },
  …

  // The UI for the WordPress editor
  edit: props => {
    // Pull out the props we'll use
    const { attributes, className, setAttributes } = props

    // Pull out specific attributes for clarity below
    const { image } = attributes

    return (
      <div className={className}>
        <MediaUploadCheck>
          <MediaUpload
            className="js-book-details-image wp-admin-book-details-image"
            allowedTypes={['image']}
            multiple={false}
            value={image ? image.id : ''}
            onSelect={image => setAttributes({ image: image })}
            render={({ open }) => (
              image ?
                <div>
                  <p>
                    <img src={image.url} width={image.width / 2} />
                  </p>

                  <p>
                    <Button onClick={() => setAttributes({ image: '' })} className="button is-small">Remove</Button>
                  </p>
                </div> :
                <Button onClick={open} className="button">Upload Image</Button>
            )}
          />
        </MediaUploadCheck>

        …
      </div>
    )
  },

…

index.php

In blocks/book-details/index.php, add the following code:

…

function render_dynamic_block($attributes) {
  // Parse attributes
  $book_details_imageObj = $attributes['image'];
  $book_details_image_url = $book_details_imageObj['sizes']['full']['url'];
  $book_details_image_alt_text = $book_details_imageObj['alt'];
  $book_details_image_width = $book_details_imageObj['sizes']['full']['width'] / 2;

  …

  /* BEGIN HTML OUTPUT */
?>
  <div class="block-book-details">
    <?php if ($book_details_image_url) : ?>
      <img class="book-details-image" src="<?php echo $book_details_image_url; ?>" alt="<?php echo $book_details_image_alt_text; ?>" width="<?php echo $book_details_image_width; ?>" />
    <?php endif; ?>

    …
  </div>
…

Toggle Switch

Along with the toggle switch, we also access the block’s sidebar so we can place meta details and controls there instead of inline with the content.

Steps:

  • Access InspectorControls from wp.blockEditor to add the toggle in the sidebar.
  • Use PanelBody, PanelRow, ToggleControl from wp.components to build the toggle and toggle area.

index.js

In blocks/book-details/index.js, add the following code:

…

const { InspectorControls } = wp.blockEditor
const { PanelBody, PanelRow, ToggleControl } = wp.components

…

registerBlockType('davidyeiser-detailer/book-details', {
  …
  // Set up data model for custom block
  attributes: {
    haveRead: {
      type: 'boolean',
      selector: 'js-book-details-read'
    },
    …
  },
  …

  // The UI for the WordPress editor
  edit: props => {
    // Pull out the props we'll use
    const { attributes, className, setAttributes } = props

    // Pull out specific attributes for clarity below
    const { haveRead } = attributes

    return (
      <div className={className}>

        {/* Sidebar Controls */}
        <InspectorControls>
          <PanelBody title={__('Book Status')}>
            <PanelRow>
              <ToggleControl
                className="js-book-details-read"
                label="Read"
                checked={haveRead}
                help={haveRead ? "This book has been read." : "Currently unread."}
                onChange={checked => setAttributes({ haveRead: checked })}
              />
            </PanelRow>
          </PanelBody>
        </InspectorControls>

        …
      </div>
    )
  },

…

index.php

In blocks/book-details/index.php, add the following code:

…

function render_dynamic_block($attributes) {
  // Parse attributes
  $book_details_have_read = $attributes['haveRead'];

  …

  /* BEGIN HTML OUTPUT */
?>
  <div class="block-book-details">
    <?php if ($book_details_have_read) : ?>
      <p><em>This book has been read.</em></p>
    <?php endif; ?>

    …
  </div>
…

Using state to manage variable number of items

Note that this one requires a structural change to the edit property of the registerBlockType() function from a stateless component function to a WordPress (React) Component class. I also added the shortid npm package to generate ids for React keys.

Also, there was a blog post I referenced for a lot of this, but I can no longer find it! If I do I’ll link it here.

Steps:

  • Add shortid package to create ids (yarn add shortid).
  • Access WordPress’s Component class.
  • Change stateless component to extend the WordPress (React) Component class so we can use state to manage data.

index.js

In blocks/book-details/index.js, add the following code:


const { Component } = wp.element

…

registerBlockType('davidyeiser-detailer/book-details', {
  …
  // Set up data model for custom block
  attributes: {
    quotes: {
      type: 'array',
      selector: 'js-book-details-quotes'
    },
    …
  },
  …

  // The UI for the WordPress editor
  edit: class BookDetails extends Component {
    constructor() {
      super(...arguments)

      // Match current state to saved quotes (if they exist)
      this.state = {
        quotes: this.props.attributes.quotes || []
      }

      this.addQuote = this.addQuote.bind(this)
      this.removeQuote = this.removeQuote.bind(this)
      this.editQuote = this.editQuote.bind(this)
    }

    // adds empty placeholder for quote
    addQuote(e) {
      e.preventDefault()

      // get quotes from state
      const { quotes } = this.state

      // set up empty quote
      const emptyQuote = {
        id: shortid.generate(),
        content: '',
        pageRef: ''
      }

      // append new emptyQuote object to quotes
      const newQuotes = [...quotes, emptyQuote]

      // save new placeholder to WordPress
      this.props.setAttributes({ quotes: newQuotes })

      // and update state
      return this.setState({ quotes: newQuotes })
    }

    // remove item
    removeQuote(e, index) {
      e.preventDefault()

      // make a true copy of quotes
      // const { quotes } = this.state does not work
      const quotes = JSON.parse(JSON.stringify(this.state.quotes))

      // remove specified item
      quotes.splice(index, 1)

      // save updated quotes and update state (in callback)
      return (
        this.props.setAttributes(
          { quotes: quotes },
          this.setState({ quotes: quotes })
        )
      )
    }

    // handler function to update quote
    editQuote(key, index, value) {
      // make a true copy of quotes
      const quotes = JSON.parse(JSON.stringify(this.state.quotes))
      if (quotes.length === 0) return

      // update value
      quotes[index][key] = value

      // save values in WordPress and update state (in callback)
      return (
        this.props.setAttributes(
          { quotes: quotes },
          this.setState({ quotes: quotes })
        )
      )
    }

    render() {
      // Pull out the props we'll use
      const { attributes, className, setAttributes } = this.props

      // Pull out specific attributes for clarity below
      const { haveRead, image, quotes } = attributes

      return (
        <div className={className}>
          …

          {!!quotes && quotes.map((quote, index) =>
            <div key={quote.id || index} className="wp-admin-book-details-quote">
              <RichText
                className="wp-admin-book-details-quote-content"
                value={quote.content}
                onChange={value => this.editQuote('content', index, value)}
                tagName="div"
                multiline="p"
                placeholder="Quote"
              />

              <RichText
                className="wp-admin-book-details-quote-page-ref"
                value={quote.pageRef}
                onChange={value => this.editQuote('pageRef', index, value)}
                tagName="p"
                placeholder="Page number"
              />

              <p>
                <input
                  className="button-secondary button"
                  type="submit"
                  value="Remove Quote"
                  onClick={(e) => this.removeQuote(e, index)}
                />
              </p>
            </div>
          )}

          <p class="wp-admin-book-details-quote">
            <input
              className="button-primary button"
              type="submit"
              value="Add Quote"
              onClick={(e) => this.addQuote(e)}
            />
          </p>

          …
        </div>
      )
    }
  },

…

index.php

In blocks/book-details/index.php, add the following code:

…

function render_dynamic_block($attributes) {
  // Parse attributes
  $book_details_quotes = $attributes['quotes'];

  …

  /* BEGIN HTML OUTPUT */
?>
  <div class="block-book-details">
    …

    <div class="book-details-quotes">
    <?php
      foreach($book_details_quotes as $quote) :
    ?>
      <blockquote class="book-details-quote-<?php echo $quote['id']; ?> book-details-quote">
        <?php echo $quote['content']; ?>
        <cite><?php echo $quote['pageRef']; ?></cite>
      </blockquote>
    <?php
      endforeach;
    ?>
    </div>

    …
  </div>
…