Create Your Own Dynamic Gutenberg Block for Wordpress, Part 2

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.

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:

js

const { MediaUpload, MediaUploadCheck, RichText } = wp.blockEditorconst { 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:

php

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:

js

const { InspectorControls } = wp.blockEditorconst { 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:

php

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 nanoid 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 nanoid package to create ids (yarn add nanoid).
  • 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:

js

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: nanoid(), 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:

php

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 if ($book_details_quotes) : 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; endif; ?> </div>

</div>

Update 11/25/20

I added an if conditional for the foreach($book_details_quotes as $quote) loop to prevent a warning from appearing when $book_details_quotes does not exist. Also, replaced the deprecated shortid package with nanoid.