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

cover image


回到 手把手開始寫 Svelte


前言

繼上一篇事件處理後,終於到組件(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)} />

可以試試看把 Inneron: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 這個 nameslot,然後來做一些處理,例如:

<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 的文字。




最後更新時間: 2023年04月05日.