Integrating Storybook with Drupal

Albert Skibinski
11 min readNov 1, 2019

--

Note: this article was written when Drupal 8 was the active Drupal version. Drupal 10.1 now has Single Directory Components in core (beta) offering a different implementation using the same concepts . See also the contrib sdc project and modules that extend this functionality for Storybook integration options.

Storybook is a tool for organizing UI components and an alternative to PatternLab, another design system which is more opiniated towards atomic design.

Both systems can be used to independently design and create components and integrate them into Drupal but Storybook has some interesting features.

Storybook is much more oriented towards modern webapps built in Javascript frameworks like React, Vue or Angular. In fact, Storybook itself is a tool built in JS and because of this has allows for great reusability (more on this later).

Also, Storybook is most powerful when used in larger projects, with potentially multiple teams working on different apps but within the same brand. They’re honest about this on their design system introduction:

If you work with a modest team on a single app, you’re better off with a directory of UI components instead of setting up the infrastructure to enable a design system.

So, Storybook really shines in large projects with decoupled apps. If you already have a decoupled Drupal with one of the popular JS frameworks, you can simple start by using one of the guides for your preferred framework.

Note: There is an active issue on the roadmap to support multiple frameworks in one Storybook.

What about Twig?

If you’re using Drupal with its default frontend template layer Twig, you need to roll your own Storybook integration. For this post, our team at Intracto (special thanks to Dennis Cohn) investigated a couple of approaches to this challenge. The goal being a guide to the most simple integrations of Storybook with your existing Drupal Twig theme (non decoupled Drupal).

You can follow along with this post by using this Github repo or by using a theme of your own. The only requirement is the block.html.twig file is present in your theme and compatible with the one in Drupal core’s Classy theme.

We start by using Storybook for HTML, because this is the most simple version without any JS framework dependencies.

In your Drupal theme, add Storybook by installing it with npm. Here we assume you already have a package.json because you probably already use gulp/webpack or other tools in your frontend build pipeline. If not, just npm init first.

npm init
npm install @storybook/html --save-dev

And don’t forget to add it to your npm scripts in package.json:

"scripts": {
"storybook": "start-storybook"
}

You might already have Babel as a dependency, otherwise add them too:

npm install babel-loader @babel/core --save-dev

Now we will need some additional libraries to handle Twig templates. Why? Well, Twig templates are server-side templates which are normally rendered with TwigPHP to HTML by Drupal, but Storybook is a JS tool. In order to render our Twig templates in Storybook, we need to make Storybook understand Twig.

We need the JS equivalent of TwigPHP and that is exactly what Twig.js is.

npm install twig --save-dev

This the core of Twig functionality but we also need three more libraries to help us:

npm install twig-loader twig-drupal-filters drupal-attribute --save-dev

Let’s go through them so we now what we are doing:

Twig-loader is a webpack loader; it automatically requires twig.js, compiles the Twig template and returns a function. Without it we can’t actually load the Twig templates later on.

Twig-drupal-filters is the JS implementations of the Twig functions and filters which are added and used by Drupal. Without it you wouldn’t be able to use templates with filters like clean_class or safe_join.

And drupal-attribute adds the helper methods like addClass(), removeClass(), setAttribute() and so one, which are provided by Drupal.

Note: if your Twig templates contain custom Twig functions which you will need to convert these to JS if you want these templates to function in Storybook.

Configuration

Next stop is configuration. In your Drupal theme create a .storybook folder and in it two files: config.js and webpack.config.js.

config.js:

import { configure } from '@storybook/html';
import Twig from 'twig';
import twigDrupal from 'twig-drupal-filters';
// Add the filters to Twig instance.
twigDrupal(Twig);

configure(require.context('../components', true, /\.stories\.js$/), module);

This is the main Storybook configuration and here we assume our components will be created (in just a minute) in a components folder in your theme. But this can be anywhere. We also import the Twig dependencies we added earlier.

webpack.config.js:

const path = require('path');

module.exports = ({ config }) => {

// Twig:
config.module.rules.push({
test: /\.twig$/,
use: [
{
loader: 'twig-loader',
},
],
});
return config;
};

This is the webpack configuration necessary for twig-loader. Note that you can add twigOptions using the options parameter. See documentation.

Create a component

Create a components folder in your Drupal theme folder and in it a block folder with the following files: block.stories.js, block.twig, and block.css.

block.stories.js:

export default { title: 'Blocks' };

import block from './block.twig';
import drupalAttribute from 'drupal-attribute'
import './block.css';
import './block.js';
export const default_block = () => (
block({
attributes: new drupalAttribute(),
title_attributes: new drupalAttribute(),
plugin_id: "Some plugin",
title_prefix: "",
title_suffix: "",
label: "I'm a block!",
content: "Lorem ipsum dolor sit amet.",
configuration: {
provider: "Some module"
}
})
);

This component story syntax is pretty new in Storybook and uses ES6 arrow function syntax. We provide the variables which are needed in the Twig template, which is also the reason why use drupal-attribute here: the Twig templates uses attributes.addClass as we will see next.

block.twig:

{%
set classes = [
'block',
'block-' ~ configuration.provider|clean_class,
'block-' ~ plugin_id|clean_class,
]
%}
<div{{ attributes.addClass(classes) }}>
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% block content %}
{{ content }}
{% endblock %}
</div>

This is exactly the same twig template you will find in Drupal core’s Classy template: block.html.twig.

block.css:

.block {
border: 3px solid #FF0000;
padding: 20px;
}

h2 {
margin-top: 0;
}

The CSS file contains just something to set it apart for demoing purposes.

block.js:

console.log('Javascript works!');

Normally, the block component in Drupal doesn’t use any special JS, but for our example we will add something for demonstration purposes.

Your theme folder should look something like this:

Allright, let’s start Storybook up:

npm run storybook

You should see a browser window with something like this:

Using the Twig template in Drupal

At this point your basic Storybook setup is ready. Now, we would like to acually use this Twig template in Drupal. The easiest way to do this is by including the Twig file from another Twig file already provided by Drupal.

We can do this by leveraging Twig namespaces. Drupal core already does this (see FilesystemLoader) for all your normal modules and themes but does not provide a way to register custom namespaces.

This is why we need the components module to setup namespaces so we can easily include our Twig templates.

composer require drupal/components

Enable the module and setup the path in your theme’s info.yml file:

component-libraries:
storybook:
paths:
- components

You can easily create an atomic design structure here (or any structure you desire), with atoms, molecules, organisms and so on. But for the sake of simplicity in this article we only define one @storybook namespace which maps to our components.

block.html.twig (in Drupal templates):

{% include "@storybook/block/block.twig" with {
"attributes": attributes,
"title_attributes": title_attributes,
"plugin_id": plugin_id,
"title_prefix": title_prefix,
"title_suffix": title_suffix,
"label": label,
"content": content,
"configuration": configuration,
} %}

Here we include our Storybook component with the variables needed.

What about Javascript and CSS?

At this point we don’t have the javascript or CSS (fromt the storybook component) included in the Drupal twig file. So let’s set it up.

For JS, the recommended way to attach javascript behaviours is by attaching it with the Drupal.behaviour object. However, Storybook knows nothing about Drupal.behaviours (nor should it). Ideally you just want to write nice system-independent ES6 code for your components.

Fortunately, there is a Babel plugin which adds this a wrapper to generated JS files.

npm install --save-dev @babel/cli babel-plugin-drupal-behaviors

Note: we also need babel-cli for running the next command. Create a new build script for this in your package.json:

"scripts": {
"storybook-js": "npx babel components -w -d dist/components --ignore 'components/**/*.stories.js' --presets=@babel/preset-env,babel-preset-minify --plugins=babel-plugin-drupal-behaviors"
}

Running npm run storybook-js will now generate the same folder structure of our storybook components but with the JS wrapped in a Drupal behaviour in a folder dist/components in your Drupal theme. Nice! This we can use in Drupal.

But first let’s also generate processed CSS in the same destination structure using Webpack.

npm install --save-dev webpack webpack-cli mini-css-extract-plugin webpack-fix-style-only-entries

Create a webpack.config.js in a new webpack folder:

const path = require('path');
const glob = require('glob');
const loaders = require('./loaders');
const plugins = require('./plugins');

module.exports = {
entry: glob.sync('./components/**/*.css').reduce((entries, path) => {
const entry = path.replace('/index.js', '');
entries[entry] = path;
return entries;
}, {}),
output: {
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
loaders.CSSLoader,
]
},
plugins: [
plugins.MiniCssExtractPlugin,
plugins.FixStyleOnlyEntriesPlugin,
],
};

Note: checkout the repository for the loaders.js and plugins.js configuration included here for readability!

Let’s go through this config. First of all it creates dynamic entry and output paths so we keep the same folder structure which is used in the storybook components folder. It creates an entry for each CSS file in our storybook components folder.

Then we are using the CSS loader because we want to load .css files. Two plugins are used. MiniCssExtractPlugin because Webpack by default generates JS but we want CSS as output. And the FixStyleOnlyEntriesPlugin because otherwise Webpack will always also generate a JS file for each entry CSS file and we want our destination dir to be clean.

Add a script for this config:

"scripts": {
"storybook-css": "webpack -d --config ./webpack/webpack.config.js --display-error-details",
},

And run it via npm run storybook-css. You should now see a block.css next to the generated block.js which was generated by storybook-js.

Note: At this point you might ask why bother with Webpack because effectively we didn’t do much (yet) with the CSS except copying it. That’s true but you can now easily expand this webpack config to include any post processing, minifying, linting and so on. Or you could use SASS in Storybook (see below) and use sass-loader to create CSS.

Drupal asset libraries

The recommended way to add CSS and JS to Drupal is using asset libraries. This can be done through the theme’s *.libraries.yml. For example, we could add something like this:

block:
version: VERSION
css:
component:
dist/components/block/block.css: { weight: -10 }

But defining the library assets in the yml file like above can be cumbersome and easy to forget with hundreds of components. So, we can use hook_library_info_build to dynamically define these libraries by traversing the components for CSS (and JS).

drupal_storybook_theme.theme:

/**
* Implements hook_library_info_build().
*/
function drupal_storybook_theme_library_info_build() {
$extensions = ['css', 'js'];
$directory = 'themes/drupal_storybook_theme/components';
$extensions = array_map('preg_quote', $extensions);
$extensions = implode('|', $extensions);
$file_scan = file_scan_directory($directory, "/{$extensions}$/");
$libraries = [];
foreach ($file_scan as $file) {
$parts = explode('.', $file->filename);
$extension = end($parts);
switch ($extension) {
case 'css':
$libraries[$file->name][$extension] = [
'component' => [
'/' . $file->uri => [],
],
];
break;
case 'js':
$libraries[$file->name][$extension] = [
'/' . $file->uri => [],
];
break;
}
}
return $libraries;
}

This will generate library assets with JS and CSS in one asset (as long as the filenames are unique per component and identical for CSS and JS).

One last thing: actually use this asset in the Drupal twig template we used earlier:

block.html.twig:

{{ attach_library('drupal_storybook_theme/block') }}
{% include "@storybook/block/block.twig" with {...}

Okay, that’s it! Clear the cache and reload the page where your Twig template is used (anything with a block really) and you should see the styling used in Storybook. Try changing something in the Storybook template to see the difference reflected in Drupal, after running the build commands (you can create watchers for this).

What about SASS?

It’s possible to directly use SASS files in storybook by using the sass-loader and editing the webpack.config.js in .storybook. See also storybook’s documentation on this topic.

// Sass support.
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../'),
});

Now you will be able to import .scss files directly from your stories.js.

Reuse components

A powerful feature of Storybook is reusability accross projects and teams. This is wel explained on their website but basically, you have to create NPM packages of your design components which can be used by other projects. Then, you only have to add those components as a dependency in your project and they will show up together with your projects’ components in one Storybook.

So, imagine you have a central repository with design components, then all other projects only have to update the dependency if they want to use newly added components.

Addons and extending Storybook

Storybook also has a lot of great addons which are easy to integrate. Some of the most useful ones which we started to use immediately are Notes, Viewport and A11y.

But you can also implement quality assurance tools like automated visual testing. There is quite a lot of information online about possibilities like this so check them out.

Conclusion

While it is nice to have Drupal and Storybook use the same Twig templates, CSS and JS, there are caveats and room for improvement.

First, the ‘additional Twig layer’ (by including templates in other twig templates) seem less then ideal, however it is the easiest and most practical implementation if you don’t want to change existing default Drupal behaviour. There are core ideas to make Drupal more ‘component driven’ but we will probably see more movement on this topic in contrib with initiatives like single file components or compony.

Second, with this approach you still need to make sure all the variables which are designed in Storybook are provided by Drupal. It’s one thing to convert existing templates but it’s another when your frontender wants to use data which is not easily provided by Drupal. AmazeeLabs wrote an interesting article explaining how they bypassed this problem by using GraphQL in Twig with Storybook to pull data rather then let Drupal push it.

Third, while we didn’t encounter this yet, we heard there are small differences between the Twig.js and PHP implementation. And there is the issue we mentioned earlier with custom Twig filters or functions you might have in Drupal which would have to be rebuilt in JS for Storybook.

Also worth noting is that Emulsify will switch from Patternlab to Storybook in their upcoming release and if you want to join the discussion checkout the #storybook channel at drupaltwig.slack.com. Alsoalso, Drupalcon Amsterdam 2019 has some videos about Storybook + Drupal.

Happy storybooking and any feedback is welcome!

--

--