The Pitchburgh diaries - decoupled Layout Builder wrap-up
Since Drupalcon Pittsburgh, we've been working on a decoupled Layout Builder for Drupal.
We've reached the end of the statement of work, so let's recap the requirements and discuss what’s next.
/ 7 November 2023Requirement 1: Create an npm package for developing decoupled components
Taking inspiration from @wordpress/scripts, develop an npm package that has tooling for building and packaging decoupled components.The MVP of this will include: • a script for building JS and CSSOut of scope but for future consideration:• linting• a script for checking license compatibilityRelease this as a package on npm in the @drupal namespace.
Not only does the developed package support building CSS and JS via drupal-scripts build, but we also added support for initialising a new project and code generation with drupal-scripts init and drupal-scripts generate.
Requirement 2: Determine how to deal with externals in Webpack
Wordpress has @wordpress/dependency-extraction-webpack-plugin, a webpack plugin that extracts dependencies that are provided by Wordpress.We don't have enough budget to go down the full 'roll our own' approach and it may be overkill for now. Instead, derive configuration for Webpack externals that ensures components don't bundle dependencies Drupal will guarantee to be present.This config will likely be part of the package at 1)Out of scope but for future consideration:• Create our own plugin like Wordpress with some Drupal specific replacements
Requirement 3: Provide a registry of React Layout Components
Create a simple layout registry and registration process for layout plugins. Create an MVP of some common layout plugins:• One column• Two column• Grid layoutOut of scope but for future consideration:• Attempt to use the AST-generated Twig APIs to autogenerate React components that provide the markup for a layout plugin.• Write a symfony/console command that can take the ID of a layout plugin and generate an equivalent React component.• Bundle this as a part of a new experimental module.
Requirement 4: Persistence layer
Create an OpenAPI spec for the persistence layer. The MVP will include the following endpoints:• Choose section• Add section• Remove section• Choose block• Add block• Update block• Remove blockThese will mirror existing HTML endpoints from layout builder\Create JSON equivalent versions of these routes. Some of these routes will exist to serve information for the decoupled layout builder (e.g. choose block/section) whilst others will exist to perform mutations of the configured layout on the Drupal side.Out of scope but for future consideration:• Configure section• Move block
We wrote an API specification for the needed endpoints. We didn't need add section, remove section, add block, update block and remove block, as all these are handled client side. We did need additional endpoints for saving, discarding and reverting.
All of these endpoints are implemented, with test coverage in the Decoupled Layout Builder API module
Requirement 5: Create a block discovery mechanism to associate a block plugin with a React component
Create a registry component that can translate block plugin IDs to these components. Allow modules to register a component as a React version of a Drupal Block plugin so the decoupled layout builder knows how to render it.This will take inspiration from registerBlockType in @wordpress/blockCreate a React version of the InlineBlock block plugin as a proof of conceptOut of scope but for future consideration:• Convert other Block plugins to React
We wrote a context provider to provide a block registry and a way to pass configuration of this from Drupal to React. We wrote a React implementation of the inline block, which uses formatter and widget components. This component is quite complex as it needs to load entity view and form display configuration from Drupal. We have a working version of the Field block plugin too.
Requirement 6: Create a Drupal-rendered block fallback React component
Issue: https://www.drupal.org/project/decoupled_lb/issues/3375416 (could have)
Create a fallback React block plugin component that can make a request to Drupal for a rendered previews. Extend the persistence layer to add another Route for this.Out of scope but for future consideration:• Support a legacy configuration form for these blocks as well
We ship rendered 'fallback' versions of each block in the API. This saves the individual plugins from needing to make many single requests and ensures the best performance as loading happens once upfront.
Requirement 7: Create a discovery mechanism to associate a widget plugin with a React component
Identify a list of initial widget plugins that make sense to convert to React as an initial proof of concept. Create a React version of these widget plugins making use of @wordpress/components.Create a registry component that can translate widget plugin IDs to these components. Allow modules to register a component as a React version of a Drupal widget plugin so the decoupled layout builder knows how to use it to edit an inline block.Out of scope but for future consideration:• A React powered media library widget
We wrote a context provider to create a widget registry and a way to pass the configuration of this from Drupal to React. We wrote a React implementation of the text area widget, which uses an inline CKEditor for in-place editing with rich formatting. We didn't use WordPress components - more on this below.
Requirement 8: Create a discovery mechanism to associate a formatter plugin with a React component
Identify a list of initial formatter plugins that make sense to convert to React as an initial proof of concept. Create a React version of these formatter plugins.Create a registry component that can translate formatter plugin IDs to these components. Allow modules to register a component as a React version of a Drupal formatter plugin so the decoupled layout builder knows how to render a preview of an inline block.Out of scope but for future consideration:• Devise a way for themes to modify the markup of these components, possibly by registering their own replacements in the registry.
We wrote a context provider to create a formatter registry and a way to pass configuration of this from Drupal to React. We wrote a React implementation of the text default and string default plugins.
Requirement 9: React context provider/hooks
To allow blocks/layouts/widgets/formatters to access this data without passing down props from each component, create a series of context providers and hooks to allow components to access and update this data.We can take inspiration from e.g. useBlockProps in @wordpress/block-editor
We didn't end up using hooks and context providers for this. Instead, we used a Redux toolkit - a mature state management solution designed for large complex applications. There are various selector functions made available so components can select state as well as reducers for updating state. The use of Redux means we can get things like undo and redo without much effort (more on that later).
Requirement 10: Layout builder component
Add a component which manages the following UX behaviours:• Adding a section (layout plugin)• Adding an inline block• Editing an inline blockIt is hoped that we can take inspiration from existing open-source components in this space including @wordpress/components and builder.io.Out of scope but for future consideration:• Editing (Configuring) a section• Moving a block
Here we completed the following:
- Adding a section
- Adding a block
- Editing a block
- Moving a block
- Moving a section
- Configuring a section
- Configuring a block
- Viewing an outline of the layout, including support for moving components/sections
- Saving a layout to Drupal, including autosave
Where to next?
The end of the Pitchburgh funding doesn't mean the end of the road. As we progressed through the project, we were building a backlog of out-of-scope items we discovered. A key aspect of this project was the evaluation of the feasibility of a decoupled layout builder. This work will also feed into the newly announced Page Building Experience initiative as one possible solution.
We plan to keep working through these as part of our ongoing commitment to open-source. Do you have an interest in helping us? If so, reach out to me in the #layouts channel on Drupal Slack. There's a real mix of technology at use here - not just PHP but React, Typescript and CSS.
We definitely need some help making the UI look nicer and more closely aligned with the Claro look and feel, as well as the work being done by the Admin UI initiative. If you've been looking for a way to get involved in Drupal contribution without a heavy understanding of Drupal's internals - this might be the ideal place to get started.