Svelte 組件(Component)基礎
tags: Svelte
category: Front-End
description: Svelte 組件(Component)基礎
created_at: 2023/04/04 16:00:00
updated_at: 2023/04/05 16:00:00
前言
繼上一篇事件處理後,終於到組件(Component
)了,可以做更多花樣(?)
至於為什麼要抽出組件,可以去參考我之前寫的 Vue3
的說明,除了可以抽離共用元件讓多個地方使用,讓未來修改只要修改一個地方以外,甚至你還可以包裝成套件讓大家去使用 npm
安裝下來並使用。
就算你不讓大家用,也會幫到未來的你自己
定義組件
在 Svelte
定義組件非常簡單,只要建立一個檔案,副檔名是 .svelte
,然後依照慣例檔名採用大寫開頭(如: Child.svelte
)。
雖然你檔名要用小寫也不是不行,但就是讓未來大家方便辨識,
不過在使用上一定要使用大寫 tag
,這樣 Svelte
才會幫你編譯出來,才有辦法做使用,這樣的好處是能夠快速分辨什麼是 HTML
內建的標籤,以及你定義的組件。
舉例來說
Good
<script>
import Child from '../components/Child.svelte';
</script>
<Child />
Bad 但可使用
<script>
import LowerCase from '../components/lower-case.svelte';
</script>
<LowerCase />
Bad 無法使用
<script>
import lowercase from '../components/lower-case.svelte';
</script>
<lowercase />
總之養成好習慣,自己定義的就用大寫開頭。
傳遞屬性 (Props)
組件可以根據你傳遞給他不同的 props
做一些不同的變化。
假設我有一個 Child.svelte
如下,然後我要暴露出一個屬性,名字叫做 num
。
<script lang="ts">
export let num: number;
</script>
<div>Hi Child, num = {num}</div>
在使用他就可以像這樣傳遞 num
給他
<script lang="ts">
import Child from '../components/Child.svelte';
</script>
<Child num={1} />
<Child num={2} />
<Child num={3} />
你的畫面上就會看到有三行字
Hi Child, num = 1
Hi Child, num = 2
Hi Child, num = 3
屬性的預設值
像上面的範例,num
並沒有預設值,如果希望給他預設值也很簡單
export let num = -1;
一次傳入多個屬性
有些時候,可能有組件需要有多個傳入屬性,如下
<script lang="ts">
export let a: number;
export let b: number;
export let operator: string;
</script>
{#if operator === '+'}
{a + b}
{:else if operator === '-'}
{a - b}
{:else if operator === '*'}
{a * b}
{:else if operator === '/'}
{a / b}
{/if}
然後你需要這樣使用他
<script lang="ts">
import Child from '../components/Child.svelte';
let props = {
a: 1,
b: 2,
operator: '+'
};
</script>
<Child a={props.a} b={props.b} operator={props.operator} />
那個 a={props.a} b={props.b} operator={props.operator}
覺得很冗長,就好比你想簡寫下面這一段 JavaScript
一樣
const props = {
a: 1,
b: 2,
operator: '+'
}
const a = props.a
const b = props.b
const operator = props.operator
console.log(a, operator, b)
你總會想把它改成
const {a, operator, b} = props
對吧?
所以也有提供一次帶入多個的功能
<Child {...props} />
組件的事件
這是補充上一篇事件處理沒提到的部分
不過因為其實都是基於原來的 HTML
事件去做處理的,所以如果不熟可以回去看上一篇的內容。
上一篇提到,事件的語法其實就是如下: on:<event>|<modifier?>={<handler>}
當我們有自訂的組件之後,那個 event
可以自行定義,如下範例
<Child on:hello={() => console.log('child say hello')} />
hello
事件? 怎麼都沒聽說過,那是因為是由你的組件觸發的,如下範例
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const dispatchHelloEvent = () => {
dispatch('hello');
};
</script>
<button on:click={dispatchHelloEvent}>dispatch hello event</button>
這時只要點下按鈕,他就會觸發 hello
事件,如果使用這個組件的地方有用 on:hello
把他接起來,就會開始做事。
可以觸發,當然也可以傳遞值再做處理,這樣才能做更多變化。
Child.svelte
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const dispatchHelloEvent = () => {
dispatch('hello', {
message: 'hello from child component'
});
};
</script>
<button on:click={dispatchHelloEvent}>dispatch hello event</button>
然後像下面這樣使用他
<script lang="ts">
import Child from '../components/Child.svelte';
const onHello = (event: CustomEvent) => {
console.log(event.detail.message);
};
</script>
<Child on:hello={onHello} />
傳遞的資料會被放在 event.detail
下面。
是怎麼做到的?
沒有什麼神奇的魔法,原生的 JavaScript
就有提供這樣的功能,如下:
<button>Custom Event Button</button>
<script>
const button = document.querySelector('button');
button.addEventListener('click', () => {
const event = new CustomEvent('customEvent', {
detail: {
message: 'Hello World',
},
});
button.dispatchEvent(event);
});
button.addEventListener('customEvent', (event) => {
console.log(event.detail.message);
});
</script>
事件 forward
組件的事件不會冒泡(向上傳遞),所以需要手動做一個 forward
的行為,在中間加入要 forward
的事件。
Inner.svelte
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const dispatchHelloEvent = () => {
dispatch('hello', {
message: 'hello from child component'
});
};
</script>
<button on:click={dispatchHelloEvent}>Inner</button>
Outer.svelte
<script>
import Inner from './Inner.svelte';
</script>
<!-- 這裡的 on:hello 就是要 forward 的事件 -->
<Inner on:hello/>
然後這樣使用他
<script lang="ts">
import Outer from '../components/Outer.svelte';
</script>
<Outer on:hello={(e) => console.log(e)} />
可以試試看把 Inner
的 on:hello
拿掉,這樣就沒辦法在外層觸發了。
原生事件的 forward
如同前面提到的,組件的事件並不會冒泡,所以你要自己處理,就算是監聽原生的事件也一樣。
也就是說你還可以做到這件事
Child.svelte
<button>1</button>
<button on:click>2</button>
<button>3</button>
可以像下面這樣使用,那只有按鈕2被 click
,就會觸發。
<script lang="ts">
import Child from '../components/Child.svelte';
</script>
<Child on:click={() => console.log('click child')} />
當然你也可以對多個 DOM
加上 forward
。
Slot 插槽
前面所使用的組件都是在屬性(props
)做一些花樣,如果要包在標籤(tag
)之間,就需要 slot
這個東西
舉例來說,我希望 Child
組件可以吃到那個 Hello
,就可以這樣寫
<script lang="ts">
import Child from '../components/Child.svelte';
</script>
<Child>Hello</Child>
然後 Child.svelte
的定義也很簡單
<div>
Child:
<slot />
</div>
畫面上就會看到 Child: Hello
的文字。
也可以包多個東西(children
)在中間,如下:
<Child>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</Child>
總之就是組件中間的東西,就會出現在 <slot />
當中。
Slot 的預設內容
預設內容也很簡單,直接塞在 <slot></slot>
當中就可以,例如:
<div>
Child:
<slot>Default</slot>
</div>
然後像這樣使用他
<Child>Hello</Child>
<Child />
畫面上就會呈現兩行:
Child: Hello
Child: Default
有名字的 Slot
有時候可能希望有多個插槽,就可以幫他取名字來用,像下面這樣
<div>
<slot name="first">first</slot>
<hr />
<slot name="second">second</slot>
</div>
然後像這樣使用他 (slot
屬性)
<Child>
<div slot="first">First content</div>
<div slot="second">Second content</div>
</Child>
畫面上就會出現兩行字並以水平線(hr
)分隔。
檢查是否有帶入 Slot
可以使用 $$slots
物件,他是一個 { string: boolean }
,的物件,如果父層有帶 slot
下來就會是 true
,否則就沒有東西。
舉例來說,在 Child.svelte
寫這一行 $: console.log($$slots);
,你就會觀察到一些東西,如上例,會輸出:
{second: true, first: true}
換句話說你就可以用 $$slots.first
來檢查父層有沒有帶入 first
這個 name
的 slot
,然後來做一些處理,例如:
<div class={$$slots.content ? 'bg-blue-500 text-white' : ''}>
<slot name="content">No Content.</slot>
</div>
如果父層有帶入 content
,就會帶有 bg-blue-500 text-white
這個 class
,否則將顯示預設內容 No Content.
,且沒帶有 class
。
Slot 也有 Props
這個是最後一個,也是 Slot
當中最複雜的一個。
前面都很直覺,但 Props
要稍微轉一下
這個主要是我在父層,我希望拿到子組件的變數,可以利用這個方式:
Child.svelte
: 一個簡單的 counter
<script lang="ts">
let count = 0;
</script>
<div>
<slot {count}>{count}</slot>
<button on:click={() => count++}>count++</button>
</div>
然後可以像這樣使用
<script lang="ts">
import Child from '../components/Child.svelte';
</script>
<Child let:count>
count: {count}
</Child>
那個 let:count
,就可以拿到子組件的 count
變數,如果你想改名,可以改成:
<Child let:count={value}>
count: {value}
</Child>
上面這個是預設的 Slot
的做法,也可以幫帶有名字的 Slot
做這件事,不過 let
綁定的位置就不同了。
Child.svelte
<script lang="ts">
let firstContent = 'First';
let secondContent = 'Second';
</script>
<div>
<slot name="first-content" {firstContent} />
<hr />
<slot name="second-content" {secondContent} />
</div>
然後像這樣使用他們
<Child>
<div slot="first-content" let:firstContent>first: {firstContent}</div>
<div slot="second-content" let:secondContent>second: {secondContent}</div>
</Child>
通常可能用於上層要根據下層的一些狀態去做一些的改變(例如樣式或其他),例如:
<script lang="ts">
let hovering = false;
</script>
<div on:mouseenter={() => (hovering = true)} on:mouseleave={() => (hovering = false)}>
Child
<slot {hovering} />
</div>
當滑鼠碰到 Child
組件時,hovering
會變成 true
,這時上層就可以根據 hovering
做一些事
<Child let:hovering>
{#if hovering}
<p>Hovering</p>
{/if}
</Child>
例如上面這樣當 Child
被滑過就會顯示一行 Hovering
的文字。