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 :) thanks for your quick response
My main goal is to keep the stories of storybook simple to learn, simple to maintain and simple to understand.
The second goal is to provide power users the ability to reuse components in tests and other stories.
1. Globing
I was a little bit afraid that this might cause a lot of work to achieve the same features that are here today - but it looks like you already thought about this topic a lot of #4169 so I am happy to exclude it here.
2. Exporting meta data and helper functions
Actually I would like it a lot if we could manage to keep the way javascript is written for stories is like every where else.
If we manage keeping the api very simple we won't need to change the entire javascript module system.
As a simple solution to your question - if a store is exported which is easy to detect then we can get its children in a very simple way.
Actually that is how the previous codesandbox works right now.
3. Similar API
In my opinion if users buy a new product from the same brand like an operation system, a phone or a car they want to reuse most of what they learned. So even if you don't love the api is a reason for the success of storybook. Invent an entire new system (like Angular1 to Angular2) can make people quite upset because they don't want to give up on the previous user experience and learnings.
Of cause right now some legacy decision block the way forward so this part will be a challenging balance act.
However I totally agree that if the api is similar but different people will copy over the wrong examples from stackoverflow and will have a hard time to find out what is wrong.
4. Exporting
I agree that a thin api without sub dependencies is a great idea.
But I believe a store can help a lot here if it is done right and probably without any harm to jest.
The following store could easily be executed by jest as long as we don't introduce life cycle hooks:
A story file might look a little bit like the following demo:
The names
createStoryGroup
or.create
can probably be improved a lot but maybe you get the idea.The exported store can be picked up by the core storybook javascript code just like a "database".
As will know all exported names from the file and all stories from that "database" it can even reuse the exported name like in the
Orange
example.I hope you are not to scarred about the store idea and maybe you can outline where you see problems for such a tiny store.