diff --git a/code/lib/preview-api/template/stories/argMapping.stories.ts b/code/lib/preview-api/template/stories/argMapping.stories.ts index d5fa145700a7..655647660c9d 100644 --- a/code/lib/preview-api/template/stories/argMapping.stories.ts +++ b/code/lib/preview-api/template/stories/argMapping.stories.ts @@ -22,7 +22,7 @@ export default { decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => { return storyFn({ - args: { object: { ...context.args } }, + args: { object: context.args }, }); }, ], diff --git a/code/lib/preview-api/template/stories/argTypes.stories.ts b/code/lib/preview-api/template/stories/argTypes.stories.ts index e0d2115d10ae..93b9540ebb40 100644 --- a/code/lib/preview-api/template/stories/argTypes.stories.ts +++ b/code/lib/preview-api/template/stories/argTypes.stories.ts @@ -8,7 +8,7 @@ export default { // Compose all the argTypes into `object`, so the pre component only needs a single prop decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => - storyFn({ args: { object: { ...context.argTypes } } }), + storyFn({ args: { object: context.argTypes } }), ], argTypes: { componentArg: { type: 'string' }, diff --git a/code/lib/preview-api/template/stories/args.stories.ts b/code/lib/preview-api/template/stories/args.stories.ts index aa9719c4bf2c..7c80ddb679ec 100644 --- a/code/lib/preview-api/template/stories/args.stories.ts +++ b/code/lib/preview-api/template/stories/args.stories.ts @@ -20,8 +20,7 @@ export default { decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => { const { argNames } = context.parameters; - const args = { ...context.args }; - const object = argNames ? pick(args, argNames) : args; + const object = argNames ? pick(context.args, argNames) : context.args; return storyFn({ args: { object } }); }, ], diff --git a/code/lib/preview-api/template/stories/decorators.stories.ts b/code/lib/preview-api/template/stories/decorators.stories.ts index a36eb100a739..91964f5002d3 100644 --- a/code/lib/preview-api/template/stories/decorators.stories.ts +++ b/code/lib/preview-api/template/stories/decorators.stories.ts @@ -40,13 +40,15 @@ export const Hooks = { // decorator that uses hooks (storyFn: PartialStoryFn, context: StoryContext) => { useEffect(() => {}); + return storyFn({ args: { ...context.args, text: `story ${context.args['text']}` } }); }, // conditional decorator, runs before the above - (storyFn: PartialStoryFn, context: StoryContext) => - context.args.condition + (storyFn: PartialStoryFn, context: StoryContext) => { + return context.args.condition ? storyFn() - : (context.originalStoryFn as ArgsStoryFn)(context.args, context), + : (context.originalStoryFn as ArgsStoryFn)(context.unmappedArgs, context); + }, ], args: { text: 'text', diff --git a/code/lib/preview-api/template/stories/hooks.stories.ts b/code/lib/preview-api/template/stories/hooks.stories.ts index 15a4fe90a2ea..4f937686c18b 100644 --- a/code/lib/preview-api/template/stories/hooks.stories.ts +++ b/code/lib/preview-api/template/stories/hooks.stories.ts @@ -8,6 +8,8 @@ export default { }; export const UseState = { + // parameters: { inheritAttrs: true }, + args: { label: 'Clicked 0 times' }, decorators: [ (story: PartialStoryFn) => { const [count, setCount] = useState(0); diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 0835bad6da18..fd64391c0a5a 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -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} storyFn - The story function to decorate. + * @param {DecoratorFunction[]} decorators - The array of decorators to apply to the story function. + * @returns {LegacyStoryFn} - Returns a decorated version of the input storyFn. + */ export function decorateStory( storyFn: LegacyStoryFn, decorators: DecoratorFunction[] ): LegacyStoryFn { - return decorators.reduce( - (decorated: LegacyStoryFn, decorator) => (context: StoryContext) => { - 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} accuDecoratedStoryFn - The accumulator of the decorated story function. + * @param {DecoratorFunction} currentDecoratorFn - The current decorator function. + */ + const finalDecoratedStoryFn = decorators.reduce((accuDecoratedStoryFn, currentDecoratorFn) => { + 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} context - The current context of the story function. + */ + const currentDecoratedStory = (context: StoryContext) => + 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} 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) => { + const story = currentDecoratedStory(context); + if (!storyResult) storyResult = accuDecoratedStoryFn(context); + 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 - ); + 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} context - The current context of the story function. + * @returns {LegacyStoryFn} - Returns the final decorated story function with all the decorators applied. + */ + return (context: StoryContext) => + prepare(finalDecoratedStoryFn(context)) as LegacyStoryFn; } diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index fc04b6defbdb..3deb40c49b3d 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -166,7 +166,7 @@ function getTemplateComponents( if (!template) return [h(story, context?.args)]; return getComponents(template); } catch (e) { - console.log('error', e); + // console.log('error', e); return []; } } @@ -247,7 +247,7 @@ export function generateTemplateSource( .map((child) => child.content) .join('') : ''; - console.log(' vnode ', vnode, ' childSources ', childSources, ' attributes ', attributes); + const name = typeof type === 'string' ? type diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 552438220600..4790d061dc75 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,10 +1,11 @@ /* eslint-disable no-param-reassign */ -import type { App } from 'vue'; -import { createApp, h, reactive, isVNode, isReactive } from 'vue'; -import type { ArgsStoryFn, RenderContext } from '@storybook/types'; +import type { App, ConcreteComponent } from 'vue'; +import { createApp, h, reactive, isReactive } from 'vue'; +import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; -import type { StoryFnVueReturnType, StoryID, VueRenderer } from './types'; +import { cloneDeep } from 'lodash'; +import type { VueRenderer, StoryID } from './types'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -14,7 +15,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - return () => h(Component, props, generateSlots(context)); + return h(Component, props, createOrUpdateSlots(context)); }; // set of setup functions that will be called when story is created @@ -51,10 +52,9 @@ export function renderToCanvas( // normally storyFn should be call once only in setup function,but because the nature of react and how storybook rendering the decorators // we need to call here to run the decorators again // i may wrap each decorator in memoized function to avoid calling it if the args are not changed - const element = storyFn(); // call the story function to get the root element with all the decorators - const args = getArgs(element, storyContext); // get args in case they are altered by decorators otherwise use the args from the context + storyFn(); // call the story function to get the root element with all the decorators - updateArgs(existingApp.reactiveArgs, args); + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); }; @@ -66,17 +66,17 @@ export function renderToCanvas( setup() { storyContext.args = reactive(storyContext.args); const rootElement = storyFn(); // call the story function to get the root element with all the decorators - const args = getArgs(rootElement, storyContext); // get args in case they are altered by decorators otherwise use the args from the context + const appState = { vueApp, - reactiveArgs: reactive(args), + reactiveArgs: reactive(storyContext.args), }; map.set(canvasElement, appState); return () => { // not passing args here as props // treat the rootElement as a component without props - return h(rootElement); + return h(rootElement, appState.reactiveArgs); }; }, }); @@ -107,15 +107,6 @@ function generateSlots(context: StoryContext) { return reactive(Object.fromEntries(slots)); } -/** - * get the args from the root element props if it is a vnode otherwise from the context - * @param element is the root element of the story - * @param storyContext is the story context - */ - -function getArgs(element: StoryFnVueReturnType, storyContext: StoryContext) { - return element.props && isVNode(element) ? element.props : storyContext.args; -} /** * update the reactive args @@ -133,7 +124,27 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args) { } }); // update currentArgs with nextArgs - Object.assign(currentArgs, nextArgs); + Object.assign(currentArgs, cloneDeep(nextArgs)); +} + +const slotsMap = new Map< + StoryID, + { + component?: Omit, 'props'>; + reactiveSlots?: Args; + } +>(); + +function createOrUpdateSlots(context: StoryContext) { + const { id: storyID, component } = context; + const slots = generateSlots(context); + if (slotsMap.has(storyID)) { + const app = slotsMap.get(storyID); + if (app?.reactiveSlots) updateArgs(app.reactiveSlots, slots); + return app?.reactiveSlots; + } + slotsMap.set(storyID, { component, reactiveSlots: slots }); + return slots; } /** diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/ScopedSlots.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/ScopedSlots.stories.ts index 95266e452711..5f37715a3b59 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/ScopedSlots.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/ScopedSlots.stories.ts @@ -1,6 +1,6 @@ import { expect } from '@storybook/jest'; import { global as globalThis } from '@storybook/global'; -import type { Channel } from '@storybook/channels'; + import { within } from '@storybook/testing-library'; import { UPDATE_STORY_ARGS, STORY_ARGS_UPDATED, RESET_STORY_ARGS } from '@storybook/core-events'; @@ -9,7 +9,7 @@ import MySlotComponent from './MySlotComponent.vue'; declare global { // eslint-disable-next-line no-var,vars-on-top,@typescript-eslint/naming-convention - var __STORYBOOK_ADDONS_CHANNEL__: Channel; + var __STORYBOOK_ADDONS_CHANNEL__: any; } const meta = {