As with many things in development, every framework is great until you need it to do something custom. Storybook, unfortunately, is not immune to this, especially if you’re working in a non-React environment.
The documentation contains some simple examples for reorganising the default docs
page for each component’s stories file. Since we’re working in a Vue Storybook environment and we don’t have access out-of-the-box to JSX, the first example isn’t much use to us. Fortunately, there’s a second example that’s more pertinent.
Enter MDX
You can also use MDX to generate the documentation template. This is useful in non-React projects where JSX-handling is not configured.
Storybook Documentation
That’s exactly what we want! How do we implement that?
1import { Meta, Title, Primary, Controls, Stories } from '@storybook/blocks';2
3<Meta isTemplate />4 5<Title />6 7# Default implementation8 9<Primary />10 11## Inputs12 13The component accepts the following inputs (props):14 15<Controls />16 17---18 19## Additional variations20 21Listed below are additional variations of the component.22 23<Stories />
Unfortunately, that’s your lot. Great if all you need are a few formatting tweaks (and don’t mind losing some functionality from the standard Docs template), less great if you need to add new sections or do other things.
Our Challenge
In our attempts to wrangle the documentation template, we had two main goals:
- Maintain the exact same template as used by default.
- Generate a new block powered by information from the stories file that’s shown inside the template.
If you look at the JSX code that generates the default documentation template, you’ll notice a few things that our MDX “replacement” is missing:
export const DocsPage: FC = () => { const resolvedOf = useOf('meta', ['meta']); const { stories } = resolvedOf.csfFile; const isSingleStory = Object.keys(stories).length === 1;
return ( <> <Title /> <Subtitle /> <Description of="meta" /> {isSingleStory ? <Description of="story" /> : null} <Primary /> <Controls /> {isSingleStory ? null : <Stories />} </> );};
By using the MDX template, in the event of a single story in our stories file, we would lose the additional <Description>
block as well as rendering a moot <Stories>
block that’s made redundant by the <Primary>
block that’s shown earlier.
Fortunately, more recent versions of MDX are ideologically closer to JSX than standard Markdown and with a few quirks, we can write our own JSX blocks without needing to wire up JSX support for our project. Although bear in mind that you will be writing React regardless of your preferred framework… but hey, it’s all JavaScript.
The ‘export’ caveat
Very quickly in MDX, we hit a wall: you cannot declare variables in MDX. No constants, no lets, no functions, no components. All will result in an error:
There is, however, a loophole. MDX treats import
and export
statements differently and allows variables to be created in the document scope when using either of these keywords. Which means that this is valid code:
export const foo = "bar";
{ foo }
Rendering a block conditionally
Notice from our JSX basis file that we need to (1) work out if our stories file has multiple stories; and (2) render some blocks conditionally based on that fact.
Here’s what we could do:
export const checkIsSingleStory = () => { const resolvedOf = useOf('meta', ['meta']); const { stories } = resolvedOf.csfFile; return Object.keys(stories).length === 1;}
export const StoryDescription = () => { const isSingleStory = checkIsSingleStory();
return (isSingleStory ? <Description of="story" /> : null)}
export const DocumentationStories = () => { const isSingleStory = checkIsSingleStory();
return (isSingleStory ? null : <Stories />)}
<StoryDescription />
<DocumentationStories />
Since we can declare JSX functions inside MDX with export
, it allows us to reuse the code from Storybook’s default DocsPage to check if the stories file has multiple stories and react accordingly.
Note that the functions are returning JSX code inside ( ) parentheses.
Creating a brand new block
Okay, that’s the default template reconstructed, now onto our new block.
Our component library can be configured via a global JS configuration file for some components, and we wanted to allow components to provide example source code for setting this up.
If this code is detected, we want our docs page to generate a <Source>
block with a short introduction. Here’s how we might do that:
1import { Meta, Title, Primary, Controls, Stories, Subtitle, Description, useOf, DocsContext, Story, Source } from '@storybook/blocks';2
3export const EvoMarkConfig = () => {4 const resolvedOf = useOf('meta', ['meta']);5 const { evo } = resolvedOf.csfFile.meta.parameters?.docs ?? {};6
7 return evo?.config && (<>8 <h3>evoMark Global Config</h3>9 <p>You can set props globally for this component by using a <code>evomark.config.js</code> configuration file in your project root.</p>10 <p>An example of this is shown below:</p>11 <Source language="js" code={`export default = ${evo.config}`} format="dedent" />12 </>)13}14
15<EvoMarkConfig />
Now if we go to one of our stories files, we can utilise this new block via the config object:
1// some-component.stories.js2
3export default {4 title: "Some Component",5 parameters: {6 docs: {7 evo: {8 config: `{9 someComponent: {10 someProp: "some-value"11 }12 }`13 }14 }15 },16}
Note that evo.config
above is a text string inside template quotes.
Setting our documentation template as default
The final step is telling our Storybook to use our new documentation template. For this, we need to load up our .storybook/preview.js
file and tweak the exported default config object:
1import DocumentationTemplate from "./DocumentationTemplate.mdx";2
3export default {4 parameters: {5 docs: {6 page: DocumentationTemplate7 },8 }9}
Conclusion
And there we go, our very own documentation template with new blocks relying on stories data and the ability to reorder and re-format to our heart’s content.
For more information on what blocks are available by default inside your MDX template, see https://storybook.js.org/docs/writing-docs/doc-blocks#available-blocks.
And stay tuned for the next part in this series coming soon.