-
-
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
Vue3: Fix Conditional decorators #23029
Changes from all commits
48f343d
7d6626d
c950f97
0bb0788
29efc3d
a5dc5b1
2d2f7ea
ba1da92
1fb6d73
4bc5b4d
c939afb
15f94d0
5a7ce9f
1ca0097
3c4a4fa
f7ec799
e5a69e3
0781211
9a487b0
eeea195
2825671
cb44c68
5503a11
b1e7a5e
93b0c9d
94443e3
a062528
895b01b
d26c25f
51bd30f
3bcada9
aba16cb
2223cd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,56 +14,98 @@ import type { VueRenderer } from './types'; | |
function normalizeFunctionalComponent(options: ConcreteComponent): ComponentOptions { | ||
return typeof options === 'function' ? { render: options, name: options.name } : options; | ||
} | ||
|
||
function prepare( | ||
rawStory: VueRenderer['storyResult'], | ||
innerStory?: ConcreteComponent | ||
): Component | null { | ||
const story = rawStory as ComponentOptions; | ||
if (story === null) { | ||
/** | ||
* This function takes a rawStory (as returned by the storyFn), and an innerStory (optional), | ||
* which is used to render child components and returns a Component object. | ||
* If an innerStory is provided, it merges the components object of the story with the components object for the innerStory. | ||
* | ||
* @param {VueRenderer['storyResult']} rawStory - The rawStory returned by the storyFn. | ||
* @param {ConcreteComponent} innerStory - Optional innerStory used for rendering child components. | ||
* @returns {Component | null} - Returns a Component object that can be rendered. | ||
*/ | ||
function prepare(story: VueRenderer['storyResult'], innerStory?: Component): Component | null { | ||
if (!story) { | ||
return null; | ||
} | ||
if (typeof story === 'function') return story; // we don't need to wrap a functional component nor to convert it to a component options | ||
|
||
// If story is already a function, we don't need to wrap it nor convert it | ||
if (typeof story === 'function') return story; | ||
|
||
// Normalize the functional component, and make sure inheritAttrs is set to false | ||
const normalizedStory = { ...normalizeFunctionalComponent(story), inheritAttrs: false }; | ||
|
||
// If an innerStory is provided, merge its components object with story.components | ||
if (innerStory) { | ||
return { | ||
// Normalize so we can always spread an object | ||
...normalizeFunctionalComponent(story), | ||
...normalizedStory, | ||
components: { ...(story.components || {}), story: innerStory }, | ||
}; | ||
} | ||
|
||
// Return a Component object with a render function that returns the normalizedStory | ||
return { | ||
render() { | ||
return h(story); | ||
return h(normalizedStory); | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* This function takes a storyFn and an array of decorators, and returns a decorated version of the input storyFn. | ||
* | ||
* @param {LegacyStoryFn<VueRenderer>} storyFn - The story function to decorate. | ||
* @param {DecoratorFunction<VueRenderer>[]} decorators - The array of decorators to apply to the story function. | ||
* @returns {LegacyStoryFn<VueRenderer>} - Returns a decorated version of the input storyFn. | ||
*/ | ||
export function decorateStory( | ||
storyFn: LegacyStoryFn<VueRenderer>, | ||
decorators: DecoratorFunction<VueRenderer>[] | ||
): LegacyStoryFn<VueRenderer> { | ||
return decorators.reduce( | ||
(decorated: LegacyStoryFn<VueRenderer>, decorator) => (context: StoryContext<VueRenderer>) => { | ||
let story: VueRenderer['storyResult'] | undefined; | ||
/** | ||
* This function receives two arguments: accuDecoratedStoryFn (the accumulator for the decorated story function) and currentDecoratorFn (the current decorator function). | ||
* It applies the decorator to the accuDecoratedStoryFn by returning a new decorated storyFn from the currentDecoratedStory function. | ||
* | ||
* @param {LegacyStoryFn<VueRenderer>} accuDecoratedStoryFn - The accumulator of the decorated story function. | ||
* @param {DecoratorFunction<VueRenderer>} currentDecoratorFn - The current decorator function. | ||
*/ | ||
const finalDecoratedStoryFn = decorators.reduce((accuDecoratedStoryFn, currentDecoratorFn) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ndelangen @tmeasday would you mind taking a look? Thanks! |
||
let storyResult: VueRenderer['storyResult']; | ||
|
||
const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { | ||
const sanitizedUpdate = sanitizeStoryContextUpdate(update); | ||
// update the args in a reactive way | ||
if (update) sanitizedUpdate.args = Object.assign(context.args, sanitizedUpdate.args); | ||
story = decorated({ ...context, ...sanitizedUpdate }); | ||
return story; | ||
/** | ||
* This function receives a context argument (the current context of the story function), | ||
* and returns a decorated version of the story function. | ||
* | ||
* @param {StoryContext<VueRenderer>} context - The current context of the story function. | ||
*/ | ||
const currentDecoratedStory = (context: StoryContext<VueRenderer>) => | ||
currentDecoratorFn((update) => { | ||
const mergedContext = { ...context, ...sanitizeStoryContextUpdate(update) }; | ||
storyResult = accuDecoratedStoryFn(mergedContext); | ||
context.args = mergedContext.args; | ||
return storyResult; | ||
}, context); | ||
|
||
if (!story) story = decorated(context); | ||
|
||
if (decoratedStory === story) { | ||
return story; | ||
} | ||
/** | ||
* This function receives a context argument (the current context of the story function), | ||
* and returns the final decorated story function with all the decorators applied | ||
* @param {StoryContext<VueRenderer>} context - The current context of the story function. | ||
* @returns {VueRenderer['storyResult']} - Returns the final decorated story function with all the decorators applied. | ||
*/ | ||
return (context: StoryContext<VueRenderer>) => { | ||
const story = currentDecoratedStory(context); | ||
if (!storyResult) storyResult = accuDecoratedStoryFn(context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this line mean the later decorators will get called even if the earlier decorator ( As an aside it would help me at least (maybe it's obvious to others) to explain what's happening here -- why do we return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exactly @tmeasday if you don't get a deccorated Story from the decorator or the decorator did you call storyFn, we just take the last one. so we can now have decorator that returns null, so we kind of skip them. (storyFn: PartialStoryFn, context: StoryContext) => context.args.condition? storyFn(): null There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
we don't need to prepare it because the previous one is already prepared we just return it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can have decorators that have just side effects and not touching the rendering Tree. by doing something like this. (storyFn: PartialStoryFn, context: StoryContext) => {
saveData(context.args.data)
return null
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
😄 I think you are the only one or at least to first one who spotted the Key change in this PR that fix the conditional decorator issue There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm confused. So a decorator that returns There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no of course the children won't render since the parent not providing any story, one thing is important I'm sure you know it is rendering should happen down -> up the tree. decorators.movThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what I meant no effect on the parent so only if put it like last to run decorators: [
// conditional decorator, runs before last
(storyFn: PartialStoryFn, context: StoryContext) => {
return context.args.condition
? storyFn()
: null;
},
// decorator A that uses hooks
(storyFn: PartialStoryFn, context: StoryContext) => {
useEffect(() => {});
return storyFn({ args: { ...context.args, text: `WrapperA( ${context.args['text']} )` } });
},
// decorator B that uses hooks
(storyFn: PartialStoryFn, context: StoryContext) => {
useEffect(() => {});
return storyFn({ args: { ...context.args, text: `WrapperB( ${context.args['text']} )` } });
},
], in this case, won't affect the rendered tree. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is a repo on Stackblitz. if you want to test that fast without spawning any sandbox https://stackblitz.com/~/github.com/chakAs3/storybook-vue3-decorators |
||
if (!story) return storyResult; | ||
if (story === storyResult) return storyResult; | ||
|
||
const innerStory = () => h(story!); | ||
return prepare(decoratedStory, innerStory) as VueRenderer['storyResult']; | ||
}, | ||
(context) => prepare(storyFn(context)) as LegacyStoryFn<VueRenderer> | ||
); | ||
return prepare(story, () => h(storyResult, context.args)) as VueRenderer['storyResult']; | ||
}; | ||
}, storyFn); | ||
/** | ||
* This function receives a context argument (the current context of the story function), and returns the final decorated story function with all the decorators applied. | ||
* | ||
* @param {StoryContext<VueRenderer>} context - The current context of the story function. | ||
* @returns {LegacyStoryFn<VueRenderer>} - Returns the final decorated story function with all the decorators applied. | ||
*/ | ||
return (context: StoryContext<VueRenderer>) => | ||
prepare(finalDecoratedStoryFn(context)) as LegacyStoryFn<VueRenderer>; | ||
} |
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 disable attribute fallthough on all the prepared stories including decorated onces,