📚

Svelte 初心者用チートシート

2022/03/23に公開

概要

  • 自分用に整理しただけ
  • 基本的には公式ドキュメントのチュートリアルから抜粋
  • 普段はVueを書いていて Svelte はド素人の状態で作成したので内容に誤りがあるかも
  • TypeScript, SvelteKit への言及ナシ

開発環境構築

多分 Vite が一番早い

yarn create vite my-svelte-app --template svelte
cd my-svelte-app/
yarn install
yarn dev

localhost:3000 でもう動いてる

vscode なら Svelte 用の拡張を入れておけば、シンタックスハイライトやコードスニペットが機能する

https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode

コンポーネントの基本構造

Vue の SFC 同様に、スクリプト、テンプレート、スタイルを一つのファイル (.svelte) に記述する。

<script>
  const name = 'Svelte'
</script>

<main>
  <h1>Hello, {name}</h1>
</main>

<style>
  h1 {
    color: gray;
  }
</style>

要素のプロパティへのバインド

{} でデータバインディングできる

<script>
  const src = "https://storage.googleapis.com/zenn-user-upload/avatar/845fa75ba8.jpeg"
</script>

<main>
  <img src={src}>
</main>

プロパティ名とバインドする変数名が一致してる場合は省略可能

<main>
  <img {src}>
</main>

スタイリング

style ブロックに記述した CSS はデフォルトでコンポーネントローカルとなり、タグセレクタを使用しても他のコンポーネントに干渉することはない

<p>This is a paragraph.</p>

<style>
  p {
    color: purple;
    font-family: "Comic Sans MS", cursive;
    font-size: 2em;
  }
</style>

外部コンポーネントの利用

import するだけですぐに使用可能に

<script>
  import Child from "./components/Child.svelte";
</script>

<Child />

サニタイズせずにテキスト出力

XSS には気をつけよう

<script>
  const html = '<h1>Hello, World!</h1>'
</script>

<div>{@html html}</div>

DOMイベントハンドリング

<script>
  let count = 0
  function increment() {
    count += 1
  }
</script>

<button on:click={increment}>
  {count}
</button>

インラインでも可

<div on:mousemove={e => m = { x: e.clientX, y: e.clientY }}>
	The mouse position is {m.x} x {m.y}
</div>

モディファイア (once, preventDefault, stopPropagation など) は | で設定可能

<button on:click|once|capture={handleClick}>
	Click me
</button>

Reactive Declarations

日本語でなんて呼ぶのかわからないけど、 Vue の computed みたいなもの

let count = 0
$: doubled = count * 2

ブロック展開して複数行も書ける

$: {
  console.log('count changed!!')
  console.log(`the count is ${count}`)
}

なんなら条件式も書けちゃう

$: if (count > 10) {
  alert('count is over 10')
}

配列やオブジェクトの更新方法

チートシートというか Tips だけど、 Vue (v2) と同様に、配列・オブジェクトの操作ではリアクティブが反映されないことがある。

Array#push だとリアクティブにならないが、

<script>
  let array = [1,2,3]
  function push() {
    array.push(array[array.length - 1] + 1) // ← NG
  }
</script>

<button on:click={push}>
  {array.join(',')}
</button>

Svelte だと代入がリアクティブのトリガーになるので、明示的な代入をすればOK

<script>
  let array = [1,2,3]
  function push() {
    array = [...array, array[array.length - 1] + 1] // ← OK
  }
</script>

<button on:click={push}>
  {array.join(',')}
</button>

コンポーネントの props の定義

変数を export するだけで、 props として親コンポーネントから注入できるようになる

Child.svelte
<script>
  export let value
</script>

<p>props value is {value}</p>
Parent.svelte
<Child value=1 />

当然 props へのバインディングも可能

Parent.svelte
<Child value={count} />

props 側に初期値がついていると、デフォルト値になる

Child.svelte
export let value = 1

props が複数ある場合、オブジェクトで一括指定が可能

Child.svelte
export let a
export let b
export let c
Parent.svelte
<script>
  import Child from './components/Child.svelte'
  const props = {
    a: 1,
    b: 2,
    c: 3
  }
</script>

<Child {...props} />

テンプレートでの条件分岐

Vue の v-if みたいなもの

{#if value % 3 === 0 && value % 5 === 0}
  <div>FizzBuzz</div>
{:else if value % 3 === 0}
  <div>Fizz</div>
{:else if value % 5 === 0}
  <div>Buzz</div>
{:else}
  <div>{value}</div>
{/if}

テンプレートでのループ

Vue の v-for みたいなもの

{#each values as value}
  <p>{value}</p>
{/each}

テンプレートで Promise の状態による分岐

Promise 変数の状態によってテンプレートで分岐できるため、外部データの読み込み中はメッセージを表示するなどができる

<script>
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('resolved');
    }, 1000);
  });
</script>

{#await promise}
  <p>...waiting</p>
{:then value}
  <p>{value}</p>
{:catch error}
  <p>{error.message}</p>
{/await}

コンポーネントからイベントを発火する

Vue の emit に近いやつ

Child.svelte
<script>
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()
  const message = 'Child Value'

  function sayHello() {
    dispatch('sayHello', { message })
  }
</script>

<button on:click={sayHello}>clickMe</button>
Parent.svelte
<script>
  import Child from './components/Child.svelte'
</script>

<Child on:sayHello={e => console.log(e.detail.message)}/>

カスタムイベントでなく、DOMイベントをそのまま親に伝搬する場合は省略記法が使える

<button on:click>ClickMe</button>

フォーム要素へのバインディング

input

<input bind:value={name}>

input[type=number] input[type=range] の場合は、数値に暗黙的に変換される

<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>

Checkbox

<input type=checkbox bind:checked={yes}>

Textarea

<textarea bind:value={value}></textarea>

Select

<select bind:value={selectedValue} on:change="{() => answer = ''}">

Select(multiple)

<select multiple bind:value={flavours}>

Radio(グループ)

複数のラジオをグループ化し、いずれか一つが選択された状態にする

<label>
  <input type=checkbox bind:group={selectedValues} name="option" value="one">
  One
</label>

<label>
  <input type=checkbox bind:group={selectedValues} name="option" value="two">
  Two
</label>

<label>
  <input type=checkbox bind:group={selectedValues} name="option" value="three">
  Three
</label>

Checkbox(グループ)

複数のチェックボックスをグループ化し、各チェックボックスの状態を配列で監視する

<label>
  <input
    type="checkbox"
    bind:group={selectedValues}
    name="option"
    value="one"
  />
  One
</label>

<label>
  <input
    type="checkbox"
    bind:group={selectedValues}
    name="option"
    value="two"
  />
  Two
</label>

<label>
  <input
    type="checkbox"
    bind:group={selectedValues}
    name="option"
    value="three"
  />
  Three
</label>

要素への特殊なバインディング

Readonly なプロパティへのバインディング

DOM の状態が変わった場合に変数に反映されるが、変数を書き換えても DOM の状態は変わらない

<div bind:clientWidth={w} bind:clientHeight={h}>Hello</div>
{ w } { h }

DOM に対する this コンテキストをバインドすることで、要素を直接 JavaScript から参照可能にする

<canvas
  bind:this={canvas}
  width={32}
  height={32}
></canvas>

コンポーネントライフサイクル

onMount/onDestroy は import が必要

import { onMount, onDestroy } from 'svelte'

onMount(() => console.log('Mounted'))
onDestroy(() => console.log('Destroyed'))

Vue の nextTick みたいに、次のレンダリングを待機するのには ticks を使う

import { tick } from 'svelte'
await tick();

class 名のバインディング

だいたい Vue と同じで、状態を元に class を切り替えられる

<button
	class="{current === 'foo' ? 'selected' : ''}"
	on:click="{() => current = 'foo'}"
>foo</button>

クラス名と変数名が同じ場合は省略可能

<div class:big>
	<!-- ... -->
</div>

Store

ストアは関連性のないコンポーネント間、モジュール間でステートを共有するための仕組み

writable で書込み可能なストアを生成可能

import { writable } from 'svelte/store'
export const count = writable(0)

writable インタフェースを用いた値の更新は set update で行える

function increment() {
  count.update(n => n + 1)
}

function reset() {
  count.set(0)
}

writable インタフェースは subscribe メソッドを通じて値の変更監視ができる。 subscribe メソッドのは、 unsubscribe するためのメソッドが戻り値で返ってくるので、適切に unsubscribe してメモリリークを避ける

const unsubscribe = count.subscribe(value => localValue = value)
onDestroy(() => unsubscribe())

という、ストアの購読とライフサイクルの管理が面倒すぎるので、ショートハンドが用意されている。 $ を頭に付けて参照することで、 subscribe/unsubscribe を勝手にやってくれる

$count

読み取り専用ストアは readable で作成可能

readable は第一引数に初期値、第二引数に、初めて subscribe された際に実行されるコールバック関数を設定する。そのタイミングでのみ set を通じて値の設定が可能。

また、コールバック関数の戻り値で、購読者が0になったときの後処理を記述できる

import { readable } from 'svelte/store'

const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date())
  }, 1000)

  return () => clearInterval(interval)
})

他のストアの値に依存したストアを derived で作成可能。ストアの computed みたいなものかな。

const count = writable(0)
const doubleCount = derived(count, $count => $count * 2)

subscribe メソッドさえオブジェクトに含まれていれば、それはストアとして機能するため、 set update を隠蔽したカプセル化も可能。

import { writable } from "svelte/store";

function createCount() {
  const { subscribe, set, update } = writable(0);

  return {
    subscribe,
    increment: () => update((n) => n + 1),
    decrement: () => update((n) => n - 1),
    reset: () => set(0),
  };
}

export const count = createCount();

Motion

tweened はなめらかな値の更新を行うオブジェクトを生成し、ストア同様に subscribe set メソッドで更新と購読を行う。

import { tweened } from 'svelte/motion'

const progress = tweened(0)
progress.subscribe(value => console.log(value))
progress.set(100)

0 から 100 に単純に更新するのでなく、何度も更新作業を行い少しずつ0から100に近づける。

springtweened とにてるが、より更新が頻繁に行われる場合に用いる

import { spring } from 'svelte/motion'

const progress = spring(0)
progress.subscribe(value => console.log(value))
progress.set(100)

Transitions

要素が表示されるとき、非表示になるときのアニメーションを容易に設定できる

<script>
  import { fade } from 'svelte/transition'
  let isShow = false
</script>

<label>
  <input type="checkbox" bind:checked={isShow}>
  show
</label>

{#if isShow}
  <p transition:fade>トランジションサンプル</p>
{/if}

import する transition 関数に応じて異なるトランジションを表現できるほか、パラメータの付与によって微調整も行える

{#if isShow}
  <p transition:fly="{{ y: 200, duration: 2000 }}">トランジションサンプル</p>
{/if}

表示するときと、非表示になるときで異なるトランジションを設定することも可能

<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>トランジションサンプル</p>

各トランジションライフサイクルをフックする

<p
  transition:fade
  on:introstart={() => console.log("introstart")}
  on:outrostart={() => console.log("outrostart")}
  on:introend={() => console.log("introend")}
  on:outroend={() => console.log("outroend")}
>
  トランジションサンプル
</p>

要素が更新されるたびにトランジションを再実行したい場合は {#key} ブロックを使用する

{#key value}
  <div transition:fade>{value}</div>
{/key}

Actions

アクションは、要素単位のライフサイクルを定義するために使用できる。

actions/outerClickable
export function outerClickable(node) {
  const handleClick = (event) => {
    if (!node.contains(event.target)) {
      node.dispatchEvent(new CustomEvent("outclick"));
    }
  };

  document.addEventListener("click", handleClick, true);

  return {
    destroy() {
      document.removeEventListener("click", handleClick, true);
    },
  };
}

要素が生成されたタイミングと、破棄されたタイミングの処理を定義できるため、DOMイベントの安全な管理に活用できる。

App.svelte
<script>
  import { outerClickable } from './actions/outerClickable'
</script>

<div use:outerClickable on:outclick={() => alert('outclick!')}>要素の外側をクリックするとアラートが鳴るよ</div>

アクションにはパラメータの指定も可能

actions/longpress.js
export function longpress(node, duration) {
}
App.svelte
<button use:longpress={duration}>Push</button>

Slot

だいたい Vue ないし Web Components と一緒

<div class="box">
  <slot></slot>
</div>

Slot に何も注入されなかった倍のフォールバックの指定

<div class="box">
  <slot>
    <em>no content was provided</em>
  </slot>
</div>

名前付きスロット

ContentCard.svelte
<article class="contact-card">
  <h2>
    <slot name="name">
      <span class="missing">Unknown name</span>
    </slot>
  </h2>

  <div class="address">
    <slot name="address">
      <span class="missing">Unknown address</span>
    </slot>
  </div>

  <div class="email">
    <slot name="email">
      <span class="missing">Unknown email</span>
    </slot>
  </div>
</article>
App.svelte
<ContactCard>
  <span slot="name"> P. Sherman </span>

  <span slot="address">
    42 Wallaby Way<br />
    Sydney
  </span>
</ContactCard>

注入されたスロットへのアクセス

{#if $$slots.comments}
	<div class="discussion">
		<h3>Comments</h3>
		<slot name="comments"></slot>
	</div>
{/if}

スロットへのプロパティ注入

Child.svelte
<div>
	<slot value={localValue}></slot>
</div>
Parent.svelte
<Child let:value>
  { value }
</Child>

Context API

React の Context とだいたい一緒

const key = Symbol();
setContext(key, {
  getValue: () => value,
});
const { getValue } = getContext(key)

Special elements

自分自身を再帰的に描画する

<svelte:self>

描画するコンポーネントを動的に決定する

<svelte:component this={selected.component}/>

Window オブジェクトのイベントリスナーをバインド出来る抽象コンポーネント

<svelte:window bind:scrollY={y} on:keydown={handleKeydown}/>

同じく body

<svelte:body
  on:mouseenter={handleMouseenter}
  on:mouseleave={handleMouseleave}
/>

header へのタグ注入

<svelte:head>
  <link rel="stylesheet" href="/tutorial/dark-theme.css">
</svelte:head>

複数要素を束ねる抽象要素

<Box>
  <svelte:fragment slot="footer">
    <p>All rights reserved.</p>
    <p>Copyright (c) 2019 Svelte Industries</p>
  </svelte:fragment>
</Box>

Module context

同じコンポーネントを複数使用する際に、コンポーネント間で共通のスコープを持てる。private static みたいな…?

<script context="module">
  let current;
</script>
<script context="module">
  const elements = new Set();

  export function stopAll() {
    elements.forEach(element => {
      element.pause();
    });
  }
</script>

Debugging

レンダリングされる際にログを出力できる

{@debug user}
GitHubで編集を提案

Discussion