Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions code/frameworks/vue3-vite/template/cli/ts/Page.stories.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
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<typeof MyPage, { footer?: string }>;

const meta = {
title: 'Example/Page',
component: MyPage,
render: () => ({
render: (args) => ({
components: { MyPage },
template: '<my-page />',
setup() {
return { args };
},
template: `
<my-page v-bind="args">
<template v-slot:footer>
<footer v-if="args.footer" v-html="args.footer" />
</template>
</my-page>
`,
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof MyPage>;
} satisfies Meta<PageArgs>;

export default meta;
type Story = StoryObj<typeof meta>;

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) => {
Expand Down
121 changes: 120 additions & 1 deletion code/renderers/vue3/src/public-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Button>;
Expand Down Expand Up @@ -209,3 +211,120 @@ it('mount accepts a Component', () => {
};
expectTypeOf(Basic).toMatchTypeOf<StoryObj<typeof Button>>();
});




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<typeof MyComponent, {
footer?: string;
theme?: 'light' | 'dark';
}>;

const meta: Meta<CustomArgs> = {
component: MyComponent,
render: ({ footer, theme, ...componentProps }) => ({
components: { MyComponent },
template: '<my-component v-bind="componentProps" />',
}),
};

type Story = StoryObj<typeof meta>;

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<typeof MyComponent>;

const meta: Meta<NoCustomArgs> = {
component: MyComponent,
args: {
label: 'Test',
},
};

type Story = StoryObj<typeof meta>;

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<typeof MyPage, {
showHeader?: boolean;
footerText?: string;
}>;

const meta: Meta<PageArgs> = {
component: MyPage,
render: ({ showHeader, footerText, variant }) => ({
components: { MyPage },
template: `
<div>
<header v-if="showHeader">Header</header>
<my-page :variant="variant" />
<footer v-if="footerText">{{ footerText }}</footer>
</div>
`,
setup() {
return { showHeader, footerText, variant };
},
}),
};

type Story = StoryObj<typeof meta>;

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;
}>();
});
});
51 changes: 51 additions & 0 deletions code/renderers/vue3/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof MyPage, {
* footer?: string;
* theme?: 'light' | 'dark';
* }>;
*
* const meta = {
* component: MyPage,
* render: ({ footer, theme, ...pageProps }) => ({
* components: { MyPage },
* template: `
* <div :class="theme">
* <MyPage v-bind="pageProps" />
* <footer v-if="footer">{{ footer }}</footer>
* </div>
* `,
* setup() {
* return { footer, theme, pageProps };
* }
* })
* } satisfies Meta<PageArgs>;
*
* export default meta;
* type Story = StoryObj<typeof meta>;
*
* 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<string, any> = Record<string, never>
> = ComponentPropsAndSlots<TComponent> & TCustomArgs;

/**
* Metadata to configure the stories for a component.
*
Expand Down
Loading