<div>The Pitchburgh Diaries - decoupled Layout Builder Sprint 5 & 6</div>
Welcome to the third edition of the Pitchburgh Diaries, a regular update on progress as we work on our plan for a decoupled Layout Builder with React.
by
lee.rowlands
/ 7 November 2023
Sprints 5 and 6 were our final sprints in the project. Keep an eye out for a final wrap-up and summary of the next steps, which we’ll publish in the coming weeks.
Bundling and extending
In this sprint, we focused on the nuts and bolts of how contrib and custom code will extend decoupled Layout Builder functionality.
We began by creating a new Drupal 10-compatible version of the React module. Thanks to @corbacho for adding us as co-maintainers for the project!
When we bundle the decoupled Layout Builder code for use in the browser, we don't include React. Instead, we rely on the React module to load it. This allows other modules that need React (e.g. Gutenberg) to use a shared version of React. React doesn't work if two versions are loaded on the same page.
The new version of the React module makes use of a relatively new browser API called import maps. Import maps allow you to write ECMAScript modules (ESM) with naked imports and have the browser resolve the dependency for you.
So, for example, our bundled code still has import React from 'react' in it. With an import map, the browser can resolve that to a Javascript file and load it for you.
To support this functionality, we wrote and released an import maps module which both the Decoupled Layout Builder API and React module make use of. We believe this functionality belongs in core because you can only have one import map on a page. So we opened a core feature request for that too.
With this module in place, bundling for contrib and custom code that wants to add additional components to the decoupled Layout Builder becomes much simpler. Essentially the build configuration needs to mark imports of React, React DOM, the JSX runtime and the decoupled layout builder components as 'external'. This ensures the bundled code retains the original import statements. Both Vite and Webpack support this feature.
For those who recall how we built ES6 code in Drupal 9, you would know we had scripts in core's package.json and webpack configuration to transpile it into code that worked in older browsers like Internet Explorer. With Drupal 10, we removed that step as all of our supported browsers have native support for ES6 code. Similarly, if you've built a CKEditor 5 plugin, you would know it also uses Webpack for bundling.
As a result, Webpack felt like the natural choice for bundling here too. WordPress uses it to bundle block components for their Gutenberg editor. However, the web landscape moves quickly. The tool we chose N years ago may no longer be the best choice. With all modern browsers supporting ESM, the bundling landscape has changed.
Those who follow front-end web development would know that many projects are actively moving away from Webpack towards Vite. Storybook added support for Vite in v7, and just last week, Remix had a major announcement about Vite support. CKEditor5 has also added Vite support. For this reason, we evaluated both Vite and Webpack for use in our utility Drupal scripts package. Thispackage is designed to make writing and bundling code for use with the decoupled Layout Builder simpler. Based on our evaluation and the broader front-end landscape moving towards Vite, we chose it for our bundling.
As a result, we have an npm package @drupal/scripts that we will release in the coming weeks with the following features:
- A simple build step. No need to manage your own bundling configuration - just add a dependency on @drupal/scripts (more on that below) and add this to your package.json for building - drupal-scripts build -o js/dist js/src
- A simple setup process - if you install the package globally, you can run drupal-scripts init to have it automatically update your package.json with the required changes
- Support for scaffolding components for use with decoupled Layout Builder - just run drupal-scripts generate and follow the steps.
Screencast of code generation with the drupal-scripts package. Click to watch (8Mb)Feature development
In our first four sprints, we focused on building the Layout Editor in a decoupled application. We were mocking APIs so development could occur without a Drupal site.
In these two sprints, we switched to instantiating the Layout Editor in an actual Drupal site.
The Layout Editor uses React components that mirror Blocks, Formatters, Widgets and Layout plugins from Drupal. We have always intended for these to be the extension points for the application. If you need to change how any of those work in a project, you should be able to swap in your own custom React component.
To facilitate this, the entry point for the decoupled Layout Builder is the Layout Editor component. It takes a map of components for each of the Blocks, Formatters, Widgets and Layout plugins. This map is keyed by the plugin ID (same IDs as in Drupal). The values of the map are a function that return a promise, that will resolve the components. What each component comprises depends on the type.
For example, a Block component needs an Edit and Preview component but might also need a Settings component. You can read more about what each component comprises in the storybook documentation.
In order to boot the Layout Editor, Drupal needs to construct these maps. To do this, we make use of existing plugin definitions and extend them to add an entry for the decoupled Layout Builder.
Here's an example of nominating the path to a React component for a layout plugin:
/** * Implements hook_layout_alter(). */function mymodule_layout_alter(&$definitions) { $path = '/' . rtrim(\Drupal::service('extension.list.module')->getPath('mymodule''), '/') . '/js/dist/'; /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */ if (isset($definitions['my_layout''])) { $definitions['mylayout'']->set('decoupled_lb', [ 'library' => 'mymodule/mylayout', 'componentPath' => $path . 'MyLayout.js', ]); }}
In this example, the file MyLayout.js would be scaffolded with the drush-scripts generate command and updated according to the documentation for a Layout component.
In the Decoupled Layout Builder API module, we replace the default LayoutBuilder render element with a decoupled one. When this component is rendered, it loops over all of the block, formatter, widget and layout plugin definitions and builds up a mapping from ID to component path. This is then stored in drupalSettings. The element also attaches some Javascript to boot the React application that reads this value back and turns the file paths into promises using the browser's native import operator.
With all this in place, we were able to boot the new Layout Builder in Drupal 🎉. Here's a screenshot of that for the Recipe content-type in the Umami demo profile:
Other key highlights
So, while we've focussed mainly on the big ticket items, we were also able to complete a fair few of our other wish list items in these final sprints, including:
- Fallback rendering when no React component exists - as seen above, we're able to fallback to Drupal rendering where no React component exists yet
- Support for layout builder restrictions so that you can only drag and drop components into supported regions
- Support for saving changes in Drupal - including autosaving
- General improvements to Drag and drop so that it was easier to drag new components into existing regions
- General normalisation improvements so that section and region UUIDs are generated Drupal side and stored in third-party settings.
- GitLab CI integration so that Storybook builds on every push.
- Additional documentation and tests.
- Layout settings pane
Where to next
We've reached the end of our sprints for Pitchburgh. But that doesn't mean the work stops. We plan to continue working on the project and have quite a backlog of new features we'd like to add.
In our next post, we'll recap each of the completed stories for our Pitchburgh grant statement of work, go into more detail about our future plans and let you know where you can help.
Tagged