dark_mode

Custom Blocks

How to create new React components that automatically generate Storybook documentation and rich, composable Gutenberg blocks.


An Atlas Custom Block is all of the following:

  • A React component, so it can be used in the usual way, with no special rules.
  • A Gutenberg block, with controls corresponding to all properties, optional child blocks, and a WYSIWYG editing experience.
  • A Storybook Story, with examples, defaults, and the right controls for all properties.

Extending our work from the pure-layout block, we'll use the same files, but this time we'll need to create real implementations of the Gutenberg editor and the React component.

Hello, World!

You know how this begins! The simplest-possible block that just displays "Hello, World."

Start by creating a new directory to hold the block, similar to the pure-layout block example. Here we're assuming that's in /blocks/hello.

Exactly as with the pure-layout block, create a meta.ts file to hold the meta-data about the block. This time, there will not be a layout:

import { Props, SuperBlockMeta } from '@asmartbear/gutenberg-bridge/dist/types';
export interface HelloProps extends Props {
// nothing here... yet!
}
const definition: SuperBlockMeta<HelloProps> = {
uiType: 'tutorial/hello',
blockMeta: {
title: "Hello!",
description: "A 'hello, world' tutorial custom block",
},
propsMeta: {
// nothing here... yet!
},
gutenbergMeta: {
dashicon: "admin-appearance",
category: "tutorial",
},
};
export default definition;

We haven't introduced a new concept (yet!). Note that we decided to create a Typescript interface for properties, although there's nothing there yet. We'll fill that in soon!

Next, create the React component. Create the index.tsx file (note the "x"!), again similar to the layout component, but this time we'll need to supply a React component:

import { SuperBlock, registerWithDefaults } from "@asmartbear/gutenberg-bridge/dist/react-util";
import { default as meta, HelloProps } from './meta';
export const tutorialHelloDefinition: SuperBlock<HelloProps> = {
...meta,
createComponent: () => {
return (
<div style={{ fontStyle: "italic" }}>
Hello, World!
</div>
);
},
};
export default registerWithDefaults(tutorialHelloDefinition);

Walking through the code:

  • We imported from a subdirectory @asmartbear/gutenberg-bridge/dist/react-util, because this keeps React-specific code separate from generic code, allowing this module to work with non-React systems.
  • We use a Typescript type for a custom React block called SuperBlock, and a utility function registerWithDefaults() that registers our block with the system, and also creates the React component. The React component's properties are all options; missing properties are automatically filled in with defaults (which you'll supply shortly). This allows you to assume all properties exist in your component implementation.
  • We include everything from meta.ts. Which begs the question: Why is meta.ts a separate file? The answer is: meta.ts ends up holding the code shared between Gutenberg and React. This is especially powerful when you start creating your own subroutines, accessing other code in your project, or even other included npm modules, and everything still just works in Gutenberg!
  • ...meta reuses the shared meta-data in React.
  • createComponent is a functional React component (i.e. Typescript type React.FC<HelloProps>).

Although we haven't created the Gutenberg part yet, we can already use this as a normal React component. Let's do this. In the main page of the application, import the block and use it like any other React component. Here it is, together with the other things we've built so far, just inserted after the title:

import Hello from '../blocks/hello'
...
return <>
<AtlasContainer theme={theme}>
<Box marginLeft={1} marginRight={1} backgroundColor="backdrop">
<Box marginBottom={1}>
<Prose preset="h1" content="Hello Atlas" />
</Box>
<Hello /> {/* <-- right here */}
<AtlasGutenberg htmlRaw={raw} />
<CopyBlockConfig />
</Box>
</AtlasContainer>
</>;

The result may be boring, but it shows things are working so far:

[image]

Installing custom blocks in Gutenberg

Unlike pure-layout blocks, custom blocks have custom editors in Gutenberg. Fortunately, there's boiler-plate code from Atlas you can leverage to get started easily. In fact, some blocks never need to extend the boiler-plate. But, it's also nice that you can implement absolutely anything that you could implement directly in Gutenberg, if you want to create a special, powerful editing experience.

Create the editor.js file in the block directory, again like the pure-layout block. Here's the boilerplate:

import { getEditorPanels } from 'gutenberg';
export default (params) => {
const { editor } = params;
return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div style={{ fontStyle: "italic" }}>
Hello, World!
</div>
</div>
);
}

Notice how we used the same inner <div> implementation that we used in the React component. We want to render the component identically in Gutenberg, as in React.

Here's what the rest of the code is doing:

<div {...editor.useBlockProps()}>
This creates a proper Gutenberg block. You'll always wrap your block like this. This is standard Gutenberg code, not Atlas code.
{getEditorPanels(params)}
This is an Atlas-specific utility that automatically creates all the Inspector controls for the properties of your block. (The "Inspector" is that right sidebar of controls when you click on a block in the editor.) Often this is all you ever need! But, you can also add custom controls in this area.

Let's test it. We're already configured to scan the entire /blocks directory in our package.json configuration, so the only thing we have to do is put a new copy of the theme configuration into atlas-block.config.json. Adding a block changes that configuration, so that's why we have to recreate it. Then, just run npm run wp-theme, get the new theme into WordPress, and see our new block:

[image]

Custom block properties

This block is boring! Let's add some properties. Properties are also called "properties" in React, and are called "attributes" in Gutenberg, but either way, they are typed name/value pairs of data. Unlike in React, Atlas block properties must be simple data types like strings, numbers, boolean, colors, angles, and HTML. This is mandatory, because properties are stored as JSON by Gutenberg, and need to integrate with controls inside the Gutenberg editor, therefore they have to be simple enough to conform to all those systems.

Add properties by adding them to the Typescript interface, and then by adding propsMeta entries that describe each property sufficiently for the system to automatically create the Gutenberg editor controls. It all happens in meta.ts.

The first property to add, is a way to change the message. It will be a simple string:

export interface HelloProps extends Props {
message:string,
}
const definition: SuperBlockMeta<HelloProps> = {
...
propsMeta: {
message: {
type: "string",
title: "Message",
description: "Text to display inside the component.",
defaultValue: "Hello, World!",
}
},
};

If you make a typo, you will quickly discover that Typescript has your back. If you are missing a property, or name it incorrectly, or don't use the right data type for defaultValue, you'll get a compile-time error, probably right in your IDE.

This is already sufficient for the system to create a control in the sidebar of the Gutenberg editor, but it's not going to display yet, because we didn't update the React component nor the editor code to use this new property. So let's do that. First, the React component:

createComponent: ({ message }) => {
return (
<div style={{ fontStyle: "italic" }}>
{message}
</div>
);
},

If you run this code now, you'll notice it still displays "Hello, World!", even though you never supplied the new message property in React. That's the defaultValue from propsMeta, as promised.

To really test this code, supply a message property in React.

Now we repeat this code-change in the Gutenberg editor, but it works slightly differently. Here we're using Gutenberg and WordPress conventions, names, and data types. Specifically, we extract attributes from the input parameters -- that's what Gutenberg calls "properties." We then use it in the same way as React:

import { getEditorPanels } from 'gutenberg';
export default (params) => {
const { editor, attributes } = params;
return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div style={{ fontStyle: "italic" }}>
{attributes.message}
</div>
</div>
);
}
[image]

Notice how data from propsMeta is used, like the title and help text. When you edit the content of the message control, the widget updates immediately.

Now the exciting part! Save the Gutenberg editor with some content that you typed in Gutenberg, then reload your web application. Your block will display with the Gutenberg configuration. We've now completed the loop of creating a block in React, which is powered by a WYSIWYG block in Gutenberg.

Property types

There are many property types and optional fields that go with them. A good reference is the Typescript types for PropMeta -- there you will find all the optional fields with documentation. For examples, see the Atlas blocks, which use all of those features.

Controls will automatically turn into sliders for integers or floats with small min/max ranges, color-pickers with theme-specific palettes for colors, radio buttons for enumerations that have a small handful of items, drop-down lists for enumerations with many items, and even a special control for angles.

Using a common codebase to style both Gutenberg and React

One thing you should be unhappy about in the foregoing example, is that we had to implement everything twice -- once for the React component, and once for the Gutenberg editor. This gets even worse when there are lot of properties. There is a better way: Use the fact that both React and Gutenberg can include meta.ts.

Here's a particularly convenient way to do this. First, add a few more properties to our example, and then we'll implement them in both places by writing code only once:

export interface TutorialHelloProps extends Super.Props {
message: string;
hasBorder: boolean,
rotation: number,
background: string,
}
...
propsMeta: {
message: {
type: "string",
title: "Message",
description: "Text to display inside the component.",
defaultValue: "Hello, World!",
},
hasBorder: {
type: "boolean",
title: "Border?",
description: "Controls whether to draw a thin border.",
defaultValue: false,
},
rotation: {
type: "float",
title: "Rotation Angle",
description: "Rotates the component this number of degrees.",
defaultValue: 0,
minValue: -10,
maxValue: 10,
},
background: {
type: "color",
title: "Background Color",
description: "Sets a solid background color.",
defaultValue: "transparent",
},
},

Now we'll implement these properties as CSS styles. Create a function for this purpose inside meta.ts. Everything exported here can be included in both places, so this can become as complex as needed. Let's keep it simple, but type-safe:

type CommonBlockStyling = {
style: React.CSSProperties,
}
export function getCommonBlockStylings(props: HelloProps): CommonBlockStyling {
return {
style: {
border: props.hasBorder ? '1px solid black' : 'none',
transform: `rotate(${props.rotation}deg)`,
padding: '1em',
background: props.background,
}
};
}

Now use this style-generating function in React. Because of how we defined the return-result, we can just "expand" the result into the React component:

// Now import getCommonBlockStylings() too:
import { default as meta, HelloProps, getCommonBlockStylings } from './meta';
...
// Now load all properties as a single object
createComponent: (props) => {
return (
// Terse way to import everything directly into React
<div {...getCommonBlockStylings(props)}>
{props.message}
</div>
);
},

The Gutenberg editor is just as easy. We use the same trick, remembering that the properties are called attributes:

import { getEditorPanels } from 'gutenberg';
// Now import getCommonBlockStylings(); yes even from a Typescript file!
import { getCommonBlockStylings } from './meta';
export default (params) => {
const { editor, attributes } = params;
return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div {...getCommonBlockStylings(attributes)}>
{attributes.message}
</div>
</div>
);
}

Sharing code is convenient for the developer, and ensures that React and Gutenberg will work identically, which in turn ensures Gutenberg is as WYSIWYG as possible:

[image]

Themes: Palettes, text, and dark mode

If you run this example, you'll notice that the Gutenberg color palette is broken. If you set a "custom" color (i.e. not from the palette), that works, but the palette colors do nothing. The reason is: Palette colors are stored using the names you specified in the theme; they're not stored as the actual CSS color. The benefit of this system, is that you can tweak those colors later without having to change anything else, and you can tweak things like your preferred constrasting text color. The bad news is: You can't just give CSS those colors. You have to translate from palette colors, to CSS colors. (But also still support custom CSS colors!)

Fortunately, this is easy to do. The theme object has a utility function that does this for us. Let's start with meta.ts, where we ask for the theme object to be passed in as an argument to the function:

// Import the Theme data type, with lots of utilities
import { Theme } from '@asmartbear/gutenberg-bridge/dist/themes'
// Add the Theme as a parameter for others to pass in
export function getCommonBlockStylings(
theme: Theme,
props: HelloProps
): CommonBlockStyling {
...
// Use this utility function to get the CSS color
background: theme.getActualBackgroundColor(props.background),
...
}

(You'll find lots of other useful utilities on that object.)

Now let's update the React component to pass in the current theme. There's a utility function that does all the work:

// Import "useTheme" from the React Utilities
import { SuperBlock, registerWithDefaults, useTheme } from "@asmartbear/gutenberg-bridge/dist/react-util";
...
createComponent: (props) => {
// Getting the current Theme object is this easy!
// Then just pass it into our function.
const theme = useTheme()
return (
<div {...getCommonBlockStylings(theme, props)}>
{props.message}
</div>
);
},

In the Gutenberg editor, it's even easier, because the theme object is already passed into your function. You don't even need another import! We just need to extract it from the input parameter, and pass it over to the common function:

export default (params) => {
// The "theme" is another parameter we can grab.
const { theme, editor, attributes } = params;
// Pass it into our function below:
return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div {...getCommonBlockStylings(theme, attributes)}>
{attributes.message}
</div>
</div>
);
}

Now the palette will work properly in both Gutenberg and the front-end. If you change the color associated with that item in the theme, all existing configuration will update automatically, thanks to this system.

To see how powerful this can be, try using a dark background. The black text becomes unreadable. But, Atlas has a system that automatically detects that condition, and supplies an appropriate text color. This requires just a one more line of code:

...
{
...
background: theme.getActualBackgroundColor(props.background),
color: theme.getTextColorForBackgroundColor(props.background),
}

Even more exciting: Try changing to dark-mode using the second parameter on StandardThemeGenerator, and you'll see the backgroud color automatically selects something more appropriate, and the text color will be readable.

More powerful still: Suppose we allow the foreground text color to be set with another property called foreground. That's fine... except again if the contrast between foreground and background is not big enough, the system can pick the closest color to the desired one, that contrasts sufficiently to be readable. It's as easy as this:

...
{
...
background: theme.getActualBackgroundColor(props.background),
color: theme.getActualForegroundColor(props.foreground, props.background),
}

Atlas Block SCSS

Adding styles directly to elements is efficient for small amounts of CSS, especially when the style depends on properties. But for complex components, or components that need to use CSS selectors for complex behaviors, you need proper CSS. Even better, SCSS!

To add CSS styles, create a file called styles.scss in your block's directory. Styles are picked up by the npm run wp-theme theme builder, bundled into the WordPress theme, and collected and processed down to regular CSS into the front-end CSS file that you named using block-css-output-file in package.json.

Use CSS classes in the usual way, with global scope, so for example if your class is ".my-stuff { color: red; }", you could apply it like: <div className="my-stuff">...</div>.

Usually you want styles in both React and Gutenberg, to maintain the WYSIWYG editing experience, but sometimes you want CSS to apply only to Gutenberg or only to React. (In fact, in a later section in this chapter, we show an example of Gutenberg-only.) To do this, you can use a special SCSS variable to determine which context you're in. Put the following line at the top of your block's styles.scss:

$in-gutenberg-editor: false !default;

In SCSS, !default means "set this variable, only if it's not already set." The theme builder sets this variable to true when it's generating Gutenberg CSS; in other contexts it's undefined, so this line will cause the variable to be false. You can use it for conditional styles like this:

$in-gutenberg-editor: false !default;
@if $in-gutenberg-editor {
// styles only if inside Gutenberg
.wp-block-my-block {
// ...more stuff
}
}
@if not $in-gutenberg-editor {
// styles only if not in Gutenberg
}

Often you want to apply CSS classes conditionally, based on the value of Atlas block properties. A nice way to do this is once again with meta.ts, so that both Gutenberg and React share that class configuration. Because of the way we organized our code, we can do this entirely inside meta.ts, and not even touch our React code nor our Gutenberg editor code!

In meta.ts, we'll add a new Atlas block property to demonstrate this feature, and a className field to the returned object, which will inject our new CSS classes. We'll have one CSS class for the component, and another CSS class applied conditionally, based on whether the new Atlas block property is set to true:

export interface HelloProps extends Props {
...
isRounded: boolean,
}
const definition: SuperBlockMeta<HelloProps> = {
...
propsMeta: {
...
isRounded: {
type: "boolean",
title: "Rounded",
description: "Controls whether the corners of the box are rounded.",
defaultValue: false,
},
},
};
type CommonBlockStyling = {
// Add a dynamic list of CSS classes
className: string,
style: React.CSSProperties,
}
export function getCommonBlockStylings(theme: Theme, props: HelloProps): CommonBlockStyling {
return {
// A new "tutorial-hello" CSS class & conditional "rounded" class
className: 'tutorial-hello ' + (props.isRounded ? 'rounded' : ''),
style: {
...
}
};
}

Then we implement these CSS classes in styles.scss. For a little safety, we'll scope the conditional class to our component; this is easy to do in SCSS:

.tutorial-hello {
&.rounded {
border-radius: 1em;
padding: 1.5em;
}
}

That's it! Rebuilding the theme gives us this new functionality in Gutenberg and in React.

Adding a Gutenberg text editor

Our component works, but we could enhance the editing experience by allowing the content author to type content directly onto the component, rather than having to type into a little field in a sidebar. Either way, we still want to use the message property, but the editing experience can be better! This is strictly Gutenberg coding -- there's nothing we need to change with our block or live React component.

Gutenberg has a variety of visual editing components. We need the one called PlainText. Replace the interior of the content div with an editor, instead of the static text of the message property:

...
// Now we're also using "setAttributes"
const { theme, editor, attributes, setAttributes } = params;
...
return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div {...getCommonBlockStylings(theme, attributes)}>
<editor.PlainText
value={attributes.message}
onChange={
(newValue) => setAttributes({ message: newValue })
}
/>
</div>
</div>
);

Note the new use of setAttributes() and some new code for editor.PlainText. This is partially documented below, but because this is just straightforward Gutenberg code, see that documentation for all the details.

You might discover some surprises when you use editor components like PlainText, because the Gutenberg editor proactively applies styles to objects, rather than inheriting styles like colors and typography. In this example we have a "bug" (not our fault!) where the <textarea> that Gutenberg uses for the editor is white, even if the background of the component has been changed. This is a broken UI experience:

[image]

Block styles are your friend here. In this case, assuming you have a component stylesheet set up (described above), you could add the following selector, which overrides the Gutenberg styles for <textarea>. Keep in mind that Gutenberg adds a wp-block-YOUR-NAME CSS class automatically, so we can leverage that in our CSS.

$in-gutenberg-editor: false !default;
@if $in-gutenberg-editor {
.wp-block-tutorial-hello textarea {
background: inherit;
color: inherit;
}
}

Now it works as expected:

[image]

Child blocks

So far, we haven't used the idea of child-blocks or React "children." Let's do that now. With Atlas Custom Blocks, the idea is that children can be any other Atlas Block, matching what the user can do in the Gutenberg editor.

In both React and Gutenberg, there is a single concept of a (possibly empty) list of "children." This is also how Atlas Custom Blocks works. We'll demonstrate this by replacing our string-typed message property with child blocks.

In meta.ts, delete the message property from the HelloProps interface, and from propsMeta. That's all there is to do there.

In index.tsx we can use normal React children. Simply replace message with children:

createComponent: (props) => {
const theme = useTheme()
return (
<div {...getCommonBlockStylings(theme, props)}>
{props.children /* <-- right here! */}
</div>
);
},

On the Gutenberg side of the house, in editor.js we also need to replace attributes.message (or some fancier editor) with something that tells Gutenberg "child blocks go here." Thanks to a built-in Atlas utility, this is trivial:

return (
<div {...editor.useBlockProps()}>
{getEditorPanels(params)}
<div className={className} style={styles}>
{getInnerBlocks(params) /* <-- like this */ }
</div>
</div>
);

The system does the rest! Gutenberg will show an "add block" UI, allowing blocks to be added:

[image]

On the React side, Atlas automatically converts those children to normal React components, so by the time your component receives it, the children are already good to go.

There are a few optional settings you can use to tweak how child blocks work in the editor, for example, limiting to just one, limiting which types of blocks can be added, or adding blocks horizontally instead of vertically. See the Typescript documentation on the gutenbergMeta and blockMeta sections for details.

Editor details: The input properties

The various inputs to the default editor.js function are given as a single object with fields, hence the destructuring. (This allows us to add additional parameters without breaking existing code.) Here's what those fields are:

blockJson
The contents of block.json, i.e. the standard WordPress block meta-data. Besides the usual fields, there is an additional key called atlas that contains more meta-data sourced from the block, including propsMeta, and some pre-computed values that can drive dynamic behavior inside WordPress.
blocks
This is the window.wp.blocks module, a.k.a. @wordpress/blocks. Many components don't need to use it.
editor
This is the window.wp.blockEditor module, a.k.a. @wordpress/block-editor. This is the newer version of what used to be called window.wp.editor. You need this for essential functionality, as well as using visual input components such as RichText and PlainText. The component source tree is a good way to see what choices you have.
__     (double underscore)
This is the wp.i18n.__ function, a.k.a. from the @wordpress/i18n module. This is the standard WordPress function for wrapping strings that need to be translated (both in Javascript and in PHP). Just wrap strings __("like this").
components
This is the window.wp.components module, a.k.a. @wordpress/components. Many times you don't need this, because the built-in utilities create property components for you. The source tree is a good way to see what choices you have.
attributes
These are your React component properties -- the same data that you defined for your component. In the Gutenberg Editor these are called "attributes," hence the change of name to match the context. These are read-only, like React properties.
setAttributes
A function that takes a map of property names to new values, used to update properties any time, but especially when the user changes some input or control. This is similar to React state, although with Gutenberg blocks the notions of "React property" and "React state" are combined into a single concept of the "attributes." As an example, we could toggle the hasBorder property of our component with:
setAttributes({ hasBorder: ! attributes.hasBorder }).
className
The standard block class name you should apply to your block. This is standard Gutenberg practice. It follows an obvious pattern; in our example, this will be wp-block-tutorial-hello.
clientId
This is an internal Gutenberg ID which you rarely need. If you're using advanced operations to do things like querying existing inner-blocks, you'll need this value.
theme
The Atlas theme object, as we've seen already.
styleContext
Style context is an advanced object, available both in Gutenberg and React, in which styles from parents are inherited by children. For example, if a Box is the parent to many objects, and sets its background color, children would need to know that, in order to produce contrasting text colors. The child objects will not have their own background set -- they'd need to "know" that "some parent" has that background. Style context does exactly this, automatically.

Another little feature: When you have a lot of controls, it can be nice to separate them into sections. You can see that the system already did that in the case of the background color -- it has its own section title and ability to expand or contract. To do this, simply add a sectionTitle property to each entry in propsMeta. Properties with the same title will be collected into that section.

Now that we can build our own kinds of blocks, we can explore an optional Atlas package which solves another common problem: What happens when you want a page mostly controlled by React, but with various "pockets" where Gutenberg blocks can be inserted.

  • Getting Started with the Gutenberg / React Bridge
    Display blocks from Gutenberg, and creating a WordPress Theme for WYSIWYG editing inside Gutenberg.
  • Atlas Blocks
    The built-in Atlas blocks, in React and in Gutenberg.
  • Atlas Layouts
    Custom Layout blocks: Layouts with controls
  • Atlas Custom Blocks
    How to create new React components that automatically generate Storybook documentation and rich, composable Gutenberg blocks.
  • Atlas Slabs
    Mocking up and filling sections within a React page, using sections within Gutenberg.
  • Atlas Theming
    A complete system of color, typopgraphy, and layout, supporting dark mode, Gutenberg, and Storybook
  • Storybook Integration
    How to automate Storybook stories for everything - custom, WordPress Core, and Atlas blocks.
  • Storybook Reference
    Storybook-based interactive documentation for Atlas Core and Gutenberg Core blocks.