Tag: Block

How to Work With WordPress Block Patterns

Just a little post I wrote up over at The Events Calendar blog. The idea is that a set of blocks can be grouped together in WordPress, then registered in a register_block_pattern() function that makes the group available to use as a “block pattern” in any page or post.

Block patterns are becoming upper-class citizens in the WordPress block editor. They were announced without much fanfare in WordPress 5.5 back in August, but have been given prominent real estate in the block inserter with its own tab next to blocks, including 10 or so default ones right out of the box.

Block patterns are sandwiched between Blocks and Reusable Blocks in the block inserter, which is a perfect metaphor for where it fits in the bigger picture of WordPress editing.

If the 5.6 Beta 3 release notes are any indication, then it looks like more patterns are on the way for default WordPress themes. And, of course, the block registration function has an unregister_block_pattern() companion should you need to opt out of any patterns.

What I find interesting is how the blocks ecosystem is evolving. We started with a set of default blocks that can be inserted into a post. We got reusable blocks that provide a way to assemble a group of blocks with consistent content across all pages of posts. Now we have a way to do the same, but in a much more flexible and editable way. The differences are subtle, but the use cases couldn’t be more different. We’ve actually been using reusable blocks here at CSS-Tricks for post explanations, like this:

We drop some text in here when we think there’s something worth calling out or that warrants a little extra explanation.

Any reusable block can be converted to a “regular” block. The styles are maintained but the content is not. That’s been our hack-y approach for speeding up our process around here, but now that block patterns are a thing, previous reusable blocks we’ve been using now make more sense as patterns.

Direct Link to ArticlePermalink


The post How to Work With WordPress Block Patterns appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,

Getting the WordPress Block Editor to Look Like the Front End Design

I’m a WordPress user and, if you’re anything like me, you always have two tabs open when you edit a post: one with the new fancy pants block editor, aka Gutenberg, and another with a preview of the post so you know it won’t look wonky on the front end.

It’s no surprise that a WordPress theme’s styles only affect the front end of your website. The back end posy editor generally looks nothing like the front end result. We’re used to it. But what if I said it’s totally possible for the WordPress editor nearly mirror the front end appearance?

All it takes is a custom stylesheet.

Mind. Blown. Right? Well, maybe it’s not that mind blowing, but it may save you some time if nothing else. 🙂

WordPress gives us a hint of what’s possible here. Fire up the default Twenty Twenty theme that’s packaged with WordPress, fire up the editor, and it sports some light styling.

This whole thing consists of two pretty basic changes:

  1. A few lines of PHP in your theme’s functions.php file that tell the editor you wish to load a custom stylesheet for editor styles
  2. Said custom stylesheet

Right then, enough pre-waffle! Let’s get on with making the WordPress editor look like the front end, shall we?

Step 1: Crack open the functions.php file

OK I was lying, just a little more waffling. If you’re using a WordPress theme that you don’t develop yourself, it’s probably best that you setup a child theme before making any changes to your main theme. </pre-waffle>

Fire up your favorite text editor and open up the theme’s functions.php file that’s usually located in the root of the theme folder. Let’s drop in the following lines at the end of the file:

// Gutenberg custom stylesheet add_theme_support('editor-styles'); add_editor_style( 'style-editor.css' ); // make sure path reflects where the file is located

What this little snippet of code does is tell WordPress to add support for a custom stylesheet to be used with Gutenberg, then points to where that stylesheet (that we’re calling editor-style.css) is located. WordPress has solid documentation for the add_theme_support function if you want to dig into it a little more.

Step 2: CSS tricks (see what I did there?!)

Now we’re getting right into our wheelhouse: writing CSS!

We’ve added editor-styles support to our theme, so the next thing to do is to add the CSS goodness to the stylesheet we defined in functions.php so our styles correctly load up in Gutenberg.

There are thousands of WordPress themes out there, so I couldn’t possibly write a stylesheet that makes the editor exactly like each one. Instead, I will show you an example based off of the theme I use on my website. This should give you an idea of how to build the stylesheet for your site. I’ll also include a template at the end, which should get you started.

OK let’s create a new file called style-editor.css and place it in the root directory of the theme (or again, the child theme if you’re customizing a third-party theme).

Writing CSS for the block editor isn’t quite as simple as using standard CSS elements. For example, if we were to use the following in our editor stylesheet it wouldn’t apply the text size to <h2> elements in the post.

h2 {   font-size: 1.75em; }

Instead of elements, our stylesheet needs to target Block Editor blocks. This way, we know the formatting should be as accurate as possible. That means <h2> elements needs to be scoped to the .rich-text.block-editor-rich-text__editable class to style things up.

Showing the block editor with a light yellow background, a heading that reads Holly Dolly, and a heading 2 with DevTools open to the left in dark mode and a block-editor-rich-text-__editor class highlighted in red.
It just takes a little peek at DevTools to find a class we can latch onto.
h2.rich-text.block-editor-rich-text__editable {   font-size: 1.75em; }

I just so happened to make a baseline CSS file that styles common block editor elements following this pattern. Feel free to snag it over at GitHub and swap out the styles so they complement your theme.

I could go on building the stylesheet here, but I think the template gives you an idea of what you need to populate within your own stylesheet. A good starting point is to go through the stylesheet for your front-end and copy the elements from there, but you will likely need to change some of the element classes so that they apply to the Block Editor window.

If in doubt, play around with elements in your browser’s DevTools to work out what classes apply to which elements. The template linked above should capture most of the elements though.

The results

First of all, let’s take a look at what the WordPress editor looks like without a custom stylesheet:

Showing the WordPress block editor without any custom styling, which is a plain white screen with black text including a heading one paragraph, a blockquote and a black rounded button.
The block editor sports a clean, stark UI in its default appearance. It’s pulling in Noto Serif from Google Fonts but everything else is pretty bare bones.

Let’s compare that to the front end of my test site:

Showing a webpage with the same heading, paragraph, blockquote and button, but with styles that include a red-to-orange gradient that goes left-to-right as a background behind the white heading, a typewriter-like font, the same gradient to style the blockquote borders and text, and to style the button.

Things are pretty different, right? Here we still have a simple design, but I’m using gradients all over, to the max! There’s also a custom font, button styling, and a blockquote. Even the containers aren’t exactly square edges.

Love it or hate it, I think you will agree this is a big departure from the default Gutenberg editor UI. See why I have to have a separate tab open to preview my posts?

Now let’s load up our custom styles and check things out:

The same look as the custom styles on the front end but now displayed in the WordPress block editor.

Well would you look at that! The editor UI now looks pretty much exactly the same as the front end of my website. The content width, fonts, colors and various elements are all the same as the front end. I even have the fancy background against the post title!

Ipso facto — no more previews in another tab. Cool, huh?


Making the WordPress editor look like your front end is a nice convenience. When I’m editing a post, flipping between tabs to see what the posts looks like on the front end ruins my mojo, so I prefer not to do it.

These couple of quick steps should be able to do the same for you, too!


The post Getting the WordPress Block Editor to Look Like the Front End Design appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , , ,
[Top]

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.

CSS-Tricks

, , , , ,
[Top]

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.

CSS-Tricks

, , , , , ,
[Top]

Block Links: The Search for a Perfect Solution

I was reading this article by Chris where he talks about block links — you know, like wrapping an entire card element inside an anchor — being a bad idea. It’s bad accessibility because of how it affects screen readers. And it’s bad UX because it prevents simple user tasks, like selecting text.

But maybe there’s something else at play. Maybe it’s less an issue with the pattern than the implementation of it. That led me to believe that this is the time to write follow-up article to see if we can address some of the problems Chris pointed out.

Throughout this post, I’ll use the term “card” to describe a component using the block link pattern. Here’s what we mean by that.

Let’s see how we want our Card Components to work:

  1. The whole thing should be linked and clickable.
  2. It should be able to contain more than one link.
  3. Content should be semantic so assistive tech can understand it.
  4. The text should be selectable, like regular links.
  5. Things like right-click and keyboard shortcuts should work with it
  6. Its elements should be focusable when tabbing.

That’s a long list! And since we don’t have any standard card widget provided by the browser, we don’t have any standard guidelines to build it. 

Like most things on the web, there’s more than one way to make a card component. However, I haven’t found something that checks all the requirements we just covered. In this article, we will try to hit all of them. That’s what we’re going to do now!

Method 1: Wrap everything an <a>

This is the most common and the easiest way to make a linked card. Take the HTML for the card and wrap the entire thing in an anchor tag.

<a href="/">   <!-- Card markup --> </a>

Here’s what that gives us:

  1. It’s clickable.
  2. It works with right-click and keyboard shortcuts.

Well, not great. We still can’t:

  1. Put another link inside the card because the entire thing is a single link
  2. Use it with a screen reader — the content is not semantic, so assistive technology will announce everything inside the card, starting from the time stamp
  3. Select text

That’s enough 👎 that we probably shouldn’t use it. Let’s move onto the next technique.

Method 2: Just link what needs linking

This is a nice compromise that sacrifices a little UX for improved accessibility.

With this pattern we achieve most of our goals:

  1. We can put as many links as we want. 
  2. Content is semantic.
  3. We can select the text from Card.
  4. Right Click and keyboard shortcuts work.
  5. The focus is in proper order when tabbing.

But it is missing the main feature we want in a card: the whole thing should be clickable! Looks like we need to try some other way.

Method 3: The good ol’  ::before pseudo element

In this one, we add a ::before or ::after element, place it above the card with absolute positioning and stretch it over the entire width and height of the card so it’s clickable.

But now:

  1. We still can’t add more than one link because anything else that’s linked is under the pseudo element layer. We can try to put all the text above the pseudo element, but card link itself won’t work when clicking on top of the text.
  2. We still can’t select the text. Again, we could swap layers, but then we’re back to the clickable link issue all over again.

Let’s try to actually check all the boxes here in our final technique.

Method 4: Sprinkle JavaScript on the second method

Let’s build off the second method. Recall that’s what where we link up everything we want to be a link:

<article class="card">   <time datetime="2020-03-20">Mar 20, 2020</time>   <h2><a href="https://css-tricks.com/a-complete-guide-to-calc-in-css/" class="main-link">A Complete Guide to calc() in CSS</a></h2>   <p>     In this guide, let’s cover just about everything there is to know about this very useful function.   </p>   <a class="author-name" href="https://css-tricks.com/author/chriscoyier/" target="_blank">Chris Coyier</a>     <div class="tags">       <a class="tag" href="https://css-tricks.com/tag/calc/" >calc</a>     </div> </article>

So how do we make the whole card clickable? We could use JavaScript as a progressive enhancement to do that. We’ll start by adding a click event listener to the card and trigger the click on the main link when it is triggered.

const card = document.querySelector(".card") const mainLink = document.querySelector('.main-link') 
 card.addEventListener("click", handleClick) 
 function handleClick(event){   mainLink.click(); }

Temporarily, this introduces the problem that we can’t select the text, which we’ve been trying to fix this whole time. Here’s the trick: we’ll use the relatively less-known web API window.getSelection. From MDN:

The Window.getSelection() method returns a Selection object representing the range of text selected by the user or the current position of the caret.

Although, this method returns an Object, we can convert it to a string with toString().

const isTextSelected = window.getSelection().toString()

With one line and no complicated kung-fu tricks with event listeners, we know if the user has selected text. Let’s use that in our handleClick function.

const card = document.querySelector(".card") const mainLink = document.querySelector('.main-link') 
 card.addEventListener("click", handleClick) 
 function handleClick(event){   const isTextSelected = window.getSelection().toString();   if (!isTextSelected) {     mainLink.click();   } }

This way, the main link can be clicked when no text selected, and all it took was a few lines of JavaScript. This satisfies our requirements:

  1. The whole thing is linked and clickable.
  2. It is able to contain more than one link.
  3. This content is semantic so assistive tech can understand it.
  4. The text should be selectable, like regular links.
  5. Things like right-click and keyboard shortcuts should work with it
  6. Its elements should be focusable when tabbing.

Here’s the final demo with all the JavaScript code we have added:

I think we’ve done it! Now you know how to make a perfect clickable card component.

What about other patterns? For example, what if the card contains the excerpt of a blog post followed by a “Read More’ link? Where should that go? Does that become the “main” link? What about image?

For those questions and more, here’s some further reading on the topic:

The post Block Links: The Search for a Perfect Solution appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

WordPress Block Transforms

This has been the year of Gutenberg for us here at CSS-Tricks. In fact, that’s a goal we set at the end of last year. We’re much further along that I thought we’d be, authoring all new content in the block editor¹, enabling the block editor for all content now. That means when we open most old posts, we see all the content in the “Classic” block. It looks like this:

A post written on CSS-Tricks before we were using the block editor.

The entire contents of the post is in a single block, so not exactly making any use of the block editor. It’s still “visual,” like the block editor, but it’s more like the old visual editor using TinyMCE. I never used that as it kinda forcefully mangled HTML in a way I didn’t like.

This is the #1 thing I was worried about

Transforming a Classic block into new blocks is as trivial as selecting the Classic block and selecting the “Convert to Blocks” option.

Select the option and the one block becomes many blocks.

How does the block editor handle block-izing old content, when we tell it to do that from the “Convert to Blocks” option? What if it totally screws up content during the conversion? Will we ever be able to switch?

The answer: it does a pretty darn good job. But… there are still issues. Not “bugs” but situations where we have custom HTML in our old content and it doesn’t know what to do with it — let alone how to convert it into exactly the blocks we wish it would. There is a way!

Basic Block Transforms

That’s where this idea of “Block Transforms” comes in. All (well, most?) native blocks have “to” and “from” transformations. You’re probably already familiar with how it manifests in the UI. Like a paragraph can transform “to” a quote and vice versa. Here’s a super meta screenshot of this very paragraph:

Those transforms aren’t magic; they are very explicitly coded. When you register a block, you specify the transforms. Say you were registering your own custom code block. You’d want to make sure that you could transform it…

  • From and to the default built-in code block, and probably a handful of others that might be useful.
  • Back to the built-in code block.

Which might look like:

registerBlockType("my/code-block", {   title: __("My Code Block"),   ...   transforms: {     from: [       {         type: "block",         priority: 7,         blocks: ["core/code", "core/paragraph", "core/preformatted"],         transform: function (attributes) {           return createBlock("my/code-block", {             content: attributes.content,           });         },       },     ],     to: [       {         type: "block",         blocks: ["core/code"],         transform: ({ content }) => createBlock("core/code", { content }),       },     ],        ...

Those are transforms to and from other blocks. Fortunately, this is a pretty simple block where we’re just shuffling the content around. More complex blocks might need to pass around more data, but I haven’t had to deal with that yet.

The more magical stuff: Block Transforms from raw code

Here’s the moment of truth for old content:

The “Convert to Blocks” option.

In this situation, blocks are being created not from other blocks, but from raw code. Quite literally, the HTML is being looked at and choices are being made about what blocks to make from chunks of that HTML. This is where it’s amazing the block editor does such a good job with the choices, and also where things can go wrong and it can fail, make wrong block choices, or mangle content.

In our old content, a block of code (a super very important thing) in a post would look like this:

<pre rel="JavaScript"><code class="language-javascript" markup="tt">   let html = `<div>cool</div>`; </code></pre>

Sometimes the block conversion would do OK on those, turning it into a native code block. But there were a number of problems:

  1. I don’t want a native code block. I want that to be transformed into our own new code block (blogged about that here).
  2. I need some of the information in those attributes to inform settings on the new block, like what kind of code it is.
  3. The HTML in our old code blocks was not escaped and I need it to not choke on that.

I don’t have all the answers here, as this is an evolving process, but I do have some block transforms in place now that are working pretty well. Here’s what a “raw” transform (as opposed to a “block” transform) looks like:

registerBlockType("my/code-block", {   title: __("My Code Block"),   // ...   transforms: {     from: [       {         type: "block",         priority: 7,         // ...       },       {         type: "raw",         priority: 8,         isMatch: (node) =>           node.nodeName === "PRE" &&           node.children.length === 1 &&           node.firstChild.nodeName === "CODE",         transform: function (node) {           let pre = node;           let code = node.querySelector("code");            let codeType = "html";           if (pre.classList.contains("language-css")) {             codeType = "css";           }           if (pre.getAttribute("rel") === "CSS") {             codeType = "css";           }           if (pre.classList.contains("language-javascript")) {             codeType = "javascript";           }           if (code.classList.contains("language-javascript")) {             codeType = "javascript";           }           // ... other data wrangling...            return createBlock("csstricks/code-block", {             content: code.innerHTML,             codeType: codeType,           });         },       },     ],     to: [       // ...      ],        // ...  }

That isMatch function runs on every node in the HTML it finds, so this is the big opportunity to return true from that in the special situations you need to. Note in the code above that I’m specifically looking for HTML that looks like <pre ...><code ...>. When that matches, the transform runs, and I can return a createBlock call that passes in data and content I extract from the node with JavaScript.

Another example: Pasting a URL

“Raw” transforms don’t only happen when you “Convert to Blocks.” They happen when you paste content into the block editor too. You’ve probably experienced this before. Say you have copied some table markup from somewhere and paste it into the block editor -— it will probably paste as a table. A YouTube URL might paste into an embed. This kind of thing is why copy/pasting from Word documents and the like tend to work so well with the block editor.

Say you want some special behavior when a certain type of URL is pasted into the editor. This was the situation I was in with our custom CodePen Embed block. I wanted it so if you pasted a codepen.io URL, it would use this custom block, instead of the default embed.

This is a “from” transform that looks like this:

{   type: "raw",   priority: 8, // higher number to beat out default   isMatch: (node) =>     node.nodeName === "P" &&     node.innerText.startsWith("https://codepen.io/"),    transform: function (node) {     return createBlock("cp/codepen-gutenberg-embed-block", {       penURL: node.innerText,       penID: getPenID(node.innerText), // helper function     });   }, }

So…

Is it messy? A little. But it’s as powerful as you need it to be. If you have an old site with lots of bespoke HTML and shortcodes and stuff, then getting into block transforms is the only ticket out.

I’m glad I went to WPBlockTalk and caught K. Adam White’s talk on shortcodes because there was just one slide that clued me into that this was even possible. There is a little bit of documentation on it.

One thing I’d like to figure out is if it’s possible to run these transforms on all old content in the database. Seems a little scary, but also like it might be a good idea in some situations. Once I get my transformations really solid, I could see doing that so any old content ready-to-go in the block editor when opening it up. I just have no idea how to go about it.

I’m glad to be somewhat on top of this though, as I friggin love the block editor right now. It’s a pleasure to write in and build content with it. I like what Justin Tadlock said:

The block system is not going anywhere. WordPress has moved beyond the point where we should consider the block editor as a separate entity. It is an integral part of WordPress and will eventually touch more and more areas outside of the editing screen.

It’s here to stay. Embracing the block editor and bending it to our will is key.

  1. What are we calling it anyway? “Gutenberg” doesn’t seem right anymore. Feels like that will fade away, even though the development of it still happens in the Gutenberg plugin. I think I’ll just call it “the block editor” unless specifically referring to that plugin.

The post WordPress Block Transforms appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

How to Use Block Variations in WordPress

WordPress 5.4 was released not so long ago and, along with other improvements and bug fixes, it introduced a feature called Block Variations. I had a chance to use it on one of my recent projects and am so pleasantly surprised with how smart this feature is. I actually think it hasn’t received the attention it deserves, which is why I decided to write this article.

What is a Block Variation?

Block Variations allow developers to define instances of existing blocks. An example that you’ll see below is a quote block. Perhaps your site has three variations of how to display a quote on your site. A Block Variation can be created for each one so that they are all styled differently. This sounds awfully familiar with how Block Styles, but the concept of variations goes a bit further than that, as we’ll see.

How are Block Variations different from Block Styles?

Fair question. Block variations appear in the inserter as separate blocks with unique names and (optionally) icons and can have pre-filled custom attributes, and inner blocks.

Block Styles are designed to alter the look of the block. In fact, a Block Style is a fancy way of adding a custom class to a block using the Block options in the post editor.

The difference is clear when you consider how each one is used in the post editor. Let’s say we register a new Block Style called “Fancy Quote.” We do that by extending the core “Quote” block like this example from the Block Editor Handbook:

wp.blocks.registerBlockStyle(   'core/quote',   {     name: 'fancy-quote',     label: 'Fancy Quote'   }, );

This adds a .is-style-fancy-quote class to the Quote block settings in the post editor.

Screenshot of the Block options in the WordPress post editor highlighting the options for a quote block. A "Fancy Quote" option is listed under Styles and the custom class name is in an Additional CSS Classes field.
We now have a Fancy Quote option in the Block options under “Styles” and the class for it filled in for us.

Even though it sort of sounds like it would do the same thing (which it technically can), a Block Variation can be used to pre-fill custom attributes (including custom classes) and inner blocks. They’re actually registered as separate blocks.

Let’s take a closer look at the API and what block variations can do.

Creating a Block Variation

The API for registering Block Variations is very similar that of the Block Style we just looked at:

wp.blocks.registerBlockVariation(   'core/quote',   {     name: 'fancy-quote',     title: 'Fancy Quote',   }, );

The registerBlockVariation function accepts the name of the block (in our case it is core/quote) and an object (or an array of objects) describing the variation(s).

The code above doesn’t do much by default, but it does add “Fancy Quote” to the list of available blocks.

Showing the Fancy Quote variation in the WordPress Block Inserter.
We now have two different “quote” blocks available to drop into the post.

To take full advantage of the variation. we need to provide more details in the object describing it. The list is covered in the Make WordPress post, but I’ll share it here and provide additional comments.

  • name – The unique and machine-readable name of the variation. Following the examples on Github and Make post it’s safe to assume that the best practice is to use kebab-case for naming variations.
  • title – A human-readable variation title. This is what appears under the icon in the Inserter.
  • description – A detailed variation description. Appears in the Inserter as well. If empty, the default block description will be used. (Optional)
  • icon – An icon for the variation. Can be a Dashicons slug, an SVG or an object. Follows the same declaration pattern as in registerBlockType. (Optional)
  • isDefault – Indicates whether the current variation is the default one. Defaults to false. In case of our example, if we set it to true, the Fancy Quote block will be the only Quote block available in the inserter. (Optional)
  • attributes – Values that override block attributes. These are block-specific. You can set the level for the Heading block or height for Spacer, for example.
  • innerBlocks – Initial configuration of nested blocks. Only applies to blocks that allow inner blocks in the first place, like Columns, Cover, or Group. We’ll cover this in one of the examples. (Optional)
  • example – Example provides structured data for the block preview. You can set it to undefined to disable the preview shown for the block type. This is the same as the example field in registerBlockType. (Optional) There’s more information available on this parameter.
  • scope – The list of scopes where the variation is applicable. When not provided, it assumes all available scopes. Available options are block and inserter. We’ll cover this in detail in one of the examples.

Many of you may wonder why we need this extra layer of abstraction. Let me try to answer that with a few use cases (one form my recent project).

Use case: Buttons with different widths

Let’s say you have a design system with two types of buttons: Fill and Outline.

Two buttons, one with a green fill and one with a green border. Both say Learn More.
Fill and Outline button styles in the design system

Lucky you, because these are the default styles for buttons in WordPress. No need to register any new styles or hack the editor. All you have to do is write some CSS for each style and call it a day. Life is good and everybody’s happy.

But then you look in the design spec again and notice that there is a little twist. The buttons come in three widths: Regular, Wide, and Full.

The same green buttons but with additional variations at two different widths for a totally of six buttons.
Fill and Outline button styles with different width variations

Dammit! You are a little upset because you now have two options:

  1. Write two extra classes for the new button sizes (say, .is-wide and .is-full), then teach the client to use the Advanced panel in the editor to add those classes and write a manual where you explain what each class does. Or…
  2. Register four(!) new styles that go in the Block options: Fill Wide, Fill Full, Outline Wide, and Outline Full.

Neither of those options are exactly elegant. (BTW, what is Fill Full exactly? Quite an unfortunate mouthful!)

There are two more options that I didn’t include in the list:

  • Filter the button block and add a custom width control to it
  • Build a custom block from scratch.

These obviously feel like heavy lifts for such a simple task.

Enter Block Variations! By adding just two variations, Full and Wide, we can keep things clean and simple:

wp.blocks.registerBlockVariation(   'core/buttons',   [     {       name: 'wide',       title: 'Wide Buttons',       attributes: {         className: 'is-wide'       },   },   {       name: 'full',       title: 'Full Buttons',       attributes: {         className: 'is-full'       },     }   ] );

This is the same as adding a custom class to the Buttons block, but in a neat and elegant way that can be dropped directly into a post from the Block Inserter:

Showing the Wide and Full button variations in the WordPress Block Inserter.
Button variations in the inserter

Life is good and everybody is happy again! So what did we learn from this example?

  • It shows that Block Variations are not designed to replace Block Styles. In fact, they can work pretty well together even if the variation just adds a class to a block.
  • It demos how to register multiple variations in a single declaration.

Use case: Repeating column layouts

Let’s say you are a designer and have a portfolio website with case studies. Each case study has an intro section with the name of the project, client information, and a description of your role on the project. It might look something like this:

Showing three columns, one that says Website Design, one that says Clients, and one says Role. Each one represents a column we want on the page.
The type of work (left), who it was for (center) and your role on it (right)

The problem is that it’s a bit tedious to build this part of the layout every time you create a new portfolio case study — especially because the Client and My Role headings never change. You are only editing the main title and two paragraphs.

With Block Variations, you can create a variation of a core Columns block called Project Intro that will have the columns, and inner blocks already defined. This example is a bit more involved, so we’ll build it out step-by-step.

Let’s start with registering the variation:

wp.blocks.registerBlockVariation(   'core/columns', {     name: 'project-intro',     title: 'Project Intro',     scope: ['inserter'],     innerBlocks: [       ['core/column'],       ['core/column'],       ['core/column'],     ],   } );

We are taking this example a bit further than the first one, so why not add a custom portfolio icon from the Dashicons library that’s baked right into WordPress? We do that with the icon property.

wp.blocks.registerBlockVariation(   'core/columns', {     name: 'project-intro',     title: 'Project Intro',     icon: 'portfolio',     scope: ['inserter'],     innerBlocks: [       ['core/column'],       ['core/column'],       ['core/column'],     ],   } );

This will make the block available in the block menu with our icon:

Variation with a custom icon in the Block Inserter.

The next important thing happens on where we add inner blocks:

wp.blocks.registerBlockVariation(   'core/columns', {     name: 'project-intro',     title: 'Project Intro',     icon: 'portfolio',     scope: ['inserter'],     innerBlocks: [       ['core/column'],       ['core/column'],       ['core/column'],     ],   } );

But this only gives us three empty columns. Let’s add starter content and inner blocks to each of them. We can use the same pattern we use to declare a block template in the InnerBlocks component. We can add an object with block attributes as a second element in the array describing the block, and an array of inner blocks as the third element.

The first column will look like this:

['core/column', {}, [   ['core/heading', { level: 2, placeholder: 'Project Title'} ], ]]

…and the complete block variation is like this:

wp.blocks.registerBlockVariation (   'core/columns', {     name: 'project-intro',     title: 'Project Intro',     icon: 'portfolio',     scope: ['inserter'],     innerBlocks: [       ['core/column', {}, [         ['core/heading', { level: 2, placeholder: 'Project Title' }],       ]],       ['core/column', {}, [         ['core/heading', { level: 3, content: 'Client' }],         ['core/paragraph', { placeholder: 'Enter client info' }],       ]],       ['core/column', {}, [         ['core/heading', { level: 3, content: 'My Role' }],         ['core/paragraph', { placeholder: 'Describe your role' }],       ]],     ],   } );

Cool, now we can insert the whole section with just one click. Okay, it’s a few clicks, but still faster than without using the variations.

So what did we learn from this example?

  • And demos how to use the inner blocks within the variation
  • It shows how to define a custom icon for a variation

Use case: Four-column layout

You already know that columns are a default block type, and that there are a handful of options for different types of columns. A four-column layout isn’t one of them, so we can build that. But this introduces a new concept as well: scoping in context of block variations.

Some core blocks, like Columns, already offer variations out of the box. You can choose one of them after you insert the block on the page:

Showing the columns block inserted with 5 different layout options to display up to 3 columns at varying widths.
Block-scoped variations

Let’s say you use a four-column layout on your website as often as you use two-column one. That’s unfortunate, because there is no shortcut button to create four-column layout. Creating one is a bit annoying because it takes extra clicks to get to the Columns control after the block is inserted:

Showing the slider control to change the number of columns in the Block settings.

So, what can you do to improve this workflow? Right, you can add a Block Variation that will create a four-column layout. The only difference this time, compared to previous examples, is that it makes much more sense to include this variation inside the block placeholder, next to all other column layouts.

That is exactly what the scope option is for. If you set it to [block], the variation will not appear in the Block Inserter but in the variations once the block has been inserted.

wp.blocks.registerBlockVariation(   'core/columns', {     name: 'four-columns',     title: 'Four columns; equal split',     icon: <svg ... />,     scope: ['block'], // Highlight     innerBlocks: [       ['core/column'],       ['core/column'],       ['core/column'],       ['core/column'],     ],   } );
Four-column layout variation scoped to the block.
Hey, now we have a four-column option!

Isn’t that sweet?!

I’ve omitted the full SVG code for the icon, but it’s available if you need it.

To sum up scope: If it isn’t declared, the variation will appear in the Block Inserter and inside the block placeholder — specifically for blocks that support block-scoped variations. 

If we were to remove the scope parameter from the example above, here’s how the variation would appear in the inserter:

Four-column block variation in the block inserter.
Keep in mind that the icon sizes for variations within the block and and the block icons size are different. The custom icon for columns was intended for the block scope, that’s why it looks a bit out-of-place in this example.

So what did we learn from this example?

  • It explains the difference between the block and inserter scope for the variation.
  • We learned how to use SVG for variation icon.

That’s it!

As you can see, Block Variations are pretty powerful for building a lot of things, from different variations of buttons to complete page layouts.

I’d like to wrap this up with a quick recap of different APIs for block customizations and when to use them:

  • Use Block Styles if you need to alter the appearance of the block and adding a CSS class is enough for that.
  • Use Block Variations if you need to specify the default attributes for the block and/or add inner blocks to it.
  • If that’s not enough and you need to change the markup of the block, you are probably looking into filtering the block or creating a new one from scratch.

If you’ve had a chance to play with Block Variation, let me know what you think of them in the comments!

Resources

The post How to Use Block Variations in WordPress appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Block Links Are a Pain (and Maybe Just a Bad Idea)

As we noted in our complete guide, you can put an <a href=""> link around whatever chunks of HTML you like. Let’s call that a “block link.” Like you are wanting to link up an entire “Card” of content because it makes a big clickable target.

<a href="/article/"> <!-- display: block; -->   <div class="card">     <h2>Card</h2>     <img src="..." alt="...">     <p>Content</p>   </div> </a>

On that, Adrian Roselli:

Perhaps the worst thing you can do for a block link is to wrap everything in the <a href>.

[…] for a screen reader user the entire string is read when tabbing through controls. In the following example, the first link contains the heading, image (without declaring it as an image), and block of text, taking about 25 seconds to read before announcing it as a link. When tabbing, you do not always know the control type until the accessible name is complete.

(The example is a pretty normal looking card with a header, image, and paragraph.)

So don’t do that.

The alternative is to let the link be “normal” like just the header.

<div class="card">   <h2><a href="/article/">Article</a></h2>   <img src="..." alt="...">   <p>Content</p> </div>

The extending the “clickable area” of the link to cover the entire area.

.card {   position: relative; } .card h2 a::after {   content: "";   position: absolute;   top: 0;   left: 0;   width: 100%;   height: 100%; }

That works for the clickable area and solves the penalty to screen reader users.

But there is another problem that hurts both of these solutions, and it’s text selection. You can’t just put your cursor somewhere in the card and select text normally. The click activates the link, waiting for you to mouseup while still on the link to trigger it. You don’t get the ability to select inner parts of the text as you would probably expect. It doesn’t prevent the ability to select the text at all, but it makes it awkward and annoying.

I’m not sure that is easily solveable. Perhaps there is some exotic JavaScript solution that can detect if you’ve started to select text and then not trigger a click, but if you click without dragging then it does go to the link. Something about that is a bit red-flaggy to me though.

All in all, I’d say block links are just a bad idea. But I’d love to be proven wrong and see a really good implementation that solves all these issues.

The post Block Links Are a Pain (and Maybe Just a Bad Idea) appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Recreating the CodePen Gutenberg Embed Block for Sanity.io

Chris recently put out a neat CodePen Embed Block for the Gutenberg editor in WordPress. It allows you to embed a Pen just by dropping in its URL. From there, you get access to control the size, theme, and the default tabs that render on initial load. Super neat!

Having a live preview of the embedded Pen while writing is so handy!

But it got me thinking: How difficult would it be to recreate it with Sanity Studio’s Portable Text editor? (Spoiler: Not that difficult). Since I already knew how to do it, it took me under seven minutes from start to finish. This tutorial takes you through how to get up and running with a studio, and how to add the schemas and the custom preview component for a CodePen embed.

That felt so cool that I want to teach you how to do it as well. Let’s dive right into it.

Getting Sanity Studio up and running locally

First, you’ll need to install Sanity Studio locally on your machine. In this tutorial we will be using the blog studio that you can initiate from the command line, but you can also check out the different starters on sanity.io/create. You should be able to tag along with one of those too.

This tutorial assumes that you have a bit of knowledge of JavaScript. It will use a bit of React, but only a small part. You should have installed node and npm if you haven’t already.

Oh, and you’ll want the Sanity CLI, which you can snag with the command line:

npm install --global @sanity/cli

Once the installation is done, you can initiate a new Sanity Studio with a new project by running the command sanity init. It will let you log in with your Google or GitHub account (or make a new account with an email/password). Give your project a name and follow the instructions. When given the options for a project template, choose the blog one:

? Select project template   Movie project (schema + sample data)   E-commerce (schema + sample data) ❯ Blog (schema)   Clean project with no predefined schemas

After completing the steps, change directory (cd) into the new project folder and open it in your favorite code editor. To start the developer server that will also hot reload your studio when you make changes, run sanity start. To stop this server, you press ctrl + C in most command line tools.

Adding the schemas for a CodePen embed

Schemas define which document types that are available in the Studio, and which input fields they have. These schemas are defined in JavaScript objects that you import into the schemas.js file, where they are exported as a function that the Studio translates into its UI. There’s a lot you can do with these schemas, but in this tutorial, we will keep it reasonably simple.

Start with adding a new file inside /yourproject/schemas called codepen.js. Then type in this code:

export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

Then you can go to /yourproject/schemas/schema.js and add the following two lines of code to it:

import createSchema from "part:@sanity/base/schema-creator"; import schemaTypes from "all:part:@sanity/base/schema-type";  import blockContent from "./blockContent"; import category from "./category"; import post from "./post"; import author from "./author"; import codepen from "/codepen.js"; // <= first import the object  export default createSchema({   name: "default",   types: schemaTypes.concat([     post,     author,     category,     blockContent,     codepen // <= add it to the schema types array   ]) });

So what did we just do? Well, we have now made this CodePen object available as a type in other schemas in the Studio. In other words, you can now add type: 'codepen' to get those fields anywhere else in the schema code where you add fields. Adding this type to the rich text field is also our next step. Hang on!

Adding the CodePen field to the rich text editor

Before diving into the code bit, let us take a step back and look at what is going on in terms of the data formats we operate with, and how WordPress and Sanity differ slightly.

While Gutenberg stores rich text as JSON in its runtime (which is great!), what developers end up dealing with is mostly this content as HTML and JSON objects inside of HTML comments.

Sanity stores and distributes rich text content as Portable Text, which developers then serializes in their frontends. That means that you get fine-grained control over how rich text content is rendered by letting you use custom components for your favorite framework, either it's ReactVueSvelte, or .NETPHP, or even Markdown.

In other words, you store your content as structured data in Sanity’s backend, and then decide how you want to use the data inside your frontend components. But enough exposition, let's get back to the code!

Open /schemas/blockContent.js and notice that it's of the type array. Yes, rich text is an array of different types, where one of them has to be of the block type (in which text paragraphs are stored). So the simplest way of making rich text is the following schema definition:

export default {   name: "body",   type: "array",   title: "Body",   of: [     {       type: "block"     }   ] };

Now, blockContent.js has a bunch of more stuff. You can see styles, lists, marks, and so on. All defining which properties should be available for the author. In the top array, there are two types block and image. We are going to add the third one, codepen:

export default {   title: "Block Content",   name: "blockContent",   type: "array",   of: [     {       type: "block"       // ...     },     {       type: "image",       options: { hotspot: true }     },     {       type: "codepen"     }   ] };

Save the file, and that's it! If you now run sanity start in your command line (assuming you haven't already), and open the Studio on https://localhost:3333, you should be able to find your new field in the rich text editor under the "post" type:

Sanity Studio with a CodePen button in the Rich Text editor.

If you try out the new button, you'll get a modal with the URL field that you defined in the previous section. Feel free to add the URL from a cool CodePen that you have found. We will use this one from the legendary Sara Drasner; it's pretty cool.

Just showing the URL value in the editor isn't especially inspiring, though. So let's go ahead and add the actual CodePen embed so we can interact with it directly in the editor!

Adding the CodePen embed as a preview

Open /yourproject/schemas/codepen.js again. Now we are going to make a small React component for our preview. Start by importing React in the top, and the boilerplate for the React component that we will turn into the embed:

import React from "react";  const CodePenPreview = ({ value }) => {   return <pre>{JSON.stringify(value, null, 2)}</pre>; };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

The JSON.stringify stuff is a temporary little way of outputting the incoming data in a readable manner. You could also use console.log(value), but who has time to open the developer console?

Now you must tell Sanity how to use this component for the preview. As well as which of the fields in the object it should select for the value in the preview component.

import React from "react";  const CodePenPreview = ({ value }) => {   return <pre>{JSON.stringify(value, null, 2)}</pre>; };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   preview: {     select: {       url: "url"     },     component: CodePenPreview   },   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     }   ] };

The editor should look something like this after you saved your changes:

Cool! Now we want to take the url value and somehow integrate it with a CodePen embed. The easiest way to go about this is to fit the markup for CodePen’s iFrame embed, and fit into our preview component in React.

The original iFrame element will look like this:

<iframe height="265" style="width: 100%;" scrolling="no" title="React Animated Page Transitions" src="https://codepen.io/sdras/embed/gWWQgb?height=265&theme-id=dark&default-tab=js,result" frameborder="no" allowtransparency="true" allowfullscreen="true">   See the Pen <a href='https://codepen.io/sdras/pen/gWWQgb'>React Animated Page Transitions</a> by Sarah Drasner   (<a href='https://codepen.io/sdras'>@sdras</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe>

If we paste this snippet into our preview component, it will almost work. In order to make it JSX-compatible you'll have to some few changes to some of the HTML-attributes. Make sure that you change:

  • style="width: 100%;" to style={{width: "100%"}}
  • frameborder="no" to frameBorder="no"
  • allow-transparency="true" to allowTransparency
  • allow-fullscreen="true" to allowFullScreen

You can remove the content (links, etc.) inside of the iframe, because it isn't particularly useful inside the studio. What we should end up with is something like this:

import React from "react"; import Codepen from "react-codepen-embed";  const CodePenPreview = ({ value }) => {   return (     <iframe       height="265"       style={{ width: '100%' }}       scrolling="no"       title="React Animated Page Transitions"       src="https://codepen.io/sdras/embed/gWWQgb?height=370&theme-id=dark&default-tab=js,result"       frameBorder="no"       allowTransparency       allowFullScreen     />); };  // ...

When saved, we should be able to see the CodePen embed inside the rich text editor:

Notice that the iFrame has an embed URL with some parameters for how it should be displayed. Of course, we could've asked someone to dive into CodePen to obtain this URL, but it's probably better for to use the regular one. We'll take the effort to reassemble into what we need:

The last part is to take the URL from the field, and get the hash and user out of it.

We split the URL string on forward slashes into an array. Then we use array destructuring to assign the different array elements to a variable. Since we only need the user and the hash we leave the other positions empty. This method isn't bulletproof, as it assumed a specific format for the URL, but it works for this example. Then we reassemble the embedUrl by using template literals.

import React from "react";  const CodePenPreview = ({ value }) => {   const { url } = value;   const splitURL = url.split("/");   // [ 'https:', '', 'codepen.io', 'sdras', 'pen', 'gWWQgb' ]   const [, , , user, , hash] = splitURL;   const embedUrl = `https://codepen.io/$ {user}/embed/$ {hash}?height=370&theme-id=dark&default-tab=result`;   return (     <iframe       height="370"       style={{ width: '100%' }}       scrolling="no"       title="CodePen Embed"       src={embedUrl}       frameBorder="no"       allowTransparency       allowFullScreen     />   ); }; // ...

Save the changes and voilá; we're pretty much done with the custom CodePen block!

Taking it further

Now, you probably noticed that Chris had put more settings into his custom block. Nothing is stopping us from doing the same! If we look up the documentation for the React CodePen embed component that we installed, we'll find a table of properties that it can take. We can add these as fields in the schema definition. For example, if we wanted to add the themeId, we could do it as follows:

import React from "react"; import Codepen from "react-codepen-embed";  const CodePenPreview = ({ value }) => {   const { url, themeId = "dark" } = value; // <= add themeId here, default it to "dark"   const splitURL = url.split("/");   // [ 'https:', '', 'codepen.io', 'sdras', 'pen', 'gWWQgb' ]   const [, , , user, , hash] = splitURL;   const embedUrl = `https://codepen.io/$ {user}/embed/$ {hash}?height=370&theme-id=$ {themeId}&default-tab=result`; // <= add themeId here   return (     <iframe       height="370"       style={{ width: '100%' }}       scrolling="no"       title="CodePen Embed"       src={embedUrl}       frameBorder="no"       allowTransparency       allowFullScreen     />   ); };  export default {   name: "codepen",   type: "object",   title: "CodePen Embed",   preview: {     select: {       url: "url",       themeId: "themeId" // <= add themeId here     },     component: CodePenPreview   },   fields: [     {       name: "url",       type: "url",       title: "CodePen URL"     },     // Add the new field below     {       name: "themeId",       type: "string",       title: "Theme ID",       description: 'You can use "light" and "dark" also.'     }   ] };

Conclusion

We just looked at how schemas for Sanity Studio work, and learned how to make previews for custom components to boot! Hopefully, you now know enough to make pretty much any custom component with a preview using these same principles. If you do, I would love to know about it either on Twitter or in the comments.

The post Recreating the CodePen Gutenberg Embed Block for Sanity.io appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Web Component for a Code Block

We’ll get to that, but first, a long-winded introduction.

I’m still not in a confident place knowing a good time to use native web components. The templating isn’t particularly robust, so that doesn’t draw me in. There is no state management, and I like having standard ways of handling that. If I’m using another library for components anyway, seems like I would just stick with that. So, at the moment, my checklist is something like:

  • Not using any other JavaScript framework that has components
  • Templating needs aren’t particularly complex
  • Don’t need particularly performant re-rendering
  • Don’t need state management

I’m sure there is tooling that helps with these things and more (the devMode episode with some folks from Stencil was good), but if I’m going to get into tooling-land, I’d be extra tempted to go with a framework, and probably not framework plus another thing with a lot of overlap.

The reasons I am tempted to go with native web components are:

  • They are native. No downloads of frameworks.
  • The Shadow DOM is a true encapsulation in a way a framework can’t really do.
  • I get to build my own HTML element that I use in HTML, with my own API design.

It sorta seems like the sweet spot for native web components is design system components. You build out your own little API for the components in your system, and people can use them in a way that is a lot safer than just copy and paste this chunk of HTML. And I suppose if consumers of the system wanted to BYO framework, they could.

So you can use like <our-tabs active-tab="3"> rather than <div class="tabs"> ... <a href="#3" class="tab-is-active">. Refactoring the components certainly gets a lot easier as changes percolate everywhere.

I’ve used them here on CSS-Tricks for our <circle-text> component. It takes the radius as a parameter and the content via, uh, content, and outputs an <svg> that does the trick. It gave us a nice API for authoring that abstracted away the complexity.

So!

It occurred to me a “code block” might be a nice use-case for a web component.

  • The API would be nice for it, as you could have attributes control useful things, and the code itself as the content (which is a great fallback).
  • It doesn’t really need state.
  • Syntax highlighting is a big gnarly block of CSS, so it would be kinda cool to isolate that away in the Shadow DOM.
  • It could have useful functionality like a “click to copy” button that people might enjoy having.

Altogether, it might feel like a yeah, I could use this kinda component.

This probably isn’t really production ready (for one thing, it’s not on npm or anything yet), but here’s where I am so far:

Here’s a thought dump!

  • What do you do when a component depends on a third-party lib? The syntax highlighting here is done with Prism.js. To make it more isolated, I suppose you could copy and paste the whole lib in there somewhere, but that seems silly. Maybe you just document it?
  • Styling web components doesn’t feel like it has a great story yet, despite the fact that Shadow DOM is cool and useful.
  • Yanking in pre-formatted text to use in a template is super weird. I’m sure it’s possible to do without needing a <pre> tag inside the custom element, but it’s clearly much easier if you grab the content from the <pre>. Makes the API here just a smidge less friendly (because I’d prefer to use the <code-block> alone).
  • I wonder what a good practice is for passing along attributes that another library needs. Like is data-lang="CSS" OK to use (feels nicer), and then convert it to class="language-css" in the template because that’s what Prism wants? Or is it better practice to just pass along attributes as they are? (I went with the latter.)
  • People complain that there aren’t really “lifecycle methods” in native web components, but at least you have one: when the thing renders: connectedCallback. So, I suppose you should do all the manipulation of HTML and such before you do that final shadowRoot.appendChild(node);. I’m not doing that here, and instead am running Prism over the whole shadowRoot after it’s been appended. Just seemed to work that way. I imagine it’s probably better, and possible, to do it ahead of time rather than allow all the repainting caused by injecting spans and such.
  • The whole point of this is a nice API. Seems to me thing would be nicer if it was possible to drop un-escaped HTML in there to highlight and it could escape it for you. But that makes the fallback actually render that HTML which could be bad (or even theoretically insecure). What’s a good story for that? Maybe put the HTML in HTML comments and test if <!-- is the start of the content and handle that as a special situation?

Anyway, if you wanna fork it or do anything fancier with it, lemme know. Maybe we can eventually put it on npm or whatever. We’ll have to see how useful people think it could be.

The post Web Component for a Code Block appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]