文件
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 始終適用於主要元件的 args。

讓我們來談談您可以用來減輕上述情況的一些技術,這些技術在更複雜的情況下特別有用。

重複使用故事定義

我們還可以透過重複使用故事定義來減少故事中的重複。 在這裡,我們可以在 List 的故事中重複使用 ListItem 故事的 args

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>
  ),
};

透過使用其 args 渲染 Unchecked 故事,我們能夠在 List 中重複使用來自 ListItem 故事的輸入資料。

但是,我們仍然沒有使用 args 來控制 ListItem 故事,這表示我們無法使用 controls 變更它們,也無法在其他更複雜的元件故事中重複使用它們。

使用 children 作為 arg

我們改善這種情況的一種方法是將渲染的子元件拉出到 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.dev.org.tw/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 是一個 arg,我們可能會在另一個故事中重複使用它。

但是,使用這種方法時,您應該注意一些注意事項。

children arg,就像所有 args 一樣,需要是 JSON 可序列化的。 為了避免 Storybook 發生錯誤,您應該

  • 避免使用空值
  • 如果您想使用 controls 調整值,請使用 mapping
  • 對於包含第三方函式庫的元件,請謹慎使用

我們目前正在努力改善 children arg 的整體體驗,並允許您在 control 中編輯 children arg,並允許您在不久的將來使用其他類型的元件。 但就目前而言,在實作您的故事時,您需要考慮到這個注意事項。

建立範本元件

另一個更基於「資料」的選項是建立一個特殊的「故事產生」範本元件

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.dev.org.tw/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。