Controls In Storybook The More Undifficult Way
We all love Storybook. It’s the “No one ever got fired for buying IBM” of design system documentation1.
This post is going to assume a reasonable working knowledge of Storybook; storybook itself has excellent documentation on how to configure controls and there’s no sense in reproducing that here.
One of its great features is the ability to generate all the knobs and levers necessary to control your components, so that with minimal effort you can create a fully interactive demo of your component, complete with docstrings extracted from source.
However, there are some limitations to this:
- It’s only available for React and Vue (as far as I’m aware)
- Because it magically ‘just works™’, when it doesn’t, you’re left in the lurch with having to manually override controls in the story meta.
So, if you’re sitting outside of those two conditions, you’re stuffed.
Or are you?
An excellent posting was shared on the Storybook discord server recently:
In it, Jives describes their approach to generating and injecting controls for Web Components, via the intermediate form of a Custom Elements Manifest.
I’d recommend reading the post, but broadly it involves a transform function to turn a Custom Element Manifest entry into a Storybook control definition, and then injecting it manually into the meta.
This struck a chord with me, as I’d been taking a similar approach for my own Design System work, even though I was using React.
Why not use the built-in docgen?
To take a step back, I should first explain why I was using an external source for my docgen.
Firstly, I wasn’t satisfied with the information coming out of either react-docgen
react-docgen or react-docgen-typescript
react-docgen-typescript. This
is not to besmirch them as not being any good - I think they’re both wildly impressive bits of software, but they weren’t
producing all the information I needed.
Some of this was related to React (I wanted to know what underlying HTML element the component was forwarding to), and
some completely outside react (I wanted to document which CSS @property
values were defined and consumed by
this component.
Secondly, I had other potential consumers of this manifest of information - I needed to have a persistent manifest stored within the repo. Given this had all the information necessary for generating the controls, why generate it again within Storybook?
I now have my manifest all its juicy information, and I have my stories. Sure, I could take the approach Jives did, but coming from a place where the controls were automatically injected, I didn’t want to lose that convenience.
How to inject?
So began my journey of discovery as to how Storybook docgen magic works. I had been using react-docgen-typescript
. Spoilers - it ain’t pretty.
To use React + Vite as an example:
Step one is to inject the storybook:react-docgen-plugin
. This adds a vite transformer that runs react-docgen
over each source file, then weaves in a JSON
serialised representation of the generated docs:
if (actualName && definedInFile == id) {
const docNode = JSON.stringify(docgenInfo);
s.append(`;${actualName}.__docgenInfo=${docNode}`);
}
At runtime, this injected data is pulled through by:
- read in
getDocgenSection
- called by
extractComponentProps
- called by
extractProps
- called by
extractArgTypes
- assigned to
preview.docs.extractArgTypes
- read by
enhanceArgTypes
- which was configured in the preview’s
argTypesEnhancers
phew!
My first attempt at doing this was to write my own vite plugin that simulated the behaviour of the Storybook plugin, but read from my manifest file rather than generating on the fly.
This was a bit of a pain, as aside from the difficulty of finding the right object to inject the info to (e.g., injecting after wrapping in memo
),
the format for __docgenInfo
in React is react-docgen
, so I had to massage my manifest docs into this format only so
Storybook could turn them back into Storybook Control format.
The More Undifficult Way
When I came back to look at this again, I realised that rather than injecting at the very top of the call tree, I could
bypass all of Storybook’s shenanigans if only I investigated this argTypesEnhancers
nonsense. It’s a feature that seems
to lack any external documentation that I can find.
However, the type definition:
export type ArgTypesEnhancer<TRenderer extends Renderer = Renderer, TArgs = Args> = (
context: StoryContextForEnhancers<TRenderer, TArgs>
) => StrictArgTypes<TArgs>
and usage:
contextForEnhancers.argTypes = argTypesEnhancers.reduce(
(accumulatedArgTypes, enhancer) =>
enhancer({ ...contextForEnhancers, argTypes: accumulatedArgTypes }),
contextForEnhancers.argTypes
);
are pretty easy to grok, and so writing a custom ArgTypesEnhancer
becomes incredibly trivial:
import { combineParameters } from "@storybook/preview-api";
import type { ReactRenderer } from "@storybook/react";
import type { ArgTypesEnhancer } from "@storybook/types";
import manifest from './manifest';
export const argTypesEnhancer: ArgTypesEnhancer<ReactRenderer> = (context) => {
const docArgTypes = conformArgs(manifest[context.component.displayName].properties);
return combineParameters(docArgTypes, context.argTypes);
}
The only difficult part being the implementation of conformArgs
which needs to convert from however you’ve stored your
documentation to Storybook Controls. Once that’s done, just export this from your preview.ts
and you’ll be all done!
Bonus: args
As a bonus, a very similar mechanism can be used to inject default args, using the appropriately named argsEnhancer
.
So what?
Why write this blog post? Well, a. because someone asked me too, b. I think it’s a pretty nifty feature that I wanted to share and c. as a starting point for improving documentation in this area.
I think it’s a really powerful API that Storybook would benefit from documenting more. It’s not some weird internal magic
(like __docgenInfo
is), so I would hope there wouldn’t be a problem with publicising it further.
I think there are also further extensions to this style of API that could be added - Jives’ post includes the binding of action handlers, which could equally be automatically injected from generated documentation.
-
This is perhaps a little unfair on Storybook - it’s a great piece of software in its own right. It just absolutely dominates the landscape. ↩