组装复合组件
在上一章节,我们构建了第一个组件;本章将扩展我们所学的知识,制作 TaskList(任务列表),即任务的列表。让我们将组件组合在一起,看看引入更多复杂性时会发生什么。
Tasklist
Taskbox 通过将置顶任务放置在默认任务之上来突出显示它们。它产生了您需要为其创建故事的 TaskList
的两个变体:默认项目和置顶项目。
由于 Task
数据可以异步发送,我们也需要一个加载状态,以便在没有连接时渲染。此外,当没有任务时,我们需要一个空状态。
准备设置
复合组件与其包含的基本组件没有太大区别。创建一个 TaskList
组件和一个配套的故事文件:src/components/TaskList.vue
和 src/components/TaskList.stories.js
。
从 TaskList
的粗略实现开始。您需要从前面导入 Task
组件,并将属性作为输入传入。
<template>
<div class="list-items">
<template v-if="loading"> loading </template>
<template v-else-if="isEmpty"> empty </template>
<template v-else>
<Task
v-for="task in tasks"
:key="task.id"
:task="task"
@archive-task="onArchiveTask"
@pin-task="onPinTask"
/>
</template>
</div>
</template>
<script>
import Task from './Task.vue';
import { reactive, computed } from 'vue';
export default {
name: 'TaskList',
components: { Task },
props: {
tasks: { type: Array, required: true, default: () => [] },
loading: { type: Boolean, default: false },
},
emits: ['archive-task', 'pin-task'],
setup(props, { emit }) {
props = reactive(props);
return {
isEmpty: computed(() => props.tasks.length === 0),
/**
* Event handler for archiving tasks
*/
onArchiveTask(taskId) {
emit('archive-task', taskId);
},
/**
* Event handler for pinning tasks
*/
onPinTask(taskId) {
emit('pin-task', taskId);
},
};
},
};
</script>
接下来,在故事文件中创建 Tasklist
的测试状态。
import TaskList from './TaskList.vue';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
tags: ['autodocs'],
decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
args: {
...TaskStories.ActionsData,
}
}
export const Default = {
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in Task.stories.js.
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 = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Default story.
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
},
};
export const Loading = {
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
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
组件的真正复杂性在 withPinnedTasks
、loading
和 empty
这些边缘情况下显现出来。
<template>
<div class="list-items">
<template v-if="loading">
+ <div v-for="n in 6" :key="n" class="loading-item">
+ <span class="glow-checkbox" />
+ <span class="glow-text">
+ <span>Loading</span> <span>cool</span> <span>state</span>
+ </span>
+ </div>
</template>
<div v-else-if="isEmpty" class="list-items">
+ <div class="wrapper-message">
+ <span class="icon-check" />
+ <p class="title-message">You have no tasks</p>
+ <p class="subtitle-message">Sit back and relax</p>
+ </div>
</div>
<template v-else>
+ <Task
+ v-for="task in tasksInOrder"
+ :key="task.id"
+ :task="task"
+ @archive-task="onArchiveTask"
+ @pin-task="onPinTask"
+ />
</template>
</div>
</template>
<script>
import Task from './Task.vue';
import { reactive, computed } from 'vue';
export default {
name: 'TaskList',
components: { Task },
props: {
tasks: { type: Array, required: true, default: () => [] },
loading: { type: Boolean, default: false },
},
emits: ['archive-task', 'pin-task'],
setup(props, { emit }) {
props = reactive(props);
return {
isEmpty: computed(() => props.tasks.length === 0),
+ tasksInOrder:computed(()=>{
+ return [
+ ...props.tasks.filter(t => t.state === 'TASK_PINNED'),
+ ...props.tasks.filter(t => t.state !== 'TASK_PINNED'),
+ ]
+ }),
/**
* Event handler for archiving tasks
*/
onArchiveTask(taskId) {
emit('archive-task',taskId);
},
/**
* Event handler for pinning tasks
*/
onPinTask(taskId) {
emit('pin-task', taskId);
},
};
},
};
</script>
添加的标记会产生以下 UI
请注意列表中置顶项目的位置。我们希望置顶项目在列表顶部渲染,使其成为我们用户的首要任务。