返回Storybook 简介
章节
  • 入门
  • 简单组件
  • 组合组件
  • 数据
  • 屏幕
  • 部署
  • 可视化测试
  • 插件
  • 结论
  • 贡献

组装组合组件

由更简单的组件组装组合组件

在上一章中,我们构建了第一个组件;本章将扩展我们所学的知识,以构建一个 TaskList,它是任务的列表。让我们将组件组合在一起,看看引入更多复杂性时会发生什么。

TaskList

Taskbox 通过将置顶任务置于默认任务上方来强调它们。它产生了两种不同变体的 TaskList,你需要为其创建故事:默认和置顶项。

default and pinned tasks

由于 Task 数据可以异步发送,我们需要一个加载状态,以便在没有连接时进行渲染。此外,当没有任务时,我们还需要一个空状态。

empty and loading tasks

开始设置

组合组件与它包含的基本组件没有太大区别。创建一个 TaskList 组件及其配套的故事文件:src/app/components/task-list.component.tssrc/app/components/task-list.stories.ts

TaskList 的一个初步实现开始。你需要导入前面创建的 Task 组件,并将属性和操作作为输入传递进去。

复制
src/app/components/task-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

import { Task } from '../models/task.model';

@Component({
  selector: 'app-task-list',
  standalone: false,
  template: `
    <div class="list-items">
      <div *ngIf="loading">loading</div>
      <div *ngIf="!loading && tasks.length === 0">empty</div>
      <app-task
        *ngFor="let task of tasks"
        [task]="task"
        (onArchiveTask)="onArchiveTask.emit($event)"
        (onPinTask)="onPinTask.emit($event)"
      >
      </app-task>
    </div>
  `,
})
export default class TaskListComponent {
  /** The list of tasks */
  @Input() tasks: Task[] = [];

  /** Checks if it's in loading state */
  @Input() loading = false;

  /** Event to change the task to pinned */
  // tslint:disable-next-line: no-output-on-prefix
  @Output()
  onPinTask = new EventEmitter<Event>();

  /** Event to change the task to archived */
  // tslint:disable-next-line: no-output-on-prefix
  @Output()
  onArchiveTask = new EventEmitter<Event>();
}

接下来,在故事文件中创建 TaskList 的测试状态。

复制
src/app/components/task-list.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';

import { componentWrapperDecorator, moduleMetadata } from '@storybook/angular';

import { CommonModule } from '@angular/common';

import TaskListComponent from './task-list.component';
import TaskComponent from './task.component';

import * as TaskStories from './task.stories';

const meta: Meta<TaskListComponent> = {
  component: TaskListComponent,
  title: 'TaskList',
  tags: ['autodocs'],
  decorators: [
    moduleMetadata({
      //👇 Imports both components to allow component composition with Storybook
      declarations: [TaskListComponent, TaskComponent],
      imports: [CommonModule],
    }),
    //👇 Wraps our stories with a decorator
    componentWrapperDecorator(
      (story) => `<div style="margin: 3em">${story}</div>`
    ),
  ],
  args: {
    ...TaskStories.ActionsData,
  },
};
export default meta;
type Story = StoryObj<TaskListComponent>;

export const Default: Story = {
  args: {
    tasks: [
      { ...TaskStories.Default.args?.task, id: '1', title: 'Task 1' },
      { ...TaskStories.Default.args?.task, id: '2', title: 'Task 2' },
      { ...TaskStories.Default.args?.task, id: '3', title: 'Task 3' },
      { ...TaskStories.Default.args?.task, id: '4', title: 'Task 4' },
      { ...TaskStories.Default.args?.task, id: '5', title: 'Task 5' },
      { ...TaskStories.Default.args?.task, id: '6', title: 'Task 6' },
    ],
  },
};

export const WithPinnedTasks: Story = {
  args: {
    tasks: [
      // Shaping the stories through args composition.
      // Inherited data coming from the Default story.
      ...(Default.args?.tasks?.slice(0, 5) || []),
      { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
    ],
  },
};

export const Loading: Story = {
  args: {
    tasks: [],
    loading: true,
  },
};

export const Empty: Story = {
  args: {
    // Shaping the stories through args composition.
    // Inherited data coming from the Loading story.
    ...Loading.args,
    loading: false,
  },
};

💡装饰器是一种为故事提供任意包装器的方式。在这里,我们在默认导出上使用装饰器键为渲染的组件添加一些 margin。它们也可以用来将故事包装在“提供者”中——例如,设置某些上下文的库组件。

通过导入 TaskStories,我们能够以最小的努力在故事中组合参数(args 简称)。这样,两个组件期望的数据和操作(模拟回调)都被保留下来。

现在在 Storybook 中检查新的 TaskList 故事。

构建状态

我们的组件仍然比较粗糙,但现在我们已经大致知道要构建哪些故事。你可能认为 .list-items 包装器过于简单了。你说得没错——在大多数情况下,我们不会仅仅为了添加一个包装器而创建一个新组件。但是 TaskList 组件的真正复杂性体现在 withPinnedTasksloadingempty 这些边缘情况中。

复制
src/app/components/task-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';

@Component({
  selector: 'app-task-list',
  standalone: false,
+ template: `
+   <div class="list-items">
+     <app-task
+       *ngFor="let task of tasksInOrder"
+       [task]="task"
+       (onArchiveTask)="onArchiveTask.emit($event)"
+       (onPinTask)="onPinTask.emit($event)"
+     >
+     </app-task>
+     <div
+       *ngIf="tasksInOrder.length === 0 && !loading"
+       class="wrapper-message"
+     >
+       <span class="icon-check"></span>
+       <p class="title-message">You have no tasks</p>
+       <p class="subtitle-message">Sit back and relax</p>
+     </div>
+     <div *ngIf="loading">
+       <div *ngFor="let i of [1, 2, 3, 4, 5, 6]" class="loading-item">
+         <span class="glow-checkbox"></span>
+         <span class="glow-text">
+           <span>Loading</span> <span>cool</span> <span>state</span>
+         </span>
+       </div>
+     </div>
+   </div>
  `,
})
export default class TaskListComponent {
- /** The list of tasks */
- @Input() tasks: Task[] = [];

+  /**
+  * @ignore
+  * Component property to define ordering of tasks
+  */
+ tasksInOrder: Task[] = [];

  @Input() loading = false;

  // tslint:disable-next-line: no-output-on-prefix
  @Output() onPinTask: EventEmitter<any> = new EventEmitter();

  // tslint:disable-next-line: no-output-on-prefix
  @Output() onArchiveTask: EventEmitter<any> = new EventEmitter();

+ @Input()
+ set tasks(arr: Task[]) {
+   const initialTasks = [
+     ...arr.filter(t => t.state === 'TASK_PINNED'),
+     ...arr.filter(t => t.state !== 'TASK_PINNED'),
+   ];
+   const filteredTasks = initialTasks.filter(
+     t => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'
+   );
+   this.tasksInOrder = filteredTasks.filter(
+     t => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'
+   );
+ }
}

添加的标记产生了以下 UI

注意列表中置顶项的位置。我们希望置顶项渲染在列表顶部,以便让用户优先看到。

数据要求

随着组件的增长,输入要求也会随之增加。使用 TypeScript 定义 TaskList 组件的数据要求。由于 Task 是子组件,请确保提供正确形状的数据来渲染它。为了节省时间和麻烦,请重用您之前在 task.model.ts 中定义的模型。

💡 别忘了用 git 提交你的更改!
使你的代码与本章同步。在 GitHub 上查看 cddf606。
这本免费指南对你有帮助吗?发推文点赞并帮助其他开发者找到它。
下一章
数据
学习如何将数据连接到你的 UI 组件
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,975开发者人数持续增长
原因Storybook 的原因组件驱动型 UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI