import React from 'react';
import Component from 'somewhere';
export default Component;
export const variant1 = () => <Component variant="1" />;
export const variant2 = () => <Component variant="2" />;
export const variant3 = () => <Component variant="3" />;
config.examplesGlob = `**/*.examples.js`; // <- this is the default
Note this isn't a real thing yet, but we'll figure some way to avoid using require.context
soon.
The above file will be added to your storybook as Component:variant1
, Component:variant2
and Component:variant3
. (You can have more complex names, see below).
A key benefit of a non-storybook specific file format is it becomes simple to reuse your examples in other tools, such as unit tests. For example:
import React from 'react';
import { mount } from 'enzyme';
import renderer from 'react-test-renderer';
import { variant1 } from './component.examples';
describe('variant1', () => {
it('example renders correctly', () => {
// Now you can do low-level unit tests on the example
expect(mount(variant1()).find('div').className).toMatch(/component/);
// Or you can do snapshot testing (although storyshots will still make this better)
const component = renderer.create(variant1());
let tree = component.toJSON();
expect(tree).toMatchSnapshot()
});
});
To add metadata at the example level, add extra properties to the exported function. To do it to the component, export an object rather than the component class.
import React from 'react';
import Component from 'somewhere';
// Parameters or decorators for the component (i.e. all stories):
export default {
component: Component,
parameters: { viewports: [320, 1200] },
decorators: [...],
});
// Parameters for a single story
export const variant1 = () => <Component variant="1" />;
variant1.parameters = { viewports: [320, 1200] };
We'll also make a super simple library to ensure you don't have to think about it:
import React from 'react';
import Component from 'somewhere';
import example from '@storybook/example';
export default example(Component, {
parameters: { viewports: [320, 1200] },
})
export const variant1 = example(() => <Component variant="1" />, {
parameters: { viewports: [320, 1200] },
});
- Why not return an object for examples?
We want examples to be consumed as simply as possible. Consider that developers will often be using them directly in tests etc. It is best if it can always be assume an example is a function returning something renderable.
OTOH, the component export (the default
export) is more typically used by tooling (e.g. props tables for documentation) and so can be more flexible. Assigning properties on a component class (or equiv in other frameworks) seems dangerous and ugly.
- Why mutate the function object?
You don't have to. This works too:
export variant1 = Object.assign(() => <Component />, { parameters: { viewports: ... } });
Our feeling is the mutation syntax is simpler and easier to understand.
By default, the story "chapter" (or "kind") will take the title from the component's displayName
(or equiv.), and the story's title will be the name of the example's export.
So the examples above will be called Component:variant1
, Component:variant2
and Component:variant3
.
We'll add a config flag that automatically prefixes the component's title with the pathname on disk.
Suppose you have:
config.prefixComponentTitlesByPathFrom = 'src/'
Then src/components/Button.examples.js
will name its examples like components/Button:variant1
, etc.
Alternatively, we'll support title
and titlePrefix
props on the component, and title
on the example:
export default {
component: Component,
titlePrefix: 'components',
// This would be equivalent to
// title: 'components/Component',
});
// Parameters for a single story
export const variant1 = () => <Component variant="1" />;
variant1.title = 'Initial variant, I like to use wordy story names';
Addons that are more dynamic like actions
and knobs
have an opportunity to be refactored to better reflect the new format.
Actions are enabled by default (you can use the actions: { disabled: true }
parameter to disable them, although there is no real benefit). To use:
export variant1 = ({ action }) => <Component onSomething={action('onSomething')} />;
We'll provide simple utilities that can be used for other tools:
import React from 'react';
import { mount } from 'enzyme';
import { jestAction } from '@storybook/addon-actions';
import { variant1 } from './component.examples';
it('calls the callback when you click it', () => {
const action = jestAction();
const wrapper = mount(variant1({ action }));
wrapper.find('p').simulate('click')
expect(action.onSomething).toHaveBeenCalled();
});
Knobs are similar, conceptually to actions:
export variant1 = Object.extend(
({ knobs: { name } }) => <Component name={name.get} onSetName={name.set} />, {
knobs: {
name: 'Default Name',
}
});
We'll provide simple utilities that can be used for other tools:
import React from 'react';
import { mount } from 'enzyme';
import { jestKnobs } from '@storybook/addon-knobs';
import { variant1 } from './component.examples';
it('calls the callback when you click it', () => {
const knobs = jestKnobs(variant1);
const wrapper = mount(variant1({ knobs }));
wrapper.find('p').simulate('click')
expect(knobs.name.set).toHaveBeenCalled();
const value = 'A different value'
knobs.name.set(value);
expect(wrapper.find('p').text).toBe(value)
});
Hey @tmeasday
I really like what you already create here and I would like to add my feedback from projects I worked on.
Glob (
config.examplesGlob
)I would not add a custom module resolution algorithm for storybook (in terms of
config.examplesGlob
) but would rather try to keep it the way it is right now so we can keep allwebpack
module resolution and import features.Glob usage (instead of require context)
If we don't use webpack require context but a glob pattern like you propose we will not be able to handle files created or deleted after the glob was executed.
So this will not update accordingly during development time.
Glob usage (instead of full freedom of customizable imports)
For many large scale projects we have
lerna
mono repositories which split for example small widgets like a small calculator or search component into its own package.If we use a single glob instead of as many require calls as we do right now this introduces a lot of limitations in terms of the projects file structure. - Not forcing a folder structure on the user is one of the main advantages over other styleguide solutions.
Glob usage (node_modules inside mono repositories)
Lerna sub packages may have node_module with symbolic links which might be picked up by the glob.
Api
I created a small proc which allows the following features:
You can see the proof of concept here:
https://codesandbox.io/s/6owq9351w
Parts which are not implemented
I have two further ideas which are not implemented:
Picking up the export name as title if no title is given - e.g.:
Technically it would also be possible not to export the story wrapper.
So it would be almost the same api as with the current storybook implementation:
In combination of the other idea this might look like this:
After all this gives full ide support, typescript support, data reusability, compatibility to jest and similar test frameworks.
Summary
Maybe we don't need dramatic changes but have only drop the chain ability of stories to allow all we need.
What do you think?