The New Storybook Module for Drupal

Exploring the functionality of the new Storybook.js module, enhancements over prior versions, and essential setup guidelines.

Component-based web development has become the de facto approach for new Drupal projects. Componentizing your UI has multiple advantages, like modular development, improved automated testing, easier maintenance, code reusability, better collaboration, and more. Because of these benefits, we led the effort to create a component solution for Drupal core: Single Directory Components (SDC).

As part of that suite of modules, we created CL Server. Initially developed for the CL Components module (a contrib precursor of SDC in core), it eventually became the basis for SDC in core. 

In coordination with a Storybook plugin, this module allowed anyone to create a catalog of components using Storybook, an open-source project for developing UI components in isolation. It allows developers to build, test, and document their components without constant integration with the rest of the codebase. Storybook promotes consistency across components by providing a centralized location for designers, developers, and QA engineers to share their work, with documentation capabilities to help ensure that components are easily understood and maintained.

Our Drupal integration worked great for a while. But we started to feel its limitations.

Limitations with the current approach to Storybook

Storybook has the concept of decorators, which are divs that surround the components inside of Storybook. These are useful when your component needs some contextual markup. 

For example, a grid element. This component may not look correct when rendered in isolation. It needs more context. In general, the CL Server approach was coupling the HTML in the Storybook story to the HTML that a component generates. We needed to have more control over the markup rendered in Storybook.

Another issue was that CL Server worked with the YAML/JSON version of stories, as documented in the official docs. YAML and JSON are not programming languages and lack dynamic features that we missed, like setting variables, translating strings, looping over complex structures, and providing non-scalar arguments to Storybook (like objects of type Drupal\Core\Template\Attribute). You cannot instantiate a PHP object in JSON since JSON knows nothing about PHP.

Finally, we experienced issues upgrading the necessary addon @lullabot/storybook-drupal-addon after major releases of Storybook. We kept running into backward compatibility issues, tangled dependencies, and the need to support two major versions of Storybook simultaneously during this transition.

In search of an alternative

The initial implementation of CL Server started as an experiment that people liked and used. We started it before Storybook committed to their server-side framework. Still, it worked, thanks to the addon @lullabot/storybook-drupal-addon and some conventions in JSON/YAML stories format for SDC components.

During our research for an alternative, we realized that the Storybook team did not mean for us to write JSON stories. They only used JSON because all languages know how to read and write with it. We needed to generate the JSON from our stories, which should be written in our native templating language.

In Drupal's case, that’s Twig.

If a server-side application writes the stories in their template language, then stories can import components and generate contextual markup with front-end developers' tools. Using Twig to write stories would solve all of our current limitations.

Moreover, by generating the JSON, we can have Drupal add some finicky metadata that will make the Storybook addon unnecessary. All the problems of upgrading Storybook to newer versions would be solved. By adding this extra metadata, we would only need the native integration for server-rendered components maintained by the Storybook team.

But how do we make Storybook, a JavaScript application, understand our Twig-PHP components while avoiding Twig.js?

The proposal

The goal is to create a way to write several stories in a single Twig file, in line with the official Storybook documentation on writing stories:

 

A story is a function that describes how to render a component. You can have multiple stories per component, and the simplest way to create stories is to render a component with different arguments multiple times.

 

For this to happen, we created two new Twig tags, {% stories %} and {% story %}

{% stories %} is a container for each individual story, but it also allows us to add metadata to the file as a whole. This metadata will be included in the transpiled JSON version. 

{% story %} also contains metadata for Storybook, but it is the actual Twig template to render for that particular story. In other words, it contains the twig code we want to render in the Storybook canvas when the story is selected. Pretty intuitive.

Consider this extremely simplified example:

{% stories my_id with { title: 'Components/Examples/Card' } %}

  {% story first with { name: '1. Default', args: { foo: 'bar' } } %}

    {# Here, use any Twig you want, including custom functions, filters, etc. #}

  ...

  {% endstory %}

{% endstories %}

Two main things are going on. First, the data object after with in both tags will be sent to Storybook as the story metadata. See the Storybook docs for a complete list of the available options.

The second thing is that the code inside the {% story %} tag will be turned into HTML by Drupal and sent to the Storybook application to display. You can paste static HTML, embed a component using {% embed %}, loop over a data array, and more. 

Note that the variables available in this Twig code are the ones added via args or params. Watch this 5-minute video for more details on how to write stories that embed nested single-directory components.

After writing your template, transpile it from Twig to JSON. To do so, you can use the drush commands provided by the storybook module. You can compile a single template by passing the path to the template:

drush storybook:generate-stories path/to/your/file.stories.twig

Or, you can have drush find all the template files in your system and then transpile them one by one. The command for this is:

drush storybook:generate-all-stories

This command will also check whether the JSON stories exist and whether the file is newer than the Twig stories. If so, it will skip the transpilation process for that particular file, thus saving resources in repeated runs. This is useful when executing the drush command continuously in the background every few seconds. You can "forget" about the transpilation process. You can use watch in Linux and macOS (you may need to install it with homebrew in macOS).

watch --color drush storybook:generate-all-stories

Running Storybook

Once our stories are generated in JSON, it's time to do our initial one-time setup. This will configure Storybook to search for the stories in the correct folders in your filesystem and configure CORS for Drupal.

Check out the module's README for detailed documentation on how to do this. Alternatively, this video demonstrates a full setup in less than 5 minutes.

After setup, you will find your stories in the sidebar when you start Storybook. They will all be grouped by a folder with the name specified in the `title` metadata property for the {% stories %} tag, with the name used in name on the {% story %} tag.

Storybook.js page pulled from Drupal Twig files

Also, note how the Controls tab lets you update the variables passed into Twig in real time. These correspond to the `args` provided in the `{% story %}`.

From here, you can start using your Storybook for your purposes. Take a minute to explore the available add-ons, which add functionality like a language selector, automated accessibility audits, a breakpoint selector, and more.

For more information on this module, view the full demonstration video.

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!