Currently the principal way addons work in the preview context (ie acting on the story) is via decorators.
There are 3 types of thing that the decorators do:
- Altering the story in a very real way: "modifiers"
- Inspecting the story, and sending telemetry somewhere (typically to the manager context via the addon channel): "inspectors"
- Adding context to the story: "wrappers" -- this includes both:
- CSS/Visual things like a wrapping class or background color, setting viewport size
- React
context
things like redux/router providers.
Ultimately all decorators are HOCs: they take as input a story component[1] and output a decorated story component.
[1] Currently stories don't take {props, context}
but they behave like stateless functional components in all other ways.
There are also 3 ways of applying a decorator:
- Globally (
addDecorator(decorator)
), - Per-chapter (
storiesOf(X).addDecorator(decorator)
) - Per-story (
storiesOf(X).add('name', decorator(() => <story/>))
).
It is possible to configure a decorator on a per-story basis via making it a function that takes options and returns a HOC:
// implementation
export default function myDecorator(params) {
return (storyFn) => (context) => {
// do stuff
return storyFn(context); // (or wrap it, etc)
}
}
// usage
addDecorator(myDecorator({ option: 'value'}));
You need to ensure all your "inspector" decorators apply before any "wrapper" decorators.
However, you might not realise that. This leads to bugs that are entirely avoidable (see below).
Also, suppose you are using the wrapper globally:
addDecorator(mockedI18nProvider);
There's no way to add a "per-chapter" inspector that applies before that wrapper:
storiesOf('X')
.addDecorator(jsxDecorator); // <- this will apply *after* the i18n decorator
Conversely, if you want to add a wrapper decorator at the story level, it will apply before any existing inspectors etc.
If your story looks like:
.add('name', barInspector(() =>
<FooWrapper>
<Component />
</FooWrapper>
));
It is pretty unclear which parts are integral to the story we are looking at and which parts are there for contextual or diagnostic purposes.
It would be good to separate them.
- We add a third optional argument to the
.add()
API that takes an object of per-story options.
Adding the decorators
applies one or more decorators to that story:
storiesOf('X')
.add('name', () => <story/>, { decorators: [myDecorator, foo] });
This solves problem III.
- Decorators remain as HOCs, however they can optionally have a property
type
which indicates which sort of decorator they are:
export default function myDecorator() { ... }
myDecorator.type = 'inspector';
Decorators are applied in a special order:
- All "modifier" decorators (you'll probably need to closely control the order of these, but chances are they are all added at the story level)
- All "inspector" decorators.
- All "wrapper" decorators.
Within each type, they are applied in the order the user specified them.
Decorators without the property are assumed to be wrappers.
This takes the onus of ordering decorators off the user and places it on the addon writer. It solves problem I and mostly solves problem II.
- [OPTIONAL] We also add tools that make it really easy to create inspector decorators that post to the channel:
export default createInspectorDecorator(name, (storyElement, emit) => {
emit(elementToJSX(storyElement));
});
Maybe we can continue this discussion and use it as a base to move forward these proposals: Addons composition storybookjs/storybook#1473, Addon API storybookjs/storybook#1212
Terminology offer.
My idea is to continue this "Types of decorator" but maybe in a bit simpler way:
We can divide them to addons and decorators in the sense that:
So we can say, that:
(additionaly: depending on what implementation of this list storybookjs/storybook#1473 (comment) we can allow addons to not return anything at all)
I think this is intuitively consistent with what users expect based on their previous experience with Storybook.
Of course, addons and decorators have the same API so it's just a terminology issue. But maybe it's worth to avoid mixed solutions in order to provide better user experience and in relation to my offer below:
Applying levels.
As it said in "Usage of decorators" there are 3 levels of applying an addon/decorator. Just want to add here that both addons and decorators could be applied at any level. And it would be nice to have the most similar APIs for each level.
(additionaly: actually we can have the similar API for all: addon/decorator/storyFn. I guess I could simplify many things. example: the offer below)
Offer for I and III issues
At this time we can say that we standardize that addon/decorator in very general is a function like
withX
of(storyFn, context, ...props)
arguments.My offer is that also at a very general level (without regard to any particular implementation) we can provide somehow to this function one more argument:
withX(wrappedStoryFn, context, pureStoryFn, ...props)
where the
wrappedStoryFn
is a story returned from previous decorators and thepureStoryFn
is the first provided story by.add(...)
.And so "addons" can take to process the
pureStoryFn
while "decorators" should takewrappedStoryFn
, change it and return new wrapped story to the next "decorator".So when you're creating an addon it's up to you what to use depending what it should be: "addon" or "decorator".
This allows users to not worry about in what order they should apply "addons". And "addons" could be applied within "decorators".
Of course, the order of "decorators" still matters!
Implementation example:
https://github.com/storybooks/storybook/pull/1473/files#diff-d93e775000f95040e90de160e5c0bb2dR23
In this PR I add to
context
additional fieldcleanStory
that could be changed only bystoryOf
function providing the initial story. So you can choose to base your addon onstoryFn
which could be polluted or on purecontext.cleanStory
which always clean.(also this implementation can be improved to allow "addons" to not return anything at all)
I guess some other options from this list storybookjs/storybook#1473 (comment) could provide the similar approach.