Tag: Editor

Using Markdown and Localization in the WordPress Block Editor

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

Since the block editor is based on React, we may be tempted to use React components and HTML code for the documentation. That is the approach I followed in my previous article, which demonstrated a way to show documentation in a modal window.

But this solution is not flawless, because adding documentation through React components and HTML code could become very verbose, not to mention difficult to maintain. For instance, the modal from the image above contains the documentation in a React component like this:

const CacheControlDescription = () => {   return (     <p>The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or <code>no-store</code> if the max-age is 0</p>   ) }

Using Markdown instead of HTML can make the job easier. For instance, the documentation above could be moved out of the React component, and into a Markdown file like /docs/cache-control.md:

The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or `no-store` if the max-age is 0

What are the benefits and drawbacks of using Markdown compared to pure HTML?

Advantages Disadvantages
✅ Writing Markdown is easier and faster than HTML ❌ The documentation cannot contain React components
✅ The documentation can be kept separate from the block’s source code (even on a separate repo) ❌ We cannot use the __ function (which helps localize the content through .po files) to output text
✅ Copy editors can modify the documentation with no fear of breaking the code
✅ The documentation code isn’t added to the block’s JavaScript asset, which can then load faster

Concerning the drawbacks, not being able to use React components may not be a problem, at least for simple documentation. The lack of localization, however, is a major issue. Text in the React component added through the JavaScript __ function can be extracted and replaced using translations from POT files. Content in Markdown cannot access this functionality.

Supporting localization for documentation is mandatory, so we will need to make up for it. In this article we will pursue two goals:

  • Using Markdown to write documentation (displayed by a block of the WordPress editor)
  • Translating the documentation to the user’s language

Let’s start!

Loading Markdown content

Having created a Markdown file /docs/cache-control.md, we can import its content (already rendered as HTML) and inject it into the React component like this:

import CacheControlDocumentation from '../docs/cache-control.md'; 
 const CacheControlDescription = () => {   return (     <div       dangerouslySetInnerHTML={ { __html: CacheControlDocumentation } }     />   ); }

This solution relies on webpack, the module bundler sitting at the core of the WordPress editor.

Please notice that the WordPress editor currently uses webpack 4.42, However, the documentation shown upfront on webpack’s site corresponds to version 5 (which is still in beta). The documentation for version 4 is located at a subsite.

The content is transformed from Markdown to HTML via webpack’s loaders, for which the block needs to customize its webpack configuration, adding the rules to use markdown-loader and html-loader.

To do this, add a file, webpack.config.js, at the root of the block with this code:

// This is the default webpack configuration from Gutenberg const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 
 // Customize adding the required rules for the block module.exports = {   ...defaultConfig,   module: {     ...defaultConfig.module,     rules: [       ...defaultConfig.module.rules,       {         test: /.md$ /,         use: [           {             loader: "html-loader"           },           {             loader: "markdown-loader"           }         ]       }     ],   }, };

And install the corresponding packages:

npm install --save-dev markdown-loader html-loader

Let’s apply one tiny enhancement while we’re at it. The docs folder could contain the documentation for components located anywhere in the project. To skip having to calculate the relative path from each component to that folder, we can add an alias, @docs, in webpack.config.js to resolve to folder /docs:

const path = require( 'path' ); config.resolve.alias[ '@docs' ] = path.resolve( process.cwd(), 'docs/' )

Now, the imports are simplified:

import CacheControlDocumentation from '@docs/cache-control.md';

That’s it! We can now inject documentation from external Markdown files into the React component.

Translating the documentation to the user’s language

We can’t translate strings through .po files for Markdown content, but there is an alternative: produce different Markdown files for different languages. Then, instead of having a single file (/docs/cache-control.md), we can have one file per language, each stored under the corresponding language code:

  • /docs/en/cache-control.md
  • /docs/fr/cache-control.md
  • /docs/zh/cache-control.md
  • etc.

We could also support translations for both language and region, so that American and British English can have different versions, and default to the language-only version when a translation for a region is not provided (e.g. "en_CA" is handled by "en"):

  • /docs/en_US/cache-control.md
  • /docs/en_GB/cache-control.md
  • /docs/en/cache-control.md

To simplify matters, I’ll only explain how to support different languages, without regions. But the code is pretty much the same.

The code demonstrated in this article can also be seen in the source code of a WordPress plugin I made.

Feeding the user’s language to the block

The user’s language in WordPress can be retrieved from get_locale(). Since the locale includes the language code and the region (such as "en_US"), we parse it to extract the language code by itself:

function get_locale_language(): string  {   $ localeParts = explode( '_', get_locale() );   return $ localeParts[0]; }

Through wp_localize_script(), we provide the language code to the block, as the userLang property under a global variable (which, in this case, is graphqlApiCacheControl):

// The block was registered as $ blockScriptRegistrationName wp_localize_script(   $ blockScriptRegistrationName,   'graphqlApiCacheControl',   [     'userLang' => get_locale_language(),   ] );

Now the user’s language code is available on the block:

const lang = window.graphqlApiCacheControl.userLang; 

Dynamic imports

We can only know the user’s language at runtime. However, the import statement is static, not dynamic. Hence, we cannot do this:

// `lang` contains the user's language import CacheControlDocumentation from '@docs/$ { lang }/cache-control.md';

That said, webpack allows us to dynamically load modules through the import function which, by default, splits out the requested module into a separate chunk (i.e. it is not included within the main compiled build/index.js file) to be loaded lazily.

This behavior is suitable for showing documentation on a modal window, which is triggered by a user action and not loaded up front. import must receive some information on where the module is located, so this code works:

import( `@docs/$ { lang }/cache-control.md` ).then( module => {   // ... });

But this seemingly similar code does not:

const dynamicModule = `@docs/$ { lang }/cache-control.md` import( dynamicModule ).then( module => {   // ... });

The content from the file is accessible under key default of the imported object:

const cacheControlContent = import( `@docs/$ { lang }/cache-control.md` ).then( obj => obj.default )

We can generalize this logic into a function called getMarkdownContent, passing the name of the Markdown file alongside the language:

const getMarkdownContent = ( fileName, lang ) => {   return import( `@docs/$ { lang }/$ { fileName }.md` )     .then( obj => obj.default ) } 

Managing the chunks

To keep the block assets organized, let’s keep the documentation chunks grouped in the /docs subfolder (to be created inside the build/ folder), and give them descriptive file names.

Then, having two docs (cache-control.md and cache-purging.md) in three languages (English, French and Mandarin Chinese), the following chunks will be produced:

  • build/docs/en-cache-control-md.js
  • build/docs/fr-cache-control-md.js
  • build/docs/zh-cache-control-md.js
  • build/docs/en-cache-purging-md.js
  • build/docs/fr-cache-purging-md.js
  • build/docs/zh-cache-purging-md.js

This is accomplished by using the magic comment /* webpackChunkName: "docs/[request]" */ just before the import argument:

const getMarkdownContent = ( fileName, lang ) => {   return import( /* webpackChunkName: "docs/[request]" */ `@docs/$ { lang }/$ { fileName }.md` )     .then(obj => obj.default) } 

Setting the public path for the chunks

webpack knows where to fetch the chunks, thanks to the publicPath configuration option. If it’s not provided, the current URL from the WordPress editor, /wp-admin/, is used, producing a 404 since the chunks are located somewhere else. For my block, they are under /wp-content/plugins/graphql-api/blocks/cache-control/build/.

If the block is for our own use, we can hardcode publicPath in webpack.config.js, or provide it through an ASSET_PATH environment variable. Otherwise, we need to pass the public path to the block at runtime. To do so, we calculate the URL for the block’s build/ folder:

$ blockPublicPath = plugin_dir_url( __FILE__ ) . '/blocks/cache-control/build/';

Then we inject it to the JavaScript side by localizing the block:

// The block was registered as $ blockScriptRegistrationName wp_localize_script(     $ blockScriptRegistrationName,     'graphqlApiCacheControl',     [       //...       'publicPath' => $ blockPublicPath,     ] );

And then we provide the public path to the __webpack_public_path__ JavaScript variable:

__webpack_public_path__ = window.graphqlApiCacheControl.publicPath;

Falling back to a default language

What would happen if there is no translation for the user’s language? In that case, calling getMarkdownContent will throw an error.

For instance, when the language is set to German, the browser console will display this:

Uncaught (in promise) Error: Cannot find module './de/cache-control.md'

The solution is to catch the error and then return the content in a default language, which is always satisfied by the block:

const getMarkdownContentOrUseDefault = ( fileName, defaultLang, lang ) => {   return getMarkdownContent( fileName, lang )     .catch( err => getMarkdownContent( fileName, defaultLang ) ) }

Please notice the different behavior from coding documentation as HTML inside the React component, and as an external Markdown file, when the translation is incomplete. In the first case, if a string has been translated but another one has not (in the .po file), then the React component will end up displaying mixed languages. It’s all or nothing in the second case: either the documentation is fully translated, or it is not. 

Setting the documentation into the modal

By now, we can retrieve the documentation from the Markdown file. Let’s see how to display it in the modal.

We first wrap Gutenberg’s Modal component, to inject the content as HTML:

import { Modal } from '@wordpress/components'; 
 const ContentModal = ( props ) => {   const { content } = props;   return (     <Modal        { ...props }     >       <div         dangerouslySetInnerHTML={ { __html: content } }       />     </Modal>   ); };

Then we retrieve the content from the Markdown file, and pass it to the modal as a prop using a state hook called page. Dynamically loading content is an async operation, so we must also use an effect hook to perform a side effect in the component. We need to read the content from the Markdown file only once, so we pass an empty array as a second argument to useEffect (or the hook would keep getting triggered):

import { useState, useEffect } from '@wordpress/element';
 const CacheControlContentModal = ( props ) => {   const fileName = 'cache-control'   const lang = window.graphqlApiCacheControl.userLang   const defaultLang = 'en' 
   const [ page, setPage ] = useState( [] ); 
   useEffect(() => {     getMarkdownContentOrUseDefault( fileName, defaultLang, lang ).then( value => {       setPage( value )     });   }, [] ); 
   return (     <ContentModal       { ...props }       content={ page }     />   ); };

Let’s see it working. Please notice how the chunk containing the documentation is loaded lazily (i.e. it’s triggered when the block is edited):

Tadaaaaaaaa 🎉

Writing documentation may not be your favorite thing in the world, but making it easy to write and maintain can help take the pain out of it.

Using Markdown instead of pure HTML is certainly one way to do that. I hope the approach we just covered not only improves your workflow, but also gives you a nice enhancement for your WordPress users.

The post Using Markdown and Localization in the WordPress Block Editor appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Adding a Custom Welcome Guide to the WordPress Block Editor

I am creating a WordPress plugin and there is a slight learning curve when it comes to using it. I’d like to give users a primer on how to use the plugin, but I want to avoid diverting users to documentation on the plugin’s website since that takes them out of the experience.

What would be great is for users to immediately start using the plugin once it’s installed but have access to helpful tips while they are actively using it. There’s no native feature for something like this in WordPress but we can make something because WordPress is super flexible like that.

So here’s the idea. We’re going to bake documentation directly into the plugin and make it easily accessible in the block editor. This way, users get to use the plugin right away while having answers to common questions directly where they’re working. 

My plugin operates through several Custom Post Types (CPT). What we’re going to build is essentially a popup modal that users get when they go to these CPTs. 

The WordPress block editor is built in React, which utilizes components that can be customized to and reused for different situations.  That is the case with what we’re making — let’s call it the <Guide> component — which behaves like a modal, but is composed of several pages that the user can paginate through.

WordPress itself has a <Guide> component that displays a welcome guide when opening the block editor for the first time:

Screenshot showing a modal on top of the WordPress block editor welcoming users to the editor for the first time.
WordPress displays a modal with instructions for using the block editor when a user loads the editor for the first time.

The guide is a container filled with content that’s broken up into individual pages. In other words, it’s pretty much what we want. That means we don’t have to re-invent the wheel with this project; we can reuse this same concept for our own plugin.

Let’s do exactly that. 

What we want to achieve

Before we get to the solution, let’s talk about the end goal.

The design satisfies the requirements of the plugin, which is a GraphQL server for WordPress. The plugin offers a variety of CPTs that are edited through custom blocks which, in turn, are defined through templates. There’s a grand total of two blocks: one called “GraphiQL client” to input the GraphQL query, and one called “Persisted query options” to customize the behavior of the execution.

Since creating a query for GraphQL is not a trivial task, I decided to add the guide component to the editor screen for that CPT. It’s available in the Document settings as a panel called “Welcome Guide.”

Screenshot showing the WordPress editor with the document settings panel open in the right column. a welcome guide tab is highlighted in the settings.

Crack that panel open and the user gets a link. That link is what will trigger the modal.

Close-up screenshot of the welcome guide tab opened, revealing a link that says "Open Guide: Creating Persisted Queries."

For the modal itself, I decided to display a tutorial video on using the CPT on the first page, and then describe in detail all the options available in the CPT on subsequent pages.

Screenshot showing the custom modal open in the block editor and containing an embedded video on how to use the plugin.

I believe this layout is an effective way to show documentation to the user. It is out of the way, but still conveniently close to the action. Sure, we can use a different design or even place the modal trigger somewhere else using a different component instead of repurposing <Guide>, but this is perfectly good.

Planning the implementation

The implementation comprises the following steps:

  1. Scaffolding a new script to register the custom sidebar panel
  2. Displaying the custom sidebar panel on the editor for our Custom Post Type only
  3. Creating the guide
  4. Adding content to the guide

Let’s start!

Step 1: Scaffolding the script

Starting in WordPress 5.4, we can use a component called <PluginDocumentSettingPanel> to add a panel on the editor’s Document settings like this:

const { registerPlugin } = wp.plugins; const { PluginDocumentSettingPanel } = wp.editPost;   const PluginDocumentSettingPanelDemo = () => (   <PluginDocumentSettingPanel     name="custom-panel"     title="Custom Panel"     className="custom-panel"   >     Custom Panel Contents   </PluginDocumentSettingPanel> ); registerPlugin( 'plugin-document-setting-panel-demo', {   render: PluginDocumentSettingPanelDemo,   icon: 'palmtree', } );

If you’re experienced with the block editor and already know how to execute this code, then you can skip ahead. I’ve been coding with the block editor for less than three months, and using React/npm/webpack is a new world for me — this plugin is my first project using them! I’ve found that the docs in the Gutenberg repo are not always adequate for beginners like me, and sometimes the documentation is missing altogether, so I’ve had to dig into the source code to find answers.

When the documentation for the component indicates to use that piece of code above, I don’t know what to do next, because <PluginDocumentSettingPanel> is not a block and I am unable to scaffold a new block or add the code there. Plus, we’re working with JSX, which means we need to have a JavaScript build step to compile the code.

I did, however, find the equivalent ES5 code:

var el = wp.element.createElement; var __ = wp.i18n.__; var registerPlugin = wp.plugins.registerPlugin; var PluginDocumentSettingPanel = wp.editPost.PluginDocumentSettingPanel; 
 function MyDocumentSettingPlugin() {   return el(     PluginDocumentSettingPanel,     {       className: 'my-document-setting-plugin',       title: 'My Panel',     },     __( 'My Document Setting Panel' )   ); } 
 registerPlugin( 'my-document-setting-plugin', {   render: MyDocumentSettingPlugin } );

ES5 code does not need be compiled, so we can load it like any other script in WordPress. But I don’t want to use that. I want the full, modern experience of ESNext and JSX.

So my thinking goes like this: I can’t use the block scaffolding tools since it’s not a block, and I don’t know how to compile the script (I’m certainly not going to set-up webpack all by myself). That means I’m stuck.

But wait! The only difference between a block and a regular script is just how they are registered in WordPress. A block is registered like this:

wp_register_script($ blockScriptName, $ blockScriptURL, $ dependencies, $ version); register_block_type('my-namespace/my-block', [   'editor_script' => $ blockScriptName, ]);

And a regular script is registered like this:

wp_register_script($ scriptName, $ scriptURL, $ dependencies, $ version); wp_enqueue_script($ scriptName);

We can use any of the block scaffolding tools to modify things then register a regular script instead of a block, which gains us access to the webpack configuration to compile the ESNext code. Those available tools are:

I chose to use the @wordpress/create-block package because it is maintained by the team developing Gutenberg.

To scaffold the block, we execute this in the command line:

npm init @wordpress/block

After completing all the prompts for information — including the block’s name, title and description — the tool will generate a single-block plugin, with an entry PHP file containing code similar to this:

/**  * Registers all block assets so that they can be enqueued through the block editor  * in the corresponding context.  *  * @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/  */ function my_namespace_my_block_block_init() {   $ dir = dirname( __FILE__ ); 
   $ script_asset_path = "$ dir/build/index.asset.php";   if ( ! file_exists( $ script_asset_path ) ) {     throw new Error(       'You need to run `npm start` or `npm run build` for the "my-namespace/my-block" block first.'     );   }   $ index_js     = 'build/index.js';   $ script_asset = require( $ script_asset_path );   wp_register_script(     'my-namespace-my-block-block-editor',     plugins_url( $ index_js, __FILE__ ),     $ script_asset['dependencies'],     $ script_asset['version']   ); 
   $ editor_css = 'editor.css';   wp_register_style(     'my-namespace-my-block-block-editor',     plugins_url( $ editor_css, __FILE__ ),     array(),     filemtime( "$ dir/$ editor_css" )   ); 
   $ style_css = 'style.css';   wp_register_style(     'my-namespace-my-block-block',     plugins_url( $ style_css, __FILE__ ),     array(),     filemtime( "$ dir/$ style_css" )   ); 
   register_block_type( 'my-namespace/my-block', array(     'editor_script' => 'my-namespace-my-block-block-editor',     'editor_style'  => 'my-namespace-my-block-block-editor',     'style'         => 'my-namespace-my-block-block',   ) ); } add_action( 'init', 'my_namespace_my_block_block_init' );

We can copy this code into the plugin, and modify it appropriately, converting the block into a regular script. (Note that I’m also removing the CSS files along the way, but could keep them, if needed.)

function my_script_init() {   $ dir = dirname( __FILE__ ); 
   $ script_asset_path = "$ dir/build/index.asset.php";   if ( ! file_exists( $ script_asset_path ) ) {     throw new Error(       'You need to run `npm start` or `npm run build` for the "my-script" script first.'     );   }   $ index_js     = 'build/index.js';   $ script_asset = require( $ script_asset_path );   wp_register_script(     'my-script',     plugins_url( $ index_js, __FILE__ ),     $ script_asset['dependencies'],     $ script_asset['version']   );   wp_enqueue_script(     'my-script'   ); } add_action( 'init', 'my_script_init' );

Let’s copy the package.json file over:

{   "name": "my-block",   "version": "0.1.0",   "description": "This is my block",   "author": "The WordPress Contributors",   "license": "GPL-2.0-or-later",   "main": "build/index.js",   "scripts": {     "build": "wp-scripts build",     "format:js": "wp-scripts format-js",     "lint:css": "wp-scripts lint-style",     "lint:js": "wp-scripts lint-js",     "start": "wp-scripts start",     "packages-update": "wp-scripts packages-update"   },   "devDependencies": {     "@wordpress/scripts": "^9.1.0"   } }

Now, we can replace the contents of file src/index.js with the ESNext code from above to register the <PluginDocumentSettingPanel> component. Upon running npm start (or npm run build for production) the code will be compiled into build/index.js.

There is a last problem to solve: the <PluginDocumentSettingPanel> component is not statically imported, but instead obtained from wp.editPost, and since wp is a global variable loaded by WordPress on runtime, this dependency is not present in index.asset.php (which is auto-generated during build). We must manually add a dependency to the wp-edit-post script when registering the script to make sure it loads before ours:

$ dependencies = array_merge(   $ script_asset['dependencies'],   [     'wp-edit-post',   ] ); wp_register_script(   'my-script',   plugins_url( $ index_js, __FILE__ ),   $ dependencies,   $ script_asset['version'] );

Now the script setup is ready!

The plugin can be updated with Gutenberg’s relentless development cycles. Run npm run packages-update to update the npm dependencies (and, consequently, the webpack configuration, which is defined on package "@wordpress/scripts") to their latest supported versions.

At this point, you might be wondering how I knew to add a dependency to the "wp-edit-post" script before our script. Well, I had to dig into Gutenberg’s source code. The documentation for <PluginDocumentSettingPanel> is somewhat incomplete, which is a perfect example of how Gutenberg’s documentation is lacking in certain places.

While digging in code and browsing documentation, I discovered a few enlightening things. For example, there are two ways to code our scripts: using either the ES5 or the ESNext syntax. ES5 doesn’t require a build process, and it references instances of code from the runtime environment, most likely through the global wp variable. For instance, the code to create an icon goes like this:

var moreIcon = wp.element.createElement( 'svg' );

ESNext relies on webpack to resolve all dependencies, which enables us to import static components. For instance, the code to create an icon would be:

import { more } from '@wordpress/icons';

This applies pretty much everywhere. However, that’s not the case for the <PluginDocumentSettingPanel> component, which references the runtime environment for ESNext:

const { PluginDocumentSettingPanel } = wp.editPost;

That’s why we have to add a dependency to the “wp-edit-post” script. That’s where the wp.editPost variable is defined.

If <PluginDocumentSettingPanel> could be directly imported, then the dependency to “wp-edit-post” would be automatically handled by the block editor through the Dependency Extraction Webpack Plugin. This plugin builds the bridge from static to runtime by creating a index.asset.php file containing all the dependencies for the runtime environment scripts, which are obtained by replacing "@wordpress/" from the package name with "wp-". Hence, the "@wordpress/edit-post" package  becomes the "wp-edit-post" runtime script. That’s how I figured out which script to add the dependency.

Step 2: Blacklisting the custom sidebar panel on all other CPTs 

The panel will display documentation for a specific CPT, so it must be registered only to that CPT. That means we need to blacklist it from appearing on any other post types.

Ryan Welcher (who created the <PluginDocumentSettingPanel> component) describes this process when registering the panel:

const { registerPlugin } = wp.plugins; const { PluginDocumentSettingPanel } = wp.editPost const { withSelect } = wp.data; 
 const MyCustomSideBarPanel = ( { postType } ) => { 
   if ( 'post-type-name' !== postType ) {     return null;   } 
   return(     <PluginDocumentSettingPanel       name="my-custom-panel"       title="My Custom Panel"     >       Hello, World!     </PluginDocumentSettingPanel>   ); } 
 const CustomSideBarPanelwithSelect = withSelect( select => {   return {     postType: select( 'core/editor' ).getCurrentPostType(),   }; } )( MyCustomSideBarPanel); 
 registerPlugin( 'my-custom-panel', { render: CustomSideBarPanelwithSelect } );

He also suggests an alternative solution, using useSelect instead of withSelect.

That said, I’m not totally convinced by this solution, because the JavaScript file must still be loaded, even if it isn’t needed, forcing the website to take a performance hit. Doesn’t it make more sense to not register the JavaScript file than it does to run JavaScript just to disable JavaScript?

I have created a PHP solution. I’ll admit that it feels a bit hacky, but it works well. First, we find out which post type is related to the object being created or edited:

function get_editing_post_type(): ?string {   if (!is_admin()) {     return null;   } 
   global $ pagenow;   $ typenow = '';   if ( 'post-new.php' === $ pagenow ) {     if ( isset( $ _REQUEST['post_type'] ) && post_type_exists( $ _REQUEST['post_type'] ) ) {       $ typenow = $ _REQUEST['post_type'];     };   } elseif ( 'post.php' === $ pagenow ) {     if ( isset( $ _GET['post'] ) && isset( $ _POST['post_ID'] ) && (int) $ _GET['post'] !== (int) $ _POST['post_ID'] ) {       // Do nothing     } elseif ( isset( $ _GET['post'] ) ) {       $ post_id = (int) $ _GET['post'];     } elseif ( isset( $ _POST['post_ID'] ) ) {       $ post_id = (int) $ _POST['post_ID'];     }     if ( $ post_id ) {       $ post = get_post( $ post_id );       $ typenow = $ post->post_type;     }   }   return $ typenow; }

Then, ,we register the script only if it matches our CPT:

add_action('init', 'maybe_register_script'); function maybe_register_script() {   // Check if this is the intended custom post type   if (get_editing_post_type() != 'my-custom-post-type') {     return;   } 
   // Only then register the block   wp_register_script(...);   wp_enqueue_script(...); }

Check out this post for a deeper dive on how this works.

Step 3: Creating the custom guide

I designed the functionality for my plugin’s guide based on the WordPress <Guide> component. I didn’t realize I’d be doing that at first, so here’s how I was able to figure that out.

  1. Search the source code to see how it was done there.
  2. Explore the catalogue of all available components in Gutenberg’s Storybook.

First, I copied content from the block editor modal and did a basic search. The results pointed me to this file. From there I discovered the component is called <Guide> and could simply copy and paste its code to my plugin as a base for my own guide.

Then I looked for the component’s documentation. I browsed the @wordpress/components package (which, as you may have guessed, is where components are implemented) and found the component’s README file. That gave me all the information I needed to implement my own custom guide component.

I also explored the catalogue of all the available components in Gutenberg’s Storybook (which actually shows that these components can be used outside the context of WordPress). Clicking on all of them, I finally discovered <Guide>. The storybook provides the source code for several examples (or stories). It’s a handy resource for understanding how to customize a component through props.

At this point, I knew <Guide> would make a solid base for my component. There is one missing element, though: how to trigger the guide on click. I had to rack my brain for this one!

This is a button with a listener that opens the modal on click:

import { useState } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import MyGuide from './guide'; 
 const MyGuideWithButton = ( props ) => {   const [ isOpen, setOpen ] = useState( false );   return (     <>       <Button onClick={ () => setOpen( true ) }>         { __('Open Guide: “Creating Persisted Queries”') }       </Button>       { isOpen && (         <MyGuide            { ...props }           onFinish={ () => setOpen( false ) }         />       ) }     </>   ); }; export default MyGuideWithButton;

Even though the block editor tries to hide it, we are operating within React. Until now, we’ve been dealing with JSX and components. But now we need the useState hook, which is specific to React.

I’d say that having a good grasp of React is required if you want to master the WordPress block editor. There is no way around it.

Step 4: Adding content to the guide

We’re almost there! Let’s create the <Guide> component, containing a <GuidePage> component for each page of content.

The content can use HTML, include other components, and whatnot. In this particular case, I have added three <GuidePage> instances for my CPT just using HTML. The first page includes a video tutorial and the next two pages contain detailed instructions.

import { Guide, GuidePage } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; 
 const MyGuide = ( props ) => {   return (     <Guide { ...props } >       <GuidePage>         <video width="640" height="400" controls>           <source src="https://d1c2lqfn9an7pb.cloudfront.net/presentations/graphql-api/videos/graphql-api-creating-persisted-query.mov" type="video/mp4" />           { __('Your browser does not support the video tag.') }         </video>         // etc.       </GuidePage>       <GuidePage>         // ...       </GuidePage>       <GuidePage>         // ...       </GuidePage>     </Guide>   ) } export default MyGuide;
imaged gif showing the mouse cursor clicking on the Open Guide link in the block editor's document settings, which opens the custom welcome guide containing a video with links to other pages in the modal.
Hey look, we have our own guide now!

Not bad! There are a few issues, though:

  • I couldn’t embed the video inside the <Guide> because clicking the play button closes the guide. I assume that’s because the <iframe> falls outside the boundaries of the guide. I wound up uploading the video file to S3 and serving with <video>.
  • The page transition in the guide is not very smooth. The block editor’s modal looks alright because all pages have a similar height, but the transition in this one is pretty abrupt.
  • The hover effect on buttons could be improved. Hopefully, the Gutenberg team needs to fix this for their own purposes, because my CSS aren’t there. It’s not that my skills are bad; they are nonexistent.

But I can live with these issues. Functionality-wise, I’ve achieved what I need the guide to do.

Bonus: Opening docs independently 

For our <Guide>, we created the content of each <GuidePage> component directly using HTML. However, if this HTML code is instead added through an autonomous component, then it can be reused for other user interactions.

For instance, the component <CacheControlDescription> displays a description concerning HTTP caching:

const CacheControlDescription = () => {   return (     <p>The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or "no-store" if the max-age is 0</p>   ) } export default CacheControlDescription;

This component can be added inside a <GuidePage> as we did before, but also within a <Modal> component:

import { useState } from '@wordpress/element'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import CacheControlDescription from './cache-control-desc'; 
 const CacheControlModalWithButton = ( props ) => {   const [ isOpen, setOpen ] = useState( false );   return (     <>       <Button          icon="editor-help"         onClick={ () => setOpen( true ) }       />       { isOpen && (         <Modal            { ...props }           onRequestClose={ () => setOpen( false ) }         >           <CacheControlDescription />         </Modal>       ) }     </>   ); }; export default CacheControlModalWithButton;

To provide a good user experience, we can offer to show the documentation only when the user is interacting with the block. For that, we show or hide the button depending on the value of isSelected:

import { __ } from '@wordpress/i18n'; import CacheControlModalWithButton from './modal-with-btn'; 
 const CacheControlHeader = ( props ) => {   const { isSelected } = props;   return (     <>       { __('Cache-Control max-age') }       { isSelected && (         <CacheControlModalWithButton />       ) }     </>   ); } export default CacheControlHeader;

Finally, the <CacheControlHeader> component is added to the appropriate control.

Animated gif showing the option to view a guide displaying when a block is selected in the editor.

Tadaaaaaaaa 🎉

The WordPress block editor is quite a piece of software! I was able to accomplish things with it that I would have been unable to without it. Providing documentation to the user may not be the shiniest of examples or use cases, but it’s a very practical one and something that’s relevant for many other plugins. Want to use it for your own plugin? Go for it!

The post Adding a Custom Welcome Guide to the WordPress Block Editor appeared first on CSS-Tricks.


, , , , , ,

How to Create Custom WordPress Editor Blocks in 2020

Peter Tasker on creating blocks right now:

It’s fairly straightforward these days to get set up with the WP CLI ‘scaffold’ command. This command will set up a WordPress theme or plugin with a ‘blocks’ folder that contains the PHP and base CSS and JavaScript required to create a custom block. The only drawback that I noticed is that the JavaScript uses the old ES5 syntax rather than modern ESNext. Modern JavaScript allows us to write more concise code and use JSX in our custom block code.

You can also use the ‘create-guten-block’ tool by Ahmad Awais. It gives you a lot of the boilerplate stuff you need out of the box, like Webpack, ESNext support etc. Setting it up is fairly straightforward, and it’s similar to Create React App.

I’ve used create-guten-block for the handful of custom blocks I’ve made so far, and have found it a pretty nice experience.

But… I feel like I just sort of lucked into being comfortable with all this. I have one foot in WordPress development and just so happen to have one foot in React development. Building blocks with both technologies together feels decently natural to me. If blocks were Angular or something, I feel like I might not have even given it a shot.

I’ll echo this sentiment:

I also found it really annoying working on a block that’s actively changing in code. Every time you reload Gutenberg, you’ll get the “This block appears to have been modified externally…” message because the markup of the block has changed.

I get why it’s throwing the error, but it slows you down.

At the end, Peter mentions the approach of building blocks that Advanced Custom Fields has. It almost feels like a weird bizarro-reverso world. The ACF approach seems more like what WordPress would have done in a normal world (building blocks with just PHP and templating) and third-parties would be the ones adding all the fancy React stuff.

Direct Link to ArticlePermalink

The post How to Create Custom WordPress Editor Blocks in 2020 appeared first on CSS-Tricks.


, , , , ,

8 Little Videos About the Firefox Shape Path Editor

It sometimes takes a quick 35 seconds for a concept to really sink in. Mikael Ainalem delivers that here, in the case that you haven’t quite grokked the concepts behind path-based CSS properties like clip-path and shape-outside.

Here are two of my favorites. The first demonstrates animating text into view using a polygon as a clip.

The second shows how the editor can help morph one shape into another.

Direct Link to ArticlePermalink

The post 8 Little Videos About the Firefox Shape Path Editor appeared first on CSS-Tricks.


, , , , , ,