In a previous guide, we explored how to build a WordPress Gutenberg block plugin from scratch. It was a fantastic journey that allowed us to create a block plugin with basic functionality. However, as with any project, there’s always room for improvement and refinements.
Below is the plugin that we will be creating:
A Rea
WordP
Displ
Displ
Displ
Displ
Displ
In this guide, we will revisit our block plugin and refine it, considering feedback from the community and incorporating new techniques. We’ll also rectify some omissions from our previous guide. The purpose is to equip you with an even better understanding of building Gutenberg block plugins, ensuring you have the skills to create robust, versatile, and user-friendly blocks.
Our main goal is to create a block that displays a shape, and when clicked, it reveals a modal with content from a selected post. In this process, we’ll leverage the native HTML dialog
element for modals, improving SEO and reducing our codebase. We’ll also be optimizing our JavaScript files, creating a more efficient and readable code.
Acknowledging Initial Changes and Previous Mistakes
Before we delve into the code, it’s important to acknowledge a crucial step that was overlooked in our initial guide: passing post data to the localize-script.js
file. This step is essential for our block to function correctly as it allows our JavaScript to access server-side data. Without it, we can’t pull the required data from our WordPress site into our JavaScript code, resulting in a non-functional block.
In the initial guide, we used the Tingle.js library to create the modal that displays the post content. After considering feedback and re-evaluating our approach, we decided to replace Tingle.js with the HTML dialog
element. This decision was influenced by several factors:
- Better SEO: Search engines better understand native HTML elements, which can positively impact your site’s SEO.
- More Compact Code: By using a native HTML element, we can eliminate the additional JavaScript library, reducing our overall codebase.
- Improved User Experience: The HTML
dialog
element provides a more native and seamless experience for users across different browsers and devices.
Let’s now explore how these changes and improvements are implemented in our Gutenberg block plugin. We’ll look at the main files that make up our plugin, explain their purpose, and delve into their code.
Setting Up the Initial Plugin Structure
To kick things off, we’ll start by setting up our initial plugin structure. Our project will utilize the @wordpress/create-block
package, a zero-configuration #0CJS (Zero Configuration JavaScript) tool that helps you build block plugins with ease.
To create our initial block structure, run the following command in your terminal:
npx @wordpress/create-block abel-block
This command will create a new directory named abel-block
with all the files and configurations necessary to start building our block, further details can still be found in my previous article. The generated files include:
block.json
: This file contains metadata for the block.index.js
: This is the entry point for our block’s JavaScript.edit.js
: This file contains the logic for the block’s edit function.save.js
: This file contains the logic for the block’s save function.style.scss
: This file contains the block’s frontend styles.editor.scss
: This file contains the block’s editor-specific styles.
With our initial block structure ready, let’s move onto refining our block.
Refining Our Block: The Edit Function
The edit
function in our block is responsible for controlling what is displayed in the Gutenberg editor when our block is being used. In our plugin, we’ve defined a class AbelDisplayEdit
that extends Component
from @wordpress/element
. Let’s walk through each section of this class to understand what’s happening.
The constructor and event handlers
The constructor is standard, just calling the parent constructor and binding our event handlers:
constructor() { super( ...arguments ); this.handleCategoryChange = this.handleCategoryChange.bind( this ); this.handleTagChange = this.handleTagChange.bind( this ); this.handleDisplayStyleChange = this.handleDisplayStyleChange.bind( this ); }
We have event handlers for when categories, tags, and display style change. Each handler updates the block attributes with the new value.
componentDidMount and componentDidUpdate
In componentDidMount
, we fetch categories and tags if they aren’t already available:
componentDidMount() { const { categories, tags, setAttributes } = this.props; if ( ! categories ) { apiFetch( { path: '/wp/v2/categories?_fields=id,name' } ) .then( ( response ) => response.json() ) .then( ( fetchedCategories ) => { setAttributes( { fetchedCategories } ); } ); } if ( ! tags ) { apiFetch( { path: '/wp/v2/tags?_fields=id,name' } ) .then( ( response ) => response.json() ) .then( ( fetchedTags ) => { setAttributes( { fetchedTags } ); } ); } }
componentDidUpdate
checks whether the posts
prop has changed. If it has, it updates the posts
attribute of our block.
componentDidUpdate( prevProps ) { const { posts, setAttributes } = this.props; if ( posts !== prevProps.posts ) { setAttributes( { posts } ); } }
The render method
The render
method is responsible for returning the JSX that represents our block in the Gutenberg editor.
render() { const { posts, displayStyle, imageSize } = this.props.attributes; if ( ! posts || posts.length === 0 ) { return ( <Fragment> <div className="abel-wrapper"> { __( 'No posts found. Please select some categories and tags.' ) } </div> </Fragment> ); } // ... more code here ... }
We start by checking if we have posts. If not, we return a message to the user asking them to select some categories and tags. If we do have posts, we map over them, creating a .shape
div for each one. Inside each div, we have a dialog
element that shows the post’s title, featured image, and content. We also have a close button that, when clicked, will close the dialog.
We have a custom function getFeaturedOrFirstImage
that fetches the featured image or the first image from the post content, which is used for rendering the image inside each post’s dialog.
The render
method ultimately returns a div
that contains all the post dialogs. If there are no posts, a message indicating this is displayed.
The withSelect
and withColors
functions are used as higher-order components (HOCs) that wrap our AbelDisplayEdit
component. withSelect
allows us to select data from the WordPress store, and withColors
provides us with attributes and setters for the selected color values.
In our case, withSelect
is used to fetch posts that match the selected categories and tags, and withColors
is not used but was included for future color customization features.
This detailed approach to building the edit
function provides us with a flexible and interactive way to create and manage our block within the Gutenberg editor. Next, we’ll look at how we handle the saving of the block’s content.
Deep Dive into the Edit.js File: Part 2
In the previous section, we’ve covered the major aspects of our edit.js
file, from the import statements to the constructor and the lifecycle methods componentDidMount
and componentDidUpdate
. Now, let’s move onto the render
method, which is responsible for the actual visual output of our block in the Gutenberg editor.
The render
method begins by destructuring posts
, displayStyle
, and imageSize
from the component’s props. If no posts are available, it returns a message prompting the user to select some categories and tags.
return ( <div className={ 'abel-display-posts abel-display-style-' + displayStyle } > { posts && posts.length > 0 ? ( <div className="abel-wrapper" data-configuration="1" data-roundness="1" > { posts.map( ( post ) => { // ... } ) } </div> ) : ( <Fragment> <div className="abel-wrapper"> { __( 'No posts found.' ) } </div> </Fragment> ) } </div> );
If there are posts available, a div
with the class abel-wrapper
is returned, which contains a mapped array of the available posts. The map
function is used to iterate over each post and return a JSX representation of it.
{ posts.map( ( post ) => { const featuredImage = getFeaturedOrFirstImage( post, imageSize ); if ( featuredImage && ! featuredImage.alt ) { featuredImage.alt = post.title.rendered; } return ( <div className="shape" key={ post.id }> <dialog> <h2>{ post.title.rendered }</h2> <img src={ featuredImage.url } alt={ featuredImage.alt } width={ featuredImage.width } height={ featuredImage.height } loading="lazy" /> <div dangerouslySetInnerHTML={ { __html: post.content.rendered, } } /> <button className="close-button"> { __( 'Close' ) } </button> </dialog> { post.title.rendered } </div> ); } )}
Within the map
function, for each post, we first get the featuredImage
using our getFeaturedOrFirstImage
helper function. If the featured image doesn’t have an alt attribute, we set it to the post’s title.
We then return a div
for each post, which includes a dialog
element. This dialog
contains the post’s title, the featured image, and the post’s content. There’s also a close button to close the dialog.
A quick word about the dangerouslySetInnerHTML
In React, the dangerouslySetInnerHTML
prop is used to insert raw HTML into a component. This is similar to using innerHTML
in vanilla JavaScript.
The reason it is named so—prefixed with ‘dangerously’—is because it’s a way to signal to developers that they need to be aware of the potential risks of using this feature. The primary risk is Cross-Site Scripting (XSS) attacks, where an attacker might be able to inject malicious scripts into your web page via the HTML content. This can lead to various issues, from data theft to site defacement.
Normally, React escapes all variables inserted in the JSX to prevent this kind of attack. By using dangerouslySetInnerHTML
, you’re bypassing this default protection and need to ensure on your own that the inserted HTML is safe.
In the context of our Gutenberg block, we’re using dangerouslySetInnerHTML
to insert the post’s content into the dialog. This content is fetched from the WordPress database and it might contain HTML tags, hence the necessity for this prop.
In our case, it’s reasonably safe to use dangerouslySetInnerHTML
because:
- WordPress already applies a set of content filters when saving post content to the database, which helps prevent malicious code from being saved.
- The content we are inserting is from the post’s own data, and isn’t directly inputted by users in this context. In WordPress, users who have the ability to create or edit posts generally have trusted roles (like Administrators or Editors). This reduces the risk of malicious content, as these users typically have a higher level of responsibility and trust.
- WordPress itself uses a similar method to insert post content when rendering a post on the front end.
However, it’s always a good idea to keep the potential risks in mind. If there was a situation where user-generated content from untrusted sources was being inserted, additional sanitization or other protective measures would be necessary.
This should give a comprehensive understanding of the edit.js
file in our Gutenberg block plugin. In the next section, we’ll dive into the getFeaturedOrFirstImage.js
utility function, which plays a crucial role in retrieving the featured image for each post.
Understanding the getFeaturedOrFirstImage.js File in the /utils/ Folder
The getFeaturedOrFirstImage.js
file is a utility function that fetches either the featured image or the first image of the post. This function is essential to our Gutenberg block. We’ve placed this function in a /utils/
folder as a way to organize our code better.
Here is the revised code in getFeaturedOrFirstImage.js
:
export const getFeaturedOrFirstImage = ( post, imageSize ) => { if ( post._embedded && post._embedded[ 'wp:featuredmedia' ] && post._embedded[ 'wp:featuredmedia' ][ 0 ].media_details && post._embedded[ 'wp:featuredmedia' ][ 0 ].media_details.sizes ) { const sizes = post._embedded[ 'wp:featuredmedia' ][ 0 ].media_details.sizes; let image = sizes[ imageSize ]; // If the desired image size doesn't exist, fall back to 'full'. if ( ! image && sizes.full ) { image = sizes.full; } // If 'full' size doesn't exist, fall back to the first available size. if ( ! image ) { const availableSizes = Object.values( sizes ); image = availableSizes[ 0 ]; } if ( image ) { return { url: image.source_url, width: image.width, height: image.height, alt: post._embedded[ 'wp:featuredmedia' ][ 0 ].alt_text, }; } } const content = post.content ? post.content.rendered : null; const imgRegex = /<img[^>]+src="(http:\/\/[^">]+|https:\/\/[^">]+)"/g; const match = imgRegex.exec( content ); if ( match && match[ 1 ] ) { return { url: match[ 1 ], width: null, height: null, alt: null }; } return null; };
This function first checks if the post object has a featured image. If it does, it fetches the image details in the specified size.
In case the desired image size isn’t available, the function falls back to the ‘full’ image size. And, if the ‘full’ size is also not available, it falls back to the first available size.
The function returns an object with the image’s url
, width
, height
, and alt
text if the featured image exists.
If the post doesn’t have a featured image, the function proceeds to extract the first image from the post content. It uses a regular expression to find the first <img>
tag and extracts its src
attribute.
If an image is found, the function returns an object with the image url
and null
for the width
, height
, and alt
attributes, as these details are not available from the content.
If no featured or first image is found, the function returns null
.
This utility function ensures that we always have an image to display for each post, enriching the visual appeal of our block. In the next section, we’ll explore the useImageSizes.js
file and its role in the plugin.
Understanding the useImageSizes.js File in the /utils/ Folder
The useImageSizes.js
file is another utility function we’re using in our plugin. The purpose of this function is to fetch and make use of the image sizes of the post. This utility function is essential for our Gutenberg block. It is placed in the /utils/
folder to keep the code organized.
Here’s the useImageSizes.js
code:
import { useState, useEffect } from '@wordpress/element'; const useImageSizes = () => { const [ imageSizes, setImageSizes ] = useState( [] ); useEffect( () => { if ( window.wpImageSizes ) { setImageSizes( Object.keys( window.wpImageSizes ) ); } else { setImageSizes( [] ); } }, [] ); return imageSizes; }; export default useImageSizes;
In this function, we’re using the React Hooks useState
and useEffect
from @wordpress/element
to manage the state of our image sizes.
We initialize the imageSizes
state with an empty array using useState([])
. This will store our image sizes once they are fetched.
In the useEffect
hook, we check if window.wpImageSizes
is available. window.wpImageSizes
is an object where we store the available image sizes in the WordPress backend. If it is available, we fetch the keys of this object (which are the names of the image sizes) using Object.keys(window.wpImageSizes)
, and then update the imageSizes
state with these keys using setImageSizes
.
If window.wpImageSizes
is not available, we set the imageSizes
state to an empty array.
The useEffect
hook is run once after the component is mounted, due to the empty array []
as its second argument. This means that we only fetch the image sizes once, when the component is first rendered.
Finally, we return the imageSizes
state from our custom hook, so it can be used in the components that import this function.
This function gives us the flexibility to work with different image sizes available in the WordPress installation. This is crucial for providing a better user experience, as users can select the appropriate image size for their needs.
Passing the Featured Image Data to the Component
While we’ve discussed a lot about the JavaScript and CSS that powers our block, it’s important to remember that WordPress is primarily a PHP platform. To fully harness the power of WordPress and our block, we need to make sure our JavaScript code can interact with the PHP backend of WordPress.
In the case of our block, this interaction is crucial for fetching and handling the featured image data of the posts. Let’s see how we can achieve this.
To pass data from PHP to JavaScript in WordPress, we use the wp_localize_script
function. This function allows us to make any PHP data available to a specific script as a JavaScript object.
Here’s the code we use in abel-display.php
to register and enqueue our script, as well as to pass the image size data from PHP to JavaScript:
function enqueue_block_editor_assets() { $image_sizes = array(); foreach ( get_intermediate_image_sizes() as $size ) { $image_sizes[ $size ] = $size; } wp_register_script( 'localize-script', plugins_url( 'abel-display/localize-script.js', __DIR__ ), array(), filemtime( plugin_dir_path( __DIR__ ) . 'abel-display/localize-script.js' ), true ); wp_localize_script( 'localize-script', 'wpImageSizes', $image_sizes ); wp_enqueue_script( 'localize-script' ); } add_action( 'enqueue_block_editor_assets', 'enqueue_block_editor_assets' );
Let’s break down what this code does:
- We start by defining a function
enqueue_block_editor_assets
. This function will be hooked to the WordPress actionenqueue_block_editor_assets
, which fires when the block editor assets (scripts and styles) are enqueued. - Inside this function, we first prepare an array
$image_sizes
that will hold the available image sizes in WordPress. - We then loop through the result of the function
get_intermediate_image_sizes
, which returns all intermediate image sizes (like ‘thumbnail’, ‘medium’, ‘medium_large’, ‘large’), and add them to our$image_sizes
array. - Next, we register our
localize-script.js
usingwp_register_script
. Theplugins_url
function is used to set the correct URL for our script. Thefilemtime
function is used to version our script based on its last modification time, which is a good practice to avoid serving stale cached versions of the script. - Now comes the crucial part. We use
wp_localize_script
to pass our$image_sizes
array to ourlocalize-script.js
. The first parameter is the handle of the script we registered (localize-script
), the second parameter is the name of the JavaScript object that will hold our data (wpImageSizes
), and the third parameter is the data itself ($image_sizes
). - With
wp_enqueue_script
, we then enqueue ourlocalize-script.js
to load it in the block editor. - Finally, we hook our function to the
enqueue_block_editor_assets
action.
This way, we pass the image size data from PHP to JavaScript, and we can access it in any script in the block editor via the wpImageSizes
JavaScript object. This object is then used in useImageSizes.js
to fetch and utilize the image sizes of the post.
Note: please don’t forget to place an empty js file in /src/
called localize-script.js
for the data to be passed to the component.
In the next section, we will delve into the save.js
file and its role in the plugin.
Deep Dive into the save.js File
The save.js
file is a critical component of our Gutenberg block. It is responsible for defining how the block’s content is saved to the database, and then rendered on the front end.
Here’s the save.js
code:
import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getFeaturedOrFirstImage } from './utils/getFeaturedOrFirstImage'; const AbelDisplaySave = ( { attributes } ) => { const { posts, displayStyle, imageSize } = attributes; if ( posts.length === 0 ) { return null; } return ( <div className={ `abel-display-posts abel-display-style-${ displayStyle }` } > { posts && posts.length > 0 ? ( <div className="abel-wrapper" data-configuration="1" data-roundness="1" > { posts.map( ( post ) => { const featuredImage = getFeaturedOrFirstImage( post, imageSize ); if ( featuredImage && ! featuredImage.alt ) { featuredImage.alt = post.title.rendered; } return ( <div className="shape" key={ post.id }> <dialog> <h2>{ post.title.rendered }</h2> <img src={ featuredImage.url } alt={ featuredImage.alt } width={ featuredImage.width } height={ featuredImage.height } loading="lazy" /> <div dangerouslySetInnerHTML={ { __html: post.content.rendered, } } /> <button className="close-button"> { __( 'Close' ) } </button> </dialog> { post.title.rendered } </div> ); } ) } </div> ) : ( <Fragment> <div className="abel-wrapper"> { __( 'No posts found.' ) } </div> </Fragment> ) } </div> ); }; export default AbelDisplaySave;
Let’s walk through this code:
Firstly, we destructure posts
, displayStyle
, and imageSize
from the block attributes. These attributes represent the posts to display, the chosen style for the display, and the selected image size, respectively.
If there are no posts (posts.length === 0
), the function returns null
and doesn’t render anything.
Then, we return a <div>
element with a dynamic class name that reflects the selected displayStyle
.
Inside this <div>
, we map through the posts
array and for each post, we fetch the featured image or the first image of the post using the getFeaturedOrFirstImage
function imported from our /utils/
folder.
We set the alt
attribute of the image to the post’s title if it doesn’t already have one. This is important for accessibility and SEO.
Then, we return a <div>
with the class name “shape” and a unique key
attribute that corresponds to the post’s ID. Inside this <div>
, we create a <dialog>
element that contains the post’s title, the featured image, and the post’s content.
The post’s content is inserted using the dangerouslySetInnerHTML
property, which is safe in this case because the content comes from trusted WordPress posts, which are already sanitized for malicious code.
Finally, we add a “Close” button for closing the modal.
If there are no posts to display, we show a message “No posts found.”
With this save.js
file, we effectively instruct WordPress how to save our block’s content to the database and how to render it on the front end.
Next, we’ll discuss the CSS used to style our block.
Explaining the CSS Styling
Styling plays a crucial role in giving our block its unique look and feel. In our case, we’re using SCSS (Sassy CSS), a preprocessor scripting language that is interpreted into CSS. SCSS extends the capabilities of CSS with features like variables, nested rules, and mixins, among others.
Here’s the explanation of our SCSS file, style.scss
:
// We define multiple variables (maps) for different color schemes, block configurations, and roundness. $colors: ( // ...color definitions... ); $configurations: ( // ...configuration definitions... ); $roundness: ( // ...roundness definitions... ); $roundnessShapes: ( // ...roundness shape definitions... ); $roundnessDefault: ( // ...default roundness definitions... ); // Our main class for the block. .abel-display-posts { // ...styling for the main class... .abel-wrapper { // ...styling for the inner wrapper... dialog { // ...styling for the dialog element... } dialog[open] { // ...styling for the open dialog element... } > .shape { // ...styling for each shape inside the block... a { // ...styling for the link inside each shape... } // We apply different colors to each shape using the $colors map. @for $i from 1 through length($colors) { &:nth-child(#{$i}) { background-color: map-get($colors, $i); z-index: 2; } } } // We apply different configurations to the shapes depending on the data-configuration attribute using the $configurations map. @each $configuration, $shapes in $configurations { &[data-configuration="#{$configuration}"] { > .shape { @each $shape, $styles in $shapes { &:nth-child(#{$shape}) { @each $property, $value in $styles { #{$property}: $value; } } } } } } // We apply different roundness to the shapes depending on the data-roundness attribute using the $roundness map. // ...similar code for $roundness, $roundnessShapes, and $roundnessDefault... } }
The SCSS file heavily utilizes SCSS features such as variables (maps) and control directives (@each
and @for
).
The $colors
map is used to apply different background colors to the shapes in the block. The $configurations
map is used to apply different configurations (positioning and sizes) to the shapes based on the data-configuration
attribute.
The $roundness
, $roundnessShapes
, and $roundnessDefault
maps are used to apply different border radius values to the shapes based on the data-roundness
attribute.
Overall, the styling contributes to the dynamic and interactive nature of our block, allowing it to change its appearance based on the configuration and roundness attributes.
In the next section, let’s dive into the animate.js
file.
Animating the Display Block and Handling User Interactions with JavaScript
// Function to generate a unique random number in a range const uniqueRand = ( min, max, prev ) => { let next = prev; while ( prev === next ) next = Math.floor( Math.random() * ( max - min + 1 ) + min ); return next; }; const combinations = [ { configuration: 1, roundness: 1 }, { configuration: 1, roundness: 2 }, { configuration: 1, roundness: 4 }, { configuration: 2, roundness: 2 }, { configuration: 2, roundness: 3 }, { configuration: 3, roundness: 3 }, ]; let prev = 0; let intervalId; // Function to start the animation const startAnimation = ( wrappers ) => { if ( intervalId !== undefined ) return; intervalId = setInterval( () => { wrappers.forEach( ( wrapper ) => { const index = uniqueRand( 0, combinations.length - 1, prev ), // Destructure the combination object { configuration, roundness } = combinations[ index ]; wrapper.dataset.configuration = configuration; wrapper.dataset.roundness = roundness; prev = index; } ); }, 3000 ); }; // Define a function to start the animation on your wrappers function startAnimationOnWrappers( wrappers ) { startAnimation( wrappers ); } // Create a new MutationObserver instance const observer = new MutationObserver( ( mutationsList ) => { for ( const mutation of mutationsList ) { if ( mutation.addedNodes.length ) { const newWrappers = document.querySelectorAll( '.abel-wrapper' ), shapes = document.querySelectorAll( '.shape' ); // Start animation on wrappers newWrappers.forEach( () => { startAnimationOnWrappers( newWrappers ); } ); // Attach events to shapes shapes.forEach( ( shape ) => { if ( ! shape.dataset.eventAttached ) { const dialog = shape.querySelector( 'dialog' ); shape.addEventListener( 'click', () => { if ( dialog.open ) dialog.close(); else dialog.showModal(); } ); const closeButton = dialog.querySelector( '.close-button' ); closeButton.addEventListener( 'click', ( event ) => { event.stopPropagation(); dialog.close(); } ); shape.dataset.eventAttached = true; } } ); } } } ); document.addEventListener( 'DOMContentLoaded', () => { observer.observe( document.body, { childList: true, subtree: true } ); } );
The animate.js
file is responsible for animating the block and handling user interactions with the shapes. Here’s a breakdown of the code:
uniqueRand()
function: This function generates a unique random number within a given range that is different from the previously generated number. It takes three arguments:min
,max
, andprev
.min
andmax
define the range, andprev
is the previously generated number.combinations
array: It is an array of objects that represent different combinations ofconfiguration
androundness
. These combinations will be applied to the block during the animation.prev
andintervalId
: These variables store the previous index of the combinations array and the interval ID fromsetInterval()
, respectively.startAnimation()
function: This function starts the animation by changing theconfiguration
androundness
of the block every 3 seconds (3000 milliseconds). It iterates through the givenwrappers
and updates theirdata-configuration
anddata-roundness
attributes with a unique random index from thecombinations
array.startAnimationOnWrappers()
function: This function calls thestartAnimation()
function with the givenwrappers
as an argument.- The
MutationObserver
instance is created to observe changes in the DOM. When new nodes are added to the DOM, the observer checks for new.abel-wrapper
and.shape
elements, starts the animation on the new wrappers, and attaches event listeners to the new shapes. The event listeners handle clicks on the shapes and close buttons within the dialogs. - The
DOMContentLoaded
event listener is added to the document, which starts observing the body of the document for changes in the DOM using theMutationObserver
instance when the DOM content is loaded.
This script is responsible for animating the block’s appearance and handling user interactions with the block. The block’s configuration and roundness change every 3 seconds, giving it a dynamic and interactive feel.
Customizing Block’s Sidebar with InspectorControls
In our block, we have incorporated InspectorControls
to offer a more customized user interface in the block’s settings sidebar.
InspectorControls
is a special component imported from the ‘@wordpress/block-editor’ module. It acts as a container for controls that will be displayed in the block’s settings sidebar when the block is selected.
The code has been placed in the index.js
to make it available in the sidebar, rather than the editor controls, this is simply a decision to be made rather a ‘best practice’.
import { registerBlockType } from '@wordpress/blocks'; import './style.scss'; /** * Internal dependencies */ import Edit from './edit'; import save from './save'; import metadata from './block.json'; import useImageSizes from './utils/useImageSizes'; /** * WordPress dependencies */ import { InspectorControls } from '@wordpress/block-editor'; import { PanelBody, SelectControl, RangeControl } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; /** * This function is a Higher Order Component that wraps the original block edit component. * It provides additional props to the wrapped component, which include the categories and tags * from the WordPress REST API. * * @param {Function} OriginalEdit The original edit component to wrap. * @return {Function} The wrapped edit component. */ const withCategoriesAndTags = withSelect( ( select ) => { const { getEntityRecords } = select( 'core' ); return { categories: getEntityRecords( 'taxonomy', 'category', { per_page: -1, } ), tags: getEntityRecords( 'taxonomy', 'post_tag', { per_page: -1 } ), }; } ); /** * Every block starts by registering a new block type definition. * * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ */ registerBlockType( metadata.name, { /** * Define block attributes. */ attributes: { category: { type: 'array', default: [], }, tag: { type: 'array', default: [], }, posts: { type: 'array', default: [], }, displayStyle: { type: 'string', default: 'default', }, imageSize: { type: 'string', default: 'medium', }, numberposts: { type: 'number', default: 5, }, }, /** * @see ./edit.js */ edit: withCategoriesAndTags( ( props ) => { const { categories, tags, setAttributes } = props; if ( ! categories || ! tags ) { return 'Loading...'; } const imageSizes = useImageSizes(); return ( <> <InspectorControls> <PanelBody title="Filter Posts"> <SelectControl label="Category" multiple={ true } value={ props.attributes.category } options={ categories.map( ( category ) => ( { label: category.name, value: category.id, } ) ) } onChange={ ( value ) => setAttributes( { category: value } ) } /> <SelectControl label="Tag" multiple={ true } value={ props.attributes.tag } options={ tags.map( ( tag ) => ( { label: tag.name, value: tag.id, } ) ) } onChange={ ( value ) => setAttributes( { tag: value } ) } /> <RangeControl label={ 'Number of posts to display' } value={ props.attributes.numberposts } onChange={ ( value ) => setAttributes( { numberposts: value } ) } min={ 1 } max={ 20 } /> <SelectControl label="Image Size" value={ props.attributes.imageSize } options={ imageSizes && imageSizes.length > 0 ? imageSizes.map( ( size ) => ( { label: size, value: size, } ) ) : [] } onChange={ ( value ) => setAttributes( { imageSize: value } ) } /> <SelectControl label="Display Style" value={ props.attributes.displayStyle } options={ [ { label: 'Default', value: 'default' }, { label: 'List', value: 'list' }, { label: 'Grid', value: 'grid' }, ] } onChange={ ( value ) => setAttributes( { displayStyle: value } ) } /> </PanelBody> </InspectorControls> <Edit { ...props } /> </> ); } ), /** * @see ./save.js */ save, } );
Here, we’ve included several control components within InspectorControls
:
- SelectControl for selecting categories and tags: This allows users to filter the posts based on selected categories and tags. The
SelectControl
component is set to allow multiple selections, providing a more flexible filter for the posts. - RangeControl for determining the number of posts: This offers a slider for users to easily adjust the number of posts to be displayed.
- SelectControl for setting the image size: This provides a dropdown menu with different image sizes to choose from.
- SelectControl for choosing the display style: Users can select how they want the posts to be displayed. Options include ‘Default’, ‘List’, and ‘Grid’.
Each control component is associated with an onChange
function that updates the block’s attributes whenever a change is made to the control. This is done using the setAttributes
function provided by the block’s props
.
Furthermore, we’ve used a Higher Order Component (HOC) withSelect
to fetch categories and tags from the WordPress REST API. This is used to populate the options for the category and tag SelectControl
components.
By using InspectorControls
, we have given users a way to customize the appearance and content of the block in a user-friendly manner. In the next section, we will learn about block validation and how to ensure that changes made to the block’s content and attributes are valid.
Summary and Future Directions
In this guide, we have walked through the process of building a dynamic WordPress block plugin using the Block Editor (Gutenberg) and the WordPress REST API. We covered the basics of setting up the plugin, creating the block, fetching data from the WordPress REST API, and dynamically rendering the block in the editor and on the front end.
The project, hosted on GitHub, demonstrates the effective usage of these techniques. You have successfully implemented a Gutenberg block that can be configured to display posts with different categories, tags, and other characteristics. The use of image sizes in your project is particularly noteworthy, as it provides a granular control over the appearance of the displayed posts.
While this project serves as a solid foundation, there is always room for further enhancements. Here are a few potential areas of focus:
- Testing: It is important to ensure the robustness and reliability of your code. Automated tests can help detect errors and regressions early in the development cycle. You might consider using testing frameworks like Jest for JavaScript and PHPUnit for PHP.
- Documentation: A well-documented project is easier to understand and contribute to. Consider creating a
README.md
file detailing the purpose of the project, its dependencies, how to install it, and how to use it. ATODO.md
file, on the other hand, could help track planned enhancements or features. - Community Engagement: Open-source projects thrive on community involvement. Inviting other developers to contribute to your project not only helps improve the codebase, but it also fosters a community around your project. Encourage others to fork your repository, add features, fix bugs, and submit pull requests.
Your project on GitHub is open for collaboration. Whether you’re a novice or an experienced developer, any contributions to the codebase are welcome. Feel free to explore the code, propose changes, or leave comments. Together, we can make this project even better.
Remember, the open-source community thrives on collaboration and contributions from developers like you. Your feedback, code contributions, and suggestions are always welcome!