组装一个复合组件
上一章,我们构建了第一个组件;本章将扩展我们所学,构建 TaskList,一个任务列表。让我们把组件组合起来,看看引入更多复杂度时会发生什么。
Tasklist
Taskbox 通过将置顶任务放置在默认任务上方来强调它们。它产生了两种需要为其创建故事的 TaskList
变体:默认和置顶项。
由于 Task
数据可以异步发送,因此在没有连接时,我们也需要一个加载状态来渲染。此外,当没有任务时,我们需要一个空状态。
进行设置
复合组件与其包含的基本组件没有太大区别。创建一个 TaskList
组件、一个辅助组件来帮助我们显示正确的标记,以及一个配套的故事文件:src/components/TaskList.svelte
、src/components/MarginDecorator.svelte
和 src/components/TaskList.stories.js
。
从 TaskList
的粗略实现开始。你需要从之前导入 Task
组件,并传入属性和动作作为输入。
<script>
import Task from './Task.svelte';
/* Sets the loading state */
export let loading = false;
/* Defines a list of tasks */
export let tasks = [];
/* Reactive declaration (computed prop in other frameworks) */
$: noTasks = tasks.length === 0;
$: emptyTasks = noTasks && !loading;
</script>
{#if loading}
<div class="list-items">loading</div>
{/if}
{#if emptyTasks}
<div class="list-items">empty</div>
{/if}
{#each tasks as task}
<Task {task} on:onPinTask on:onArchiveTask />
{/each}
接下来,创建包含以下内容的 MarginDecorator
<div>
<slot />
</div>
<style>
div {
margin: 3em;
}
</style>
最后,在故事文件中创建 Tasklist
的测试状态。
import TaskList from './TaskList.svelte';
import MarginDecorator from './MarginDecorator.svelte';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
tags: ['autodocs'],
//👇 The auxiliary component will be added as a decorator to help show the UI correctly
decorators: [() => MarginDecorator],
render: (args) => ({
Component: TaskList,
props: args,
on: {
...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,
},
};
装饰器 (Decorators) 是一种为故事提供任意包装器的方式。在本例中,我们在默认导出上使用装饰器 key
来为渲染的组件添加样式。它们也可以用来为组件添加其他上下文。
通过导入 TaskStories
,我们能够轻松地组合 (compose)故事中的参数 (args)。这样,两个组件所需的数据和动作 (模拟回调) 都得到了保留。
现在在 Storybook 中查看新的 TaskList
故事。
构建状态
我们的组件仍然粗糙,但现在我们对要构建的故事有了想法。你可能认为 .list-items
包装器过于简单。你说得对——在大多数情况下,我们不会仅仅为了添加一个包装器而创建一个新组件。但 TaskList
组件的真正复杂性体现在边缘情况,即 withPinnedTasks
、loading
和 empty
。
对于加载边缘情况,我们将创建一个新组件来显示正确的标记。
创建一个名为 LoadingRow.svelte
的新文件,并在其中添加以下标记
<div class="loading-item">
<span class="glow-checkbox" />
<span class="glow-text">
<span>Loading</span>
<span>cool</span>
<span>state</span>
</span>
</div>
并将 TaskList.svelte
更新为以下内容
<script>
import Task from './Task.svelte';
import LoadingRow from './LoadingRow.svelte';
/* Sets the loading state */
export let loading = false;
/* Defines a list of tasks */
export let tasks = [];
/* Reactive declaration (computed prop in other frameworks) */
$: noTasks = tasks.length === 0;
$: emptyTasks = noTasks && !loading;
$: tasksInOrder = [
...tasks.filter((t) => t.state === 'TASK_PINNED'),
...tasks.filter((t) => t.state !== 'TASK_PINNED')
];
</script>
{#if loading}
<div class="list-items">
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
<LoadingRow />
</div>
{/if}
{#if emptyTasks}
<div 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>
{/if}
{#each tasksInOrder as task}
<Task {task} on:onPinTask on:onArchiveTask />
{/each}
添加的标记会产生以下 UI
注意列表中置顶项的位置。我们希望置顶项在列表顶部渲染,以便让用户优先看到它。