今天來繼續完善 Todo list
首先我們能運用在 Day 08 時提到的方法將我們要 reuse 的部分另外寫成一個 .svelte 檔
<script lang="ts">
interface InputProps {
label: string;
value: string;
}
let { label, value = $bindable() }: InputProps = $props();
</script>
<label class="form-control w-full">
<div class="label">
<span class="label-text text-primary">{label}</span>
</div>
<input type="text" class="input input-bordered w-full" bind:value />
</label>
<Input label="Title" bind:value={title} />
<Input label="Content" bind:value={content} />
但如果我就是不想每個 component 都抽成 .svelte 檔,我只是想在這個 +page.svelte
裡 reuse 部分的 markup 而已那該怎麼做呢?這時可以使用 {#snippet}
以及 {@render}
。
{#snippet}
就是可以讓 markup 片段可以被 reuse 的寫法,而 {@render}
就是負責渲染 {#snippet}
。
先把 Todo 卡片來寫成一個 snippet
{#snippet card(todo: Todo)}
<div class="card shadow bg-base-200 mb-4">
<div class="card-body">
<div class="flex justify-between items-center">
<h2 class="card-title">{todo.title}</h2>
<input type="checkbox" bind:checked={todo.done} class="checkbox checkbox-lg" />
</div>
<p>{todo.content}</p>
</div>
</div>
{/snippet}
{#snippet card(todo:Todo)}
的意思就是我宣告了一個名稱為 card
的 snippet 且他可以傳入一個 type 為 Todo
的參數。
而我們原本卡片的部分就可以改寫成這樣
{#each todoList as todo (todo.id)}
{@render card(todo)}
{/each}
除了能夠 reuse markup 以外我覺得另外一個好處是它能提升程式碼的可讀性,試想一下如果我這邊不只一層{#each}
或甚至還有 {#if}
再加上原本的 HTML tag 這些加在一起就讓程式碼變得不太好閱讀了吧。
那如果我的 props 是想要有其他 component 呢?或者該說 markup 的結構呢? 沒錯還是用 {#snippet}
,那就先來把原本 page 中最外層的那個 div
抽出去做為一個獨立的 component 。
<script lang="ts">
import { type Snippet } from 'svelte';
interface PageLayoutProps {
children: Snippet;
}
let { children }: PageLayoutProps = $props();
</script>
<div class="max-w-3xl mx-auto">
{@render children()}
</div>
<PageLayout>
<div class="grid grid-cols-2 mb-8 gap-x-8">
<Input label="Title" bind:value={title} />
<Input label="Content" bind:value={content} />
</div>
<button class="btn btn-primary" onclick={addTodo}> Add Todo </button>
<!-- 省略其餘部分 -->
</PageLayout>
而這裡的 children
是一個特殊的naming ,只要是「在 component 裡的 markup 片段會自動成為該 component 的 children
props 的值」,所以這段 <PageLayout></PageLayout>
裡的程式碼才能被 {@render children()}
吃到。
所以其實他的意思就跟這個是一樣的
<PageLayout>
{#snippet children()}
<div class="grid grid-cols-2 mb-8 gap-x-8">
<Input label="Title" bind:value={title} />
<Input label="Content" bind:value={content} />
</div>
<button class="btn btn-primary" onclick={addTodo}> Add Todo </button>
<!-- 省略其餘部分 -->
{/snippet}
</PageLayout>
現在來實作勾選 Todo 卡片右上角的 checkbox 後可以被標註成完成狀態,那這邊就可以使用 Day 07 時提到 class:
{#snippet card(todo: Todo)}
<div class="card shadow bg-base-200 mb-4">
<div class="card-body">
<div class="flex justify-between items-center">
<h2 class:line-through={todo.done} class="card-title">{todo.title}</h2>
<input type="checkbox" bind:checked={todo.done} class="checkbox checkbox-lg" />
</div>
<p class:text-gray-700={todo.done}>{todo.content}</p>
</div>
</div>
{/snippet}
使用了 class:line-through={todo.done}
、class:text-gray-700={todo.done}
這兩個directives 後,我們就能在當 todo.done === true
時加上這兩個 class 了。
最後再補上刪除卡片的功能就是用 array.filter()
來實現刪除特定 id
的 todo
,然後因為 array.filter()
是不會更改到原本的 array 的,所以我還是得使用 immutable update 的方式去更新原本的 todoList
。
<script lang="ts">
// 省略其餘部分
const removeTodo = (id: number) => {
todoList = todoList.filter((todo) => todo.id !== id);
};
</script>
<!-- 省略其餘部分 -->
{#snippet card(todo: Todo)}
<div class="card shadow bg-base-200 mb-4">
<div class="card-body">
<div class="flex justify-between items-center">
<h2 class:line-through={todo.done} class="card-title">{todo.title}</h2>
<input type="checkbox" bind:checked={todo.done} class="checkbox checkbox-lg" />
</div>
<p class:text-gray-700={todo.done}>{todo.content}</p>
<button class="ml-auto w-20 btn btn-error" onclick={() => removeTodo(todo.id)}>
Remove
</button>
</div>
</div>
{/snippet}
至此算是把一個最簡單的 Todo List 完成了,會發現 Svelte 在寫這種小功能非常簡單、快速,特別是在狀態更新和動態樣式的寫法相當直覺。
https://github.com/toddLiao469469/30days-for-svelte5/tree/day10/src/routes/todo-list