Docs
Storybook Docs

多个组件的故事

如果这些组件设计为协同工作,那么一次编写 渲染两个或更多组件 的故事很有用。例如,ButtonGroupListPage 组件。

¥It's useful to write stories that render two or more components at once if those components are designed to work together. For example, ButtonGroup, List, and Page components.

子组件

¥Subcomponents

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

¥When the components you're documenting have a parent-child relationship, you can use the subcomponents property to document them together. This is especially useful when the child component is not meant to be used on its own, but only as part of the parent component.

这是一个包含 ListListItem 组件的示例:

¥Here's an example with List and ListItem components:

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 属性,我们会在 ArgTypes控件 表上获得一个额外的面板,列出 ListItem 的属性:

¥Note that by adding a subcomponents property to the default export, we get an extra panel on the ArgTypes and Controls tables, listing the props of ListItem:

Subcomponents in ArgTypes doc block

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

¥Subcomponents are only intended for documentation purposes and have some limitations:

  1. 子组件的 argTypes推断(对于支持该功能的渲染器),不能手动定义或覆盖。

    ¥The argTypes of subcomponents are inferred (for the renderers that support that feature) and cannot be manually defined or overridden.

  2. 每个记录的子组件的表格不包括 controls 来更改 props 的值,因为控件始终适用于主组件的参数。

    ¥The table for each documented subcomponent does not include controls to change the value of the props, because controls always apply to the main component's args.

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

¥Let's talk about some techniques you can use to mitigate the above, which are especially useful in more complicated situations.

重复使用故事定义

¥Reusing story definitions

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

¥We can also reduce repetition in our stories by reusing story definitions. Here, we can reuse the ListItem stories' args in the story for 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 故事的输入数据。

¥By rendering the Unchecked story with its args, we are able to reuse the input data from the ListItem stories in the List.

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

¥However, we still aren’t using args to control the ListItem stories, which means we cannot change them with controls and we cannot reuse them in other, more complex component stories.

使用 children 作为参数

¥Using children as an arg

我们改善这种情况的一种方法是将渲染的子组件拉出到 children 参数中:

¥One way we improve that situation is by pulling the rendered subcomponent out into a children arg:

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.js.org/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 是一个参数,我们可以在另一个故事中重复使用它。

¥Now that children is an arg, we can potentially reuse it in another story.

但是,使用此方法时有一些注意事项需要注意。

¥However, there are some caveats when using this approach that you should be aware of.

children arg 与所有 arg 一样,需要 JSON 可序列化。为了避免 Storybook 出现错误,你应该:

¥The children arg, just like all args, needs to be JSON serializable. To avoid errors with your Storybook, you should:

  • 避免使用空值

    ¥Avoid using empty values

  • 如果你想使用 controls 调整值,请使用 mapping

    ¥Use mapping if you want to adjust the value with controls

  • 谨慎使用包含第三方库的组件

    ¥Use caution with components that include third party libraries

我们目前正在努力改善子参数的整体体验,并允许你在控件中编辑子参数,并允许你在不久的将来使用其他类型的组件。但是现在,你在实现故事时需要考虑这个警告。

¥We're currently working on improving the overall experience for the children arg and allow you to edit children arg in a control and allow you to use other types of components in the near future. But for now you need to factor in this caveat when you're implementing your stories.

创建模板组件

¥Creating a Template Component

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

¥Another option that is more “data”-based is to create a special “story-generating” template component:

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.js.org/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 插件更改组件的参数。

¥This approach is a little more complex to setup, but it means you can more easily reuse the args to each story in a composite component. It also means that you can alter the args to the component with the Controls addon.