diff --git a/code/frameworks/vue3-vite/template/cli/ts/Page.stories.ts b/code/frameworks/vue3-vite/template/cli/ts/Page.stories.ts index ec3d2488dd06..104a65040329 100644 --- a/code/frameworks/vue3-vite/template/cli/ts/Page.stories.ts +++ b/code/frameworks/vue3-vite/template/cli/ts/Page.stories.ts @@ -1,15 +1,27 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite'; +import type { Meta, StoryObj, WithCustomArgs } from '@storybook/vue3-vite'; import { expect, userEvent, within } from 'storybook/test'; import MyPage from './Page.vue'; +// Example of using custom args that don't map to component props +type PageArgs = WithCustomArgs; + const meta = { title: 'Example/Page', component: MyPage, - render: () => ({ + render: (args) => ({ components: { MyPage }, - template: '', + setup() { + return { args }; + }, + template: ` + + + + `, }), parameters: { // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout @@ -17,11 +29,17 @@ const meta = { }, // This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; +export const WithCustomFooter: Story = { + args: { + footer: 'Built with Storybook', + }, +}; + // More on component testing: https://storybook.js.org/docs/writing-tests/interaction-testing export const LoggedIn: Story = { play: async ({ canvasElement }: any) => { diff --git a/code/renderers/vue3/src/public-types.test.ts b/code/renderers/vue3/src/public-types.test.ts index 740dedb525f4..7291b8d00d46 100644 --- a/code/renderers/vue3/src/public-types.test.ts +++ b/code/renderers/vue3/src/public-types.test.ts @@ -7,12 +7,14 @@ import type { Canvas, ComponentAnnotations, StoryAnnotations } from 'storybook/i import { expectTypeOf } from 'expect-type'; import type { SetOptional } from 'type-fest'; import { h } from 'vue'; +import { defineComponent } from 'vue'; + import BaseLayout from './__tests__/BaseLayout.vue'; import Button from './__tests__/Button.vue'; import Decorator2TsVue from './__tests__/Decorator2.vue'; import DecoratorTsVue from './__tests__/Decorator.vue'; -import type { ComponentPropsAndSlots, Decorator, Meta, StoryObj } from './public-types'; +import type { ComponentPropsAndSlots, Decorator, Meta, StoryObj, WithCustomArgs } from './public-types'; import type { VueRenderer } from './types'; type ButtonProps = ComponentPropsAndSlots; @@ -209,3 +211,120 @@ it('mount accepts a Component', () => { }; expectTypeOf(Basic).toMatchTypeOf>(); }); + + + + +describe('WithCustomArgs', () => { + it(`should allow custom args that don't map to component props`, () => { + const MyComponent = defineComponent({ + props: { + title: { type: String, required: true }, + disabled: { type: Boolean, default: false }, + }, + }); + + type CustomArgs = WithCustomArgs; + + const meta: Meta = { + component: MyComponent, + render: ({ footer, theme, ...componentProps }) => ({ + components: { MyComponent }, + template: '', + }), + }; + + type Story = StoryObj; + + const story: Story = { + args: { + title: 'Hello', + footer: 'Footer text', + theme: 'light', + }, + }; + + // Verify that custom args are properly typed + expectTypeOf(story.args).toHaveProperty('title'); + expectTypeOf(story.args).toHaveProperty('footer'); + expectTypeOf(story.args).toHaveProperty('theme'); + expectTypeOf(story.args).toHaveProperty('disabled'); + }); + + it('should work with empty custom args (no additional args)', () => { + const MyComponent = defineComponent({ + props: { + label: { type: String, required: true }, + }, + }); + + type NoCustomArgs = WithCustomArgs; + + const meta: Meta = { + component: MyComponent, + args: { + label: 'Test', + }, + }; + + type Story = StoryObj; + + const story: Story = { + args: { + label: 'Hello', + }, + }; + + expectTypeOf(story.args).toHaveProperty('label'); + }); + + it('should merge component props with custom args correctly', () => { + const MyPage = defineComponent({ + props: { + variant: { type: String as () => 'primary' | 'secondary', default: 'primary' }, + }, + }); + + type PageArgs = WithCustomArgs; + + const meta: Meta = { + component: MyPage, + render: ({ showHeader, footerText, variant }) => ({ + components: { MyPage }, + template: ` +
+
Header
+ +
{{ footerText }}
+
+ `, + setup() { + return { showHeader, footerText, variant }; + }, + }), + }; + + type Story = StoryObj; + + const story: Story = { + args: { + variant: 'secondary', // From component props + showHeader: true, // Custom arg + footerText: 'Custom footer', // Custom arg + }, + }; + + // Verify all args are typed correctly + expectTypeOf(story.args).toMatchTypeOf<{ + variant?: 'primary' | 'secondary'; + showHeader?: boolean; + footerText?: string; + }>(); + }); +}); diff --git a/code/renderers/vue3/src/public-types.ts b/code/renderers/vue3/src/public-types.ts index bf9257fb63d0..9524ac775bef 100644 --- a/code/renderers/vue3/src/public-types.ts +++ b/code/renderers/vue3/src/public-types.ts @@ -21,6 +21,57 @@ import type { VueRenderer } from './types'; export type { Args, ArgTypes, Parameters, StrictArgs } from 'storybook/internal/types'; export type { VueRenderer }; +/** + * Helper type to extend component props with custom args that don't map directly to component props. + * + * This is useful when you want to use args to control aspects of rendering beyond the component's props, + * such as adding wrapper elements, conditional rendering, theming, or any custom logic in your render function. + * + * @example + * ```typescript + * import type { Meta, StoryObj, WithCustomArgs } from '@storybook/vue3'; + * import MyPage from './Page.vue'; + * + * // Define custom args that extend the component's props + * type PageArgs = WithCustomArgs; + * + * const meta = { + * component: MyPage, + * render: ({ footer, theme, ...pageProps }) => ({ + * components: { MyPage }, + * template: ` + *
+ * + *
{{ footer }}
+ *
+ * `, + * setup() { + * return { footer, theme, pageProps }; + * } + * }) + * } satisfies Meta; + * + * export default meta; + * type Story = StoryObj; + * + * export const WithFooter: Story = { + * args: { + * footer: 'Built with Storybook', + * theme: 'light' + * } + * }; + * ``` + * + * @see https://storybook.js.org/docs/writing-stories/args#args-can-modify-any-aspect-of-your-component + */ +export type WithCustomArgs< + TComponent, + TCustomArgs extends Record = Record +> = ComponentPropsAndSlots & TCustomArgs; + /** * Metadata to configure the stories for a component. *