-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Addons composition #1473
Conversation
Codecov Report
@@ Coverage Diff @@
## release/3.3 #1473 +/- ##
==============================================
Coverage ? 22.43%
==============================================
Files ? 236
Lines ? 5341
Branches ? 751
==============================================
Hits ? 1198
Misses ? 3598
Partials ? 545
Continue to review full report at Codecov.
|
@tmeasday, @shilman, @ndelangen, @wcastand, @tuchk4 interesting to test it with It's a proposal and I guess we have options:
|
Hey @usulpro, interesting proposal! Although it's maybe similar to storybook's API already, I'm not really a huge fan of jquery style chained APIs. I tend to think just a pure functional API is simpler to understand and I think the API awkwardness you mentioned can be resolved in a couple of ways:
import { compose } from 'recompose';
// ...
.add('addons composition',
compose(
withInfo('withInfo'),
addonNotes({ notes: 'with notes' })
)(() => <button>click me</button>)
);
function withInfoAndNotes(notes) {
return compose(
withInfo(notes),
addonNotes({ notes: notes })
);
}
.add('addons composition', withInfoAndNotes('my note')(() => <button>click me</button>));
// There have been a few suggestions, this seems a simple one:
.add('addons composition',
[withInfo('withInfo'), addonNotes({ notes: 'with notes' })]
() => <button>click me</button>
); (The above is a simpler variant of my addon proposal #1212 (comment)) We should revive that discussion, I think we need to collectively make a decision on this API to move forward. |
@tmeasday, thank you for your response! Totaly agree that we need to move forward, actually, it's the reason of this post. I've studied your proposal and tried to find the simplest implementation for your ideas. tl;dr What do you think if we take your third example and turn it to this: .add('addons composition',
[
withInfo('withInfo'),
addonNotes({ notes: 'with notes' }),
() => <button>click me</button>,
]); Details: It could take advantages of both approaches and still has tiny changes to API In my previous example, I passed to addons additional prop In this case, the order of "inspectors" doesn't matter and they even could go mixed with other types of addons. I guess the order of "modifiers" and "wrappers" always matter, so we can't do anything with it. At the same time maybe it worth to determine that "addons" are only "inspectors". and "decorators" are only "modifiers" or "wrappers" (with the same API though). So we can have a convention that "addons" shouldn't "pollute" the story. And does it sense that "decorators" should have the possibility to be applied both as a "per-chapter decorators" (.addDecorator) and as a "per-story decorators"? So that's why I offer this simple structure for addons/decorators: |
@wcastand doesn't you raise here #1212 (comment) the similar question of "polluted" stories?
|
If i had to choose one API proposal, i'd choose mine obviously x) but with the two choices you made on this thread (yours and tmeasday' s). Doesn't mean i think it's a good solution. How would you manage the different level for adding addons:
My main problem with your solution is that is backward compatible i guess. I think it's a bad idea to keep it backward compatible in the case of addon in storybook. Because right now it's a mess and it's not clean (for dev and users) which addons does what, which addons modify or not your story, ... A clear break, with clear guidelines and boundaries for addons will benefit storybook in the long run more than making something that works with the previous API because it mean even more ways to do the same things and no real "rules". That's how i see things, i know i'm pretty lonely on this one but i still think it's a good way of thinking about this. I don't think being backward compatible is always the best thing :). Plus, not backward compatible doesn't mean, we leave you with your old code and have fun migrating to the new one. We can provide migration guide, codemods, tutorials, and help the transition. React does it pretty well, webpack too. |
I'm always of the opinion that we shouldn't change the API unless there is some benefit to it (something we could do that we couldn't do before), so I'm in favor of the simplest proposal, which requires only that we standardize on // ...
.add('addons composition',
compose(
withInfo('withInfo'),
withNotes({ notes: 'with notes' })
)(() => <button>click me</button>)
); @tmeasday and I discussed ways that we can make this less verbose in the future, for example: registerAddon({ info: withInfo, notes: withNotes })
storiesOf('path/to/Component', module)
.add('story', () => <Component />, {
info: 'some info',
notes: { notes: 'some note', option1: 'some other option' }
}) The "benefit" here being that we've discussed the need for adding options to individual stories in other places and we are reusing that functionality here to make the addon configuration for each story less verbose. We could further standardize on on For me the priority is to create a common convention and simply use |
There are two problems with addon/decorators that need to be solved, if we assume that we are moving toward a world where addons export a decorator on the preview side (this is the
I think the two problems are fairly separate (although not completely, of course). Perhaps in this issue we should stick to problem 1. above -- @usulpro I'm not quite sure I understand what you are saying about problem 2, but maybe let's discuss elsewhere (on #1212?) So, considering just the problem of "how do I apply (multiple) decorators at the level of a story?", there seem to be a few approaches:
.add('...', A(args)(B(args)(() => <story/>)));
.add('...', compose(
A(args),
B(args)
)(() => <story/>));
.add('...', [
A(args),
B(args),
() => <story/>,
]);
.add('...', [
A(args),
B(args),
], () => <story/>);
.add('...', () => <story/>, {
decorators: [
A(args),
B(args),
]
}); // In `.storybook/config`
registerDecorators({
a: A,
b: B,
});
// In each story
.add('...', () => <story/>, {
a: args,
b: args,
});
.getAddons(A, B)
.add('with addons', context =>
context
.storyOf(<story/>)
.withA(args)
.withB(args);
) Phew! I'm not sure which is best but it's good to see them all here. |
FWIW, having needed multiple addons that use the Having introduced Storybook (with a bunch of addons) on a project recently, it definitely would have made my life easier if the addon API was like the chaining proposal. Having said that, for addons like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look like a good idea to me.
.addDecorator(withKnobs) | ||
.getAddons(addInfo, withNotes, withBorder) | ||
.add('with addons', context => | ||
context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not really an api I'm really enthusiastic about to be honest.
I don't like it because the interface is modified by addons. I fear that this is too flexible, and can lead us to all sorts of problems.
Closing this because there hasn't been any activity here for a while. Feel free to reopen when this is ready for discussion again. |
Issue:
#766 and other discussions
What we have at this moment:
withX() in general has such API:
At this moment we have
withInfo
andaddonNotes
. And we can compose them this way:It's much better but we still have issues with this:
a. It looks like a kind of "callbacks hell" when we need to compose several addons
b. withX() should have such structure:
.add()
where
context
is an object:{ story, kind }
. Usually, we omitcontext
:add(name, () => <Story />)
and decorators API is:
What we want:
Convenient and flexible way for applying addons
Easy approach for creating new addons
Proposal:
It shouldn't break the existing API and should be backward compatible.
Actually, this approach still keeps the same API:
Here
withX
just returns new component and do some side effects.What I did:
I just created an addon to implement this behavior! :)
It works this way:
In this example we create a
<Button>
and applying addon in the following order:We can add normal stories to the same
storiesOf
- they will work as usualThe pros of this approach
It's backward compatible with existing API
Easy to compose addons, change addons order, disable/enable addons.
Addons with such structure could be easily turned to decorators:
context.storyOf
it will be invoked with an argumentprevStory
- the result of previous execution ofcontext.storyOf
:In this example
withJSX()
shows details of<Button>
while we see the button wrapped into div. Even ifwithJSX()
change the story somehow,prevStory
will be the clean value passed directly from previousstoryOf
. I guess it's a quite unusual feature and may be discussed in a separate issue.How to test
Run cra-kitchen-sink
Play with the example:
src of addon:
examples/cra-kitchen-sink/.storybook/addon-composition.js
example:
examples/cra-kitchen-sink/src/stories/index.js
Additional
I guess one point could confuse in this addon implementation:
if we don't want to do it for every
storiesOf
maybe more convinient could be to do it ones inconfig.js
:but for that, we need a feature in
setAddon
API to execute a function whenstoriesOf
is creating (something like 'constructor' for classes). This feature is introduced in this PR: #1463