文档
Storybook 文档

多个组件的故事

如果这些组件被设计为一起工作,编写可以渲染两个或多个组件的故事非常有用。例如,ButtonGroupListPage 组件。

子组件

当您正在记录的组件具有父子关系时,您可以使用 subcomponents 属性将它们一起记录。当子组件不打算单独使用,而仅作为父组件的一部分使用时,这尤其有用。

以下是一个使用 ListListItem 组件的示例

List.stories.ts|tsx
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
const meta: Meta<typeof List> = {
  component: List,
  subcomponents: { ListItem }, //👈 Adds the ListItem component as a subcomponent
};
export default meta;
 
type Story = StoryObj<typeof List>;
 
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. 每个已记录子组件的表格 *不* 包括 controls 来更改 props 的值,因为 controls 始终应用于主组件的参数。

让我们讨论一些您可以用来缓解上述问题的技巧,这些技巧在更复杂的情况下尤其有用。

重用故事定义

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

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
 
const meta: Meta<typeof List> = {
  component: List,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem {...Selected.args} />
      <ListItem {...Unselected.args} />
      <ListItem {...Unselected.args} />
    </List>
  ),
};

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

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

使用子元素作为参数

我们改进这种情况的一种方法是将渲染的子组件提取到 children 参数中

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
 
//👇 Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';
 
const meta: Meta<typeof List> = {
  /* 👇 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,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
export const OneItem: Story = {
  args: {
    children: <Unchecked {...Unchecked.args} />,
  },
};

现在 children 是一个参数,我们可以在另一个故事中重用它。

但是,使用此方法时有一些注意事项,您应该注意。

children 参数,就像所有参数一样,需要是 JSON 可序列化的。为避免 Storybook 出现错误,您应该

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

我们目前正在努力改进 children 参数的整体体验,并允许您在控件中编辑 children 参数,并允许您在不久的将来使用其他类型的组件。但目前,在实现故事时需要考虑此注意事项。

创建模板组件

另一种更“数据”导向的方法是创建一个特殊的“故事生成”模板组件

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';
 
const meta: Meta<typeof List> = {
  /* 👇 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,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
//👇 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。