今天來介紹 Svelte 中怎麼寫一個 component,在前幾天的文章中有提過每一個 .svelte 檔就是一個 component ,而那個 Svelte 會自動 default export 整個 component 我們要使用時 import 它的 default export 即可。
像是我這邊封裝一個 Input
component
<!-- in Input.svelte -->
<script lang="ts">
let value = $state('');
let label = 'name';
</script>
<label>
<span>{label}</span>
<input bind:value />
</label>
<style>
input {
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.5rem;
background-color: var(--background);
color: var(--text);
}
label {
display: flex;
flex-direction: column;
color: var(--text);
}
label span {
display: block;
margin-bottom: 0.5rem;
color: var(--text);
}
</style>
然後我在其他地方要使用時只要 import Input from './Input.svelte';
即可
<script>
import Input from './Input.svelte';
</script>
<div class="inputContainer">
<Input label="name" {value} />
</div>
<style>
:root {
--background: #fff;
--text: #000;
--border: #ccc;
--body-background: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #333;
--text: #fff;
--border: #555;
--body-background: #222;
}
}
:global(body) {
background-color: var(--body-background);
margin: 0;
padding: 0;
}
.inputContainer {
width: 40%;
}
</style>
那既然是 component 勢必有時候我們會需要從外部控制它的行為或者樣式等等的東西,這時候我們可以用 $props
這個 rune 來達成這件事情。
像是如果想要讓 label
變成外部控制的話就能這麼寫
<!-- in Input.svelte -->
<script lang="ts">
let { label }: { label: string } = $props();
let value = $state('');
</script>
然後要傳入 props 時只要這樣即可
<Input label="name" />
當然 props 也可以有預設值
let { label='name' }: { label: string } = $props();
也可以選擇不解構
let label: { label: string } = $props();
那當然也可以使用 spread
let { ...restProps } : { label: string } = $props();
那 value
呢? 直覺上我們會想這麼做
<!-- in Input.svelte -->
<script lang="ts">
let { label, value }: { label: string; value: string } = $props();
$effect(() => {
console.log('[Input]', value);
});
</script>
<label>
<span>{label}</span>
<input bind:value />
</label>
然後讓 value
從外部傳入
<script>
import Input from './Input.svelte';
let value = $state('');
</script>
<Input label="name" {value} />
看起來沒什麼問題,但當我們加上 $effect
來看一下狀態更新的情況就會發現有點怪怪的,只有 Input
裡面的 value
有被更新,但我們外面的 state 沒有跟著被更新。
沒錯這裡也需要使用 bind:value
讓外部的 state 可以一起被更新,但當我們這麼寫的時候會發現網站 crash 了
<div class="inputContainer">
<Input label="name" bind:value />
</div>
這時打開 console 會發現其實 Svelte 已經告訴我們答案了
為了解決這個問題我們會需要 $bindable
這個 rune,它是可以讓 component 的 props 可以跟外部的 state 去做 binding 。
let { label = 'name', value = $bindable() }: { label: string; value: string } = $props();
會發現效果已經如我們預期了!
跟一般的 HTML tag 一樣我們也能對 Svelte component 進行 binding,首先在 Input.svelte 裡新增一個 function
並 export
出去
<!-- in Input.svelte -->
<script lang="ts">
let { label = 'name', value = $bindable() }: { label: string; value: string } = $props();
$effect(() => {
console.log('\x1b[34m%s\x1b[0m', '[Input.svelte]', value);
});
export const CONSTANT = 'CONSTANT';
export const greet = () => {
alert(`Hello, ${value}`);
};
export { greet };
</script>
請不要使用
export let
,那是 Svelte 4 中用來描述 props 的特殊語法
然後就跟對 HTML tag binding 一樣用一個 $state
並傳入到 <Input />
的 bind:this
裡
<script lang="ts">
import { onMount } from 'svelte';
import Input from './Input.svelte';
let value = $state('');
let inputElement: Input | undefined = $state(undefined);
onMount(() => {
console.log('\x1b[33m%s\x1b[0m', '[onMount]', $state.snapshot(inputElement));
});
$effect(() => {
console.log('\x1b[32m%s\x1b[0m', '[+page.svelte]', value);
});
</script>
<div class="inputContainer">
<Input bind:this={inputElement} label="name" bind:value />
</div>
<button
onclick={() => {
inputElement?.greet();
}}
>
Input.greet</button
>
當我們拿到 Input 的 instance 時我們就可以使用它所 export
的 const
variable ,所以我們就能做到在 parent 層使用 child 層所封裝的 function 或 constant 。
與 v4 相比 v5 對於 props 的寫法好懂很多
宣告 props
// 宣告 props
//v5
let { lable = ''} = $props()
// v4
export let lable = ''
//取得所有 props
//v5
let lable = $props()
//v4
$$props
但最大的不同是 event 這件事情在 v4 會建議使用 createEventDispatcher 來進行事件處理的封裝。
<!-- Inner.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
<script>
import Inner from './Inner.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage} />
簡單外部的 component 透過用 on:
來監聽內部 component 是否有 dispatch
對應的 event。以這個例子來說裡面 dispatch
名稱為 message
的 event ,並帶了 {text: 'Hello!'}
所以我外部使用的時候就可以從 function 中的 event
取出來。
但如果是 v5 會建議使用 function 的 props 來達成這件事情
<!-- Inner.svelte -->
<script lang='ts'>
let { sayHello } = $props()
</script>
<button onclick={()=>{
sayHello({text:'hello'})
}}>
Click to say hello
</button>
<script>
import Inner from './Inner.svelte';
function handleMessage(args) {
alert(args.text);
}
</script>
<Inner sayHello={handleMessage} />
當然在 v4 中也可以將 function 做為 props 傳入 component 中,但會變成這樣子
<!-- Inner.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let onclick
</script>
<button on:click={onclick}>
Click to say hello
</button>
<script>
import Inner from './Inner.svelte';
function onclick() {
console.log('clicked')
}
</script>
<Inner onclick={onclick} />
因為 Svelte 4都是使用 on:
來監聽事件而不是使用 onclick
之類的 HTML 的 attribute 來控制,所以如果直接使用上面的形式會變成一下子用 on:click
一下子用 onclick
的感覺。
以及 <slot />
在 v5 後 deprecated 了,之後會建議使用 {#snippet}
這個特殊語法,只是它不只可以用在 component 的 children 的用途所以未來有用到再來說明吧。
所以我自己的感覺是在 component 撰寫這件事情來說 v5 比 v4 更為貼近在寫 JS 而不是根據 Svelte 自己的規則在寫,這點對於從 React 來的開發者友善許多 XDD 。
https://svelte-5-preview.vercel.app/docs/old-vs-new#passing-ui-content-to-a-component
https://svelte-5-preview.vercel.app/docs/old-vs-new#passing-ui-content-to-a-component
https://github.com/toddLiao469469/30days-for-svelte5/tree/main/src/routes/day08