加入直播会议:周四,美国东部时间上午 11 点,Storybook 9 发布及问答
文档
Storybook Docs

多个组件的故事

如果组件设计为协同工作,编写能够同时渲染两个或多个组件的故事会很有用。例如,ButtonGroupListPage 组件。

子组件

当您正在文档化的组件之间存在父子关系时,您可以使用 subcomponents 属性将它们一起文档化。这在子组件不单独使用,而仅作为父组件的一部分使用时尤其有用。

这是一个使用 ListListItem 组件的示例

List.stories.ts|tsx
import React from 'react';
 
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
const meta = {
  component: List,
  subcomponents: { ListItem }, //👈 Adds the ListItem component as a subcomponent
} satisfies Meta<typeof List>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Empty: Story = {};
 
export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem />
    </List>
  ),
};

请注意,通过向默认导出添加 subcomponents 属性,我们在 ArgTypesControls 表格上会获得一个额外的面板,列出 ListItem 的 props。

Subcomponents in ArgTypes doc block

子组件仅用于文档目的,并有一些限制

  1. 子组件的 argTypes 是推断出来的(对于支持此功能的渲染器),不能手动定义或覆盖。
  2. 每个文档化的子组件的表格*不*包括用于更改 props 值的controls,因为 controls 始终应用于主组件的 args。

接下来,我们将讨论一些可以用来弥补上述限制的技术,这些技术在更复杂的情况下尤其有用。

重用故事定义

我们还可以通过重用故事定义来减少故事中的重复。在这里,我们可以在 List 的故事中重用 ListItem 故事的 args。

List.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem {...Selected.args} />
      <ListItem {...Unselected.args} />
      <ListItem {...Unselected.args} />
    </List>
  ),
};

通过使用其 args 渲染 Unchecked 故事,我们能够在 List 中重用 ListItem 故事的输入数据。

然而,我们仍然没有使用 args 来控制 ListItem 故事,这意味着我们无法使用 controls 来更改它们,也无法在其他更复杂的组件故事中重用它们。

将 children 用作 arg

改进这种情况的一种方法是将渲染的子组件提取到 children arg 中。

List.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
 
//👇 Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';
 
const meta = {
  /* 👇 The title prop is optional.
   * See https://storybook.org.cn/docs/configure/#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'List',
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const OneItem: Story = {
  args: {
    children: <Unchecked {...Unchecked.args} />,
  },
};

现在 children 是一个 arg,我们可以在另一个故事中潜在地重用它。

然而,使用这种方法时有一些需要注意的警告。

children arg 与所有 args 一样,需要是 JSON 可序列化的。为了避免 Storybook 出错,您应该

  • 避免使用空值
  • 如果您想使用 controls 调整值,请使用映射
  • 使用包含第三方库的组件时要小心

我们目前正在努力改进 children arg 的整体体验,未来将允许您在 control 中编辑 children arg,并允许您使用其他类型的组件。但目前在实现故事时需要考虑这个警告。

创建一个模板组件

另一种更“数据”驱动的选项是创建一个特殊的“故事生成”模板组件

List.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';
 
const meta = {
  /* 👇 The title prop is optional.
   * Seehttps://storybook.org.cn/docs/configure/#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'List',
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
//👇 The ListTemplate construct will be spread to the existing stories.
const ListTemplate: Story = {
  render: ({ items, ...args }) => {
    return (
      <List>
        {items.map((item) => (
          <ListItem {...item} />
        ))}
      </List>
    );
  },
};
 
export const Empty = {
  ...ListTemplate,
  args: {
    items: [],
  },
};
 
export const OneItem = {
  ...ListTemplate,
  args: {
    items: [{ ...Unchecked.args }],
  },
};

这种方法设置起来稍微复杂一些,但这意味着您可以更轻松地在复合组件中重用每个故事的 args。这也意味着您可以使用 Controls 插件更改组件的 args。