Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: React Components to Gutenberg Blocks #1522

Closed
theodesp opened this issue Aug 3, 2023 · 18 comments
Closed

RFC: React Components to Gutenberg Blocks #1522

theodesp opened this issue Aug 3, 2023 · 18 comments
Assignees
Labels
proposal RFC type: feature New functionality being added

Comments

@theodesp
Copy link
Member

theodesp commented Aug 3, 2023

Intro

This document provides a detailed proposal for converting existing React components into Gutenberg blocks. Gutenberg blocks are the fundamental building blocks used in the new WordPress editor, also known as Gutenberg. The goal of this RFC is to streamline the process of integrating React components into WordPress using Gutenberg, which will enhance the development process and make it more efficient and pleasant for the developer.

Motivation

WordPress powers a significant portion of the web, and React is a popular library for building user interfaces. Converting React components into Gutenberg blocks will enable developers to utilize the power of React in WordPress development. This approach will also make it easier to keep the code DRY (Don't Repeat Yourself) and maintain a consistent UI across different parts of a WordPress site.

For example, in many cases a developer has created many React Component that would like to use in the Gutenberg Editor. Currently there is no standard way to do this. As a matter of fact, most development of Gutenberg Blocks happen first and then are converted to React and not vice verca. This creates a lot of friction and slowing adoption of Headless WordPress initiative.

Detailed Design

The proposed solution involves creating a wrapper component that would serve as a bridge between React and Gutenberg. This wrapper component would be responsible for rendering the React component inside a Gutenberg block.

Here is an example of what the code might look like:

// MyFirstBlock.js
function MyFirstBlock({ style, className, attributes, children, ...props }) {
	const styles = {
		...style,
		backgroundColor: attributes.bg_color,
		color: attributes.text_color,
	};
	return (
		<div
			{...props}
			style={styles}
			className={className}
			dangerouslySetInnerHTML={{ __html: attributes.message }}
		/>
	);
}
// block.json
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "create-block/my-first-block",
	"version": "0.1.0",
	"title": "My First Block",
	"category": "widgets",
	"icon": "smiley",
	"description": "My block",
	"supports": {
		"html": false
	},
	"attributes": {
		"message": {
		   "type": "string",
		   "source": "text",
                    "selector": "div",
                    "default": ""
		},
		"bg_color": { "type": "string", "default": "#000000" },
        "text_color": { "type": "string", "default": "#ffffff" }
	},
	"textdomain": "my-first-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css"
}
// index.js
import './style.scss';
// import block.json
import metadata from './block.json';

// import Pure React Component
import MyFirstBlock from './MyFirstBlock';

// Register React Component in Gutenberg
import { registerFaustBlock } from '@faustwp/block-editor-utils';

registerFaustBlock(MyFirstBlock, {blockJson: metadata})

In this example,MyFirstBlock is a React component that we want to convert into a Gutenberg block.

There is a new package that exposes a helper for facilitating this conversion.

registerFaustBlock takes the following arguments:

  • component: The actual React Component to convert to into a Gutenberg block. Required.
  • metadata: Metadata object that contains several fields:
    • blockJson: The block.json object that describes the component attributes. Required.
    • edit: Provide a custom Edit function that describes the structure of your block in the context of the editor. Optional.
    • save: Provide a custom Save function that defines the way in which the different attributes should be combined into the final markup. Optional.

User Experience

The registerFaustBlock function will autogenerate custom edit and save functions that the Gutenberg Editor requires to register a Block.

Upon Component Registration, the Gutenberg Editor by default and using the blocks block.json will create editor fields that the user will be able to fill in.

Here is a screenshot of the above React Component that contains 3 fields. A textarea and two color pickers:

Screenshot 2023-08-03 at 10 53 27

By default, it will create relevant fields in the Form Editing View. In this mode, users will be able to fill in the component information using the generated control fields.

However, the developer has the option to provide hints to the registration function to assign certain fields on the sidebar asInspectorControls. Here is the same block with the two color controls located in the sidebar:

Screenshot 2023-08-03 at 10 55 58

Users would be able to switch between the form editing view to the presentational view using a preview button:

Screenshot 2023-08-03 at 11 49 38

Block Fields Configuration

This section is dedicated to provide a more detailed view of the available options to configure the default UX when using registerFaustBlock helper and to customise the block in the Editor according to your needs.

block.json fields

The registerFaustBlock helper will parse the respective block attributes and associate them with Editor Fields. The corresponding table represents the mapping logic between the block attributes and the associated fields:

type field comment
string TextControl Renders a TextControl field of type text
boolean RadioControl Renders a RadioControl field
integer TextControl Renders a TextControl field of type number
number TextControl Renders a TextControl field of type number
object TextAreaControl Renders a TextAreaControl field

Note: The array type won't be supported initially.

Labels

Each form field will consist of a Label and the associated control type. By default, the attribute name will be used as a label. Users would be able to override the default label by using the label property in the editorFields configuration object. (See below).

Ordering

Each form field will be rendered by their order they were defined in the block.json attributes list. Users would be able to override the default ordering by using the order property in the editorFields configuration object. (See below).

Providing hints with the editorFields configuration.

Since by default, when using the block.json supports only a limited types of fields, users would be able to provide more hints and configuration metadata to configure both the type of the control fields and their location. For example as mentioned in the demo component above, users can declare that some of the fields should be located in the sidebar and using a more appropriate control type like a ColorPicker. This will enhance the default editing experience.

To provide hints when registering a React Component with registerFaustBlock, create a new property called editorFields and attach this to the React Component:

// MyFirstBlock.js
function MyFirstBlock({ style, className, attributes, children, ...props }) {
  ...
}

MyFirstBlock.config = {
	name: "MyFirstBlock",
	editorFields: { // new field added
		message: { // Assume that type: "text" since the 'message' attribute is  type: "string"
			label: "My Message", // Use a custom label instead of default attibute name
			control: "textarea", // Use **TextAreaControl**  instead of **TextControl**
		},
		bg_color: {
			type: "color", // Use **ColorPicker**  instead of **TextControl**
			location: "inspector" // Specify location as sidebar or **InspectorControl**
		},
		text_color: {
			type: "color",
			default: "#eeeeee", // Overrides default value.
			location: "inspector", // Specify location as sidebar or **InspectorControl**
		},
	},
};

Here the component config section contains the new editorFields property that provides hints to the helper when building the final fields configuration. The associated field names should match the one with the block.json attributes. Each field will be merged together to form the final field configuration.

For example the first field message will have the final configuration:

"message": {
   type: "string",
   control: "textarea",
   default: "",
   label: "My Message",
   location: "editor"
}

This will create a TextAreaControl field with a default value of "", a label "My Message" and located in the Form Editor side.

Similarly the rest of the fields:bg_color and text_color will render a ColorPicker component and be located in the InpsectorControls area (block sidebar).

Available Controls

The registerFaustBlock helper with use conventional methods to merge block.json attributes and editorFields to create the final configuration for constructing the respective Editor Fields and InspectorControls.

The following control types will be available:

control field comment
color ColorPicker Renders a ColorPicker field
text TextControl Renders a TextControl field of type text
textarea TextAreaControl Renders a TextAreaControl field
radio RadioControl Renders a RadioControl field
select SelectControl Renders a SelectControl field
range RangeControl Renders a RangeControl field
number TextControl Renders a TextControl field of type number
checkbox CheckBoxControl Renders a CheckBoxControl field

Invalid combinations

We may chose to ignore certain invalid combinations of control types and attribute types. For example using type: "boolean" and control:"color".

In such cases it will fallback to regular TextControl with a warning.

Drawbacks

One potential drawback is that, depending on the complexity of the React component, the conversion process may not be straightforward. Some React components may rely on context or hooks that don't have a direct equivalent in Gutenberg. In such cases, you have the option to provide your own Custom Edit or Save functions when using the registerFaustBlock helper function.

Another caveat is that since we are registering the components as static blocks, then any use of useEffect, useState hooks are not allowed inside the component. See related SO answer.

However we could alleviate this fact by leveraging the Interactivity API in future iterations of this proposal.

In the meantime we will propose effective workarounds and limitations when using the components that would be compatible out of the box with our solution.

How to Contribute

Interested in hearing your thoughts on this and any possible enhancement's that you want to see materialised. Do the conventions make sense?

@theodesp theodesp added RFC type: feature New functionality being added proposal labels Aug 3, 2023
@jmusal
Copy link

jmusal commented Aug 3, 2023

To make sure I'm understanding this correctly, if only a blockJson is provided, and not the edit/save functionality, then the component/block will be displayed in the admin, but all editing will occur in the sidebar? Otherwise, I think I'm missing the connection between providing the schema/props for the block and the visual editor for editing conntent/props in place.

Also, I'm curious how nested blocks might be supported.

@theodesp
Copy link
Member Author

theodesp commented Aug 3, 2023

Hey @jmusal Thanks for the feedback. I still have to fill in a section in the RFC but I can answer some of your queries:

blockJson

Yes we need the blockJson since internally the registerFaustBlock helper will call the registerBlockType that Gutenberg needs. Maybe in the future we can figure out a way to autogenerate a block.json but not on this RFC.

not the edit/save functionality

You don't have to provide an edit or save functions since they will autogenerated for you by the registerFaustBlock helper. However the optional edit and save functions are there in case you want to provide your own implementation.

editing will occur in the sidebar

Yes. You will have to provide hints using the component config section (I will have to provide details in the Block Fields Configuration section) that will define where the editor fields should go. If you don't provide hints then by default all fields will be rendered within the block editing form.

nested blocks might be supported

For now you can just use regular React children prop to render the inner components. Since you will be starting with regular React Components you won't be using stuff from the @wordpress/block-editor package in your headless site.

By default the generated edit and save functions will use the standard <InnerBlocks /> and <InnerBlocks.Content /> which make sense to use only within the context of the Gutenberg Editor.

@justlevine
Copy link
Contributor

Where will these React Components coming from / should this packaged be used?

  • Are we duplicating them in our WP plugin/theme and our frontend app?
  • Are we releasing our components as libraries and importing them into both?
  • Are we defining them in just the theme/plugin or frontend (if so which) and using the Faust WP plugin to get them to the other?

Personally, I think a better immediate target for keeping things DRY and reducing friction (especially considering the noted limitation in the Drawbacks ) is to start by creating the opposite library: one that lets you seamlessly import Gutenberg blocks to use as components anywhere in your app - instead of the current situation where you need to manually maintain both wp-block components and matching React components.

Putting aside the significant comparative benefits , handling react-to-gutenberg first almost guarantees a breaking change for both the library and all the people who adopt it. It's a lot easier to extrapolate the DX in reverse.

@theodesp
Copy link
Member Author

theodesp commented Aug 3, 2023

Hey @justlevine Thanks for the comments. Let's get into them:

Where will these React Components coming from / should this packaged be used

We assume that you have already made React components that you created and you want to try in Gutenberg. That's without any prior knowledge of how to create them using the edit or save functions.

Are we duplicating them in our WP plugin/theme and our frontend app?
Are we releasing our components as libraries and importing them into both?
Are we defining them in just the theme/plugin or frontend (if so which) and using the Faust WP plugin to get them to the other?

That would be another lists considerations that @josephfusco can help you with. He is working on a POC to better handle syncing the components between WP plugin/theme and our frontend app. Maybe he can provide more clarification here.

This RFC deals specifically with what you need to use to take a React component and present it in the Gutenberg Editor without having to provide your own edit and save functions which greatly simplifies its scope.

one that lets you seamlessly import Gutenberg blocks to use as components anywhere in your app

That would be nice but not sure how feasible it is without introducing extra baggage or a lot of problematic scenarios. For example take any random Gutenberg block component from the core block-library and you will find almost immediately that it is definitely not portable outside the Gutenberg Editor context.

The promise of this RFC once implemented is that you can use the same React Component in both the Headless site and the Gutenberg Editor side without having to maintain a duplicate implementation for the Gutenberg Editor. It will be somehow a hybrid approach.

handling react-to-gutenberg first almost guarantees a breaking change for both the library and all the people who adopt it

Not necessarily. You can have your React Component developed on a package that you can import them into your blocks plugin so that Gutenberg can use them as well. The key thing is that the React Component will have to be unaware of any Gutenberg Editor related stuff so they won't be ported into you headless site.

The breaking change may happen if you commit certain React Components in the database and then change their markup. Since the registerFaustBlock helper will call the registerBlockType uses the save function, those components will be considered as static blocks: https://developer.wordpress.org/news/2023/02/static-vs-dynamic-blocks-whats-the-difference/

Gutenberg will warn you if the implementation part will change when you try to load the component from the database.

However if you have your React Components under a version control system you can use semver to keep track of their changes.

Another possibility is to provider a custom deprecated function so that you can deal with any changes.

IMO this would allow you to try-before-you-buy since the React Component will not depend on the Gutenberg side. You plug it in. You check if you are satisfied. You provide extra hints to refine this better and if you are happy you keep using it. If you are not you just simply take the React Component elsewhere.

@theodesp theodesp changed the title RFC: React Components to Gutenberg Blocks RFC: React Components to Gutenberg Blocks (WIP) Aug 3, 2023
@jmusal
Copy link

jmusal commented Aug 3, 2023

A less productive comment, but I can't express how excited I am for this. If you need any help, I'd be happy to try to help out however I can.

@justlevine
Copy link
Contributor

justlevine commented Aug 3, 2023

Thanks @theodesp 😊

Where will these React Components coming from / should this packaged be used

We assume that you have already made React components that you created and you want to try in Gutenberg.

...

The promise of this RFC once implemented is that you can use the same React Component in both the Headless site and the Gutenberg Editor side without having to maintain a duplicate implementation for the Gutenberg Editor. It will be somehow a hybrid approach.

To clarify by "coming from", I mean where are the code for the components supposed to live/get registered. While a POC is nice for working out implementation kinks, I'm just trying to understand what this library is intended to achieve, and how it will allow you to not 'maintain a duplicate implementation'.

If the goal is simply/strictly about extending WPs block registration API for non-WP users then that's one thing, but that's is a solution to a very different problem statement than what's outlined here.

Ultimately I'm trying to understand how this RFC is meant to address the frictions of using blocks in HeadlessWP (that you mentioned or otherwise )🙏

(Skipping the rest for brevity, the intention of my suggested alternative was to sketch out the ideal ecosystem pattern that the ecosystem is striving to - WordPress as a source-of-truth - and how that directly relates the problem statement in comparison to what's proposed here. A separate RFC and eventual POC would be the place to work out implementation details/ engineering hurdles just like what's happening now on this one)

@theodesp
Copy link
Member Author

theodesp commented Aug 3, 2023

To clarify by "coming from", I mean where are the code for the components supposed to live/get registered.

It's up to the developer to decide where to put the components. What's important for WordPress is to have the call to register_block_type that points to the folder that contains a valid block.json. The block.json will need to contain a reference to the editorScript that will be used to call the registerFaustBlock as mentioned in the example above. This will allow the block to be registered both in WordPress and in the Block Editor.

how this RFC is meant to address the frictions of using blocks in HeadlessWP

One big problem with Headless WordPress adoption is that a lot of agencies want to use Gutenberg blocks and share the same component in both places without having to maintain two implementations. A lot of those agencies already have some React Components that they would like to use them in Gutenberg. However they always have to start from scratch and waste tons of development time with porting it into Gutenberg.

With this RFC we provide a conventional and somewhat opinionated way to do that without having to change a lot while giving them an opportunity to plug their components directly to Gutenberg.

For example: Take a component like:

https://grubersjoe.github.io/react-activity-calendar

This component will render a github activity calendar when passed the list of dates and some other configuration.

With this proposal you will be able to provide editor controls to fill in the required properties that this component needs and it will render as it inside the editor when using the presentational view.

You don't have to provide an edit or save function since this would be automatically generated for you by the helper function. You only have to provide a simple block.json with the attributes you want to have and a component to render the activity calendar.

Some thing like:

export function MyActivityCalendar({attributes, className, children ...rest}) {
  const {data, theme} = attributes;
   return <ActivityCalendar className={className} data={data} theme={theme}  {...rest}>{children}</ActivityCalendar>
}
// Declare metadata here
MyActivityCalendar.config = {
  name: 'MyActivityCalendar',
  ...
};

This same component will be used in both the Headless site and the Editor site, thus reusing the component implementation part.

(NOTE: In some cases you have to be careful how you define the block.json to avoid issues with resolving the block attributes. However we would provide sample blocks and documentation that you can take inspiration from that follow a specific convention that works reliably).

@justlevine
Copy link
Contributor

justlevine commented Aug 3, 2023

@theodesp -

  1. In order to register a React component as a WordPress block, the component code needs to exist on the WordPress server.
  2. In order to use a React component in a NextJS app, the component code needs to exist in the application.

Which means either the duplicated code still needs to be maintained separately in both places, or the component needs to be imported as a dependency from a package (such as in your example, the unique feature here if I understood it is that the component wrapper is simplified).

If that's the case, I suggest updating the RFC (scoped along the the lines of "This library will allow Block Developers to easily import and register React components as Guternberg Blocks, without the unnecessary boilerplate".) There's no deduplication or impact on headless WP here, the benefit is avoiding parts of the block registration API.

Alternatively, this RFC is introducing a new mechanism to allow React component that only exists on the server/frontend to be imported/used somewhere else without moving it to an npm dep. That's what I've been trying to get clarification on, but I guess I'll wait for Joe's POC 👍

@theodesp theodesp changed the title RFC: React Components to Gutenberg Blocks (WIP) RFC: React Components to Gutenberg Blocks Aug 4, 2023
@theodesp
Copy link
Member Author

theodesp commented Aug 8, 2023

Pinging @josephfusco here to provide more context.

@josephfusco
Copy link
Member

josephfusco commented Aug 9, 2023

Are we duplicating them in our WP plugin/theme and our frontend app?
Are we releasing our components as libraries and importing them into both?
Are we defining them in just the theme/plugin or frontend (if so which) and using the Faust WP plugin to get them to the other?

@justlevine Hoping I can shed some light on this!

The goal would be for the code to live under version control within the NextJS app.
The Faust.js framework would handle discovering those blocks, and pushing them to WordPress as a subdirectory within the uploads directory (e.g. wp-content/uploads/faustwp), where the Faust.js WordPress Plugin would handle registering all blocks with WordPress.

I'm proposing the following CLI commands to help facilitate this:


faust generateThemeJson

This command generates the theme.json within the NextJS app if one exists within the active WordPress theme.

faust blockset

This command gathers, organizes, and prepares blocks to use, much like a traditional typesetter would with type. This would make a POST request to a REST API endpoint with the necessary contents to safely recreate those files on the WordPress filesystem.


Both of the new WP REST API endpoints would be registered within the Faust.js WordPress plugin to handle the CLI commands and secured via the FAUST_SECRET_KEY.

NPM scripts for a NextJS project could look something like:

  "scripts": {
    "predev": "npm run generate && npm run stylesheet && npm run themeJson && npm run blockset",
    "dev": "faust dev",
    "build": "faust build",
    "generate": "faust generatePossibleTypes",
    "stylesheet": "faust generateGlobalStylesheet",
    "themeJson": "faust generateThemeJson",
    "blockset": "faust blockset",
  },

With the introduction of these new commands, it would be great if Faust also provided an abstraction for commonly used commands to help improve the readability of npm scripts, although that's not critical to this proposal.

Although not complete, the POC for the above commands can be seen here.

@justlevine
Copy link
Contributor

justlevine commented Aug 9, 2023

@josephfusco that truly is ecosystem pushing! Mad props mate!

@theodesp Idk if its worth the effort to transcribe all this into the RFC while the proof-of-concept is still in flux. The tl;dr that I do think is worth adding to make the RFC clearer (and directly address the intro/motivation) is along the lines of:

Developers will be able create their React components locally in their Next.js app. The FaustJS framework would then upload them to your WordPress server and automatically register them as custom Blocks for use in the Block Editor.

@theodesp
Copy link
Member Author

@josephfusco that truly is ecosystem pushing! Mad props mate!

@theodesp Idk if its worth the effort to transcribe all this into the RFC while the proof-of-concept is still in flux. The tl;dr that I do think is worth adding to make the RFC clearer (and directly address the intro/motivation) is along the lines of:

Developers will be able create their React components locally in their Next.js app. The FaustJS framework would then upload them to your WordPress server and automatically register them as custom Blocks for use in the Block Editor.

I'll let @josephfusco handle that part since he is working on that feature.

@josephfusco
Copy link
Member

-> Working example demonstrating how block files would be moved to WordPress.
Note that this is only demonstrating the file transfer and does not include compiling steps or the actual registering of the block types.

@josephfusco
Copy link
Member

The Blockset file sync POC is now a fully working example with Next.js "blocks" showing up to the publisher within a few seconds of running faust blockset 🎉

Feel free to leave any relevant feedback you have directly in that PR.

@theodesp
Copy link
Member Author

theodesp commented Oct 19, 2023

The block support package contains an example.

@justlevine
Copy link
Contributor

control field comment
object TextAreaControl Renders a TextAreaControl field

Is this mapping of object data to a TextAreaControl intentional or a typo?

@theodesp
Copy link
Member Author

theodesp commented Jan 4, 2024

control
field
comment

object
TextAreaControl
Renders a TextAreaControl field

Is this mapping of object data to a TextAreaControl intentional or a typo?

Yeah we don't have a definite nice way to handle object types since they could be anything so we opted for having a field that stores value as a string. Maybe there is a way to make it better.

@theodesp
Copy link
Member Author

theodesp commented Jan 4, 2024

We're closing the current RFC as we have implemented the basic functionality.

If you have further suggestions for improvements, encounter related issues, or have code changes you'd like to propose, we encourage you to either open a new issue or submit a pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal RFC type: feature New functionality being added
Projects
None yet
Development

No branches or pull requests

4 participants