Closed98

Svelte 公式チュートリアル Part 2: Advanced Svelte をやっていく

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Motion / Tweens

https://learn.svelte.dev/tutorial/tweens

src/App.svelte
<script>
	import { tweened } from 'svelte/motion';
	import { cubicOut } from 'svelte/easing';

	const progress = tweened(0, {
		duration: 400,
		easing: cubicOut,
	});
</script>

<progress value={$progress} />

<button on:click={() => progress.set(0)}>
	0%
</button>

<button on:click={() => progress.set(0.25)}>
	25%
</button>

<button on:click={() => progress.set(0.5)}>
	50%
</button>

<button on:click={() => progress.set(0.75)}>
	75%
</button>

<button on:click={() => progress.set(1)}>
	100%
</button>

<style>
	progress {
		display: block;
		width: 100%;
	}
</style>

tween = between であり間を補完するものらしい。

writable ストアの代わりに tweened ストアを使うことで set() や update() を呼び出した時に指定値に即座に変化せず、現在値から指定値までゆっくり変化していく。

どのように変化するかについては初期化時または set() / update() 時にオプションとして指定できる、オプションは第 2 引数で指定する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Motion / Springs

src/App.svelte
<script>
	import { spring } from 'svelte/motion';

	let coords = spring({ x: 50, y: 50 }, {
		stiffness: 0.1,
		damping: 0.25
	});
	let size = spring(10);
</script>

<svg
	on:mousemove={(e) => {
		coords.set({ x: e.clientX, y: e.clientY });
	}}
	on:mousedown={() => size.set(30)}
	on:mouseup={() => size.set(10)}
>
	<circle
		cx={$coords.x}
		cy={$coords.y}
		r={$size}
	/>
</svg>

<div class="controls">
	<label>
		<h3>stiffness ({coords.stiffness})</h3>
		<input
			bind:value={coords.stiffness}
			type="range"
			min="0.01"
			max="1"
			step="0.01"
		/>
	</label>

	<label>
		<h3>damping ({coords.damping})</h3>
		<input
			bind:value={coords.damping}
			type="range"
			min="0.01"
			max="1"
			step="0.01"
		/>
	</label>
</div>

<style>
	svg {
		position: absolute;
		width: 100%;
		height: 100%;
		left: 0;
		top: 0;
	}

	circle {
		fill: #ff3e00;
	}

	.controls {
		position: absolute;
		top: 1em;
		right: 1em;
		width: 200px;
		user-select: none;
	}

	.controls input {
		width: 100%;
	}
</style>

Spring とはバネのことだろう。

バネのように収束していく動きを再現するのに便利そうだ。

stiffness と damping の 2 つのオプションを指定できる。

stiffness とは剛性を意味し、低いとビヨーンと伸びやすくなる。

damping は減衰を意味し、低いとブランブランしやすくなる。

stiffness を低くして damping を高くすると tween と似たような挙動になる。

値だけではなくオブジェクトを指定できるのがすごい。

https://svelte.dev/docs/svelte-motion#spring

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / The transition directive

https://learn.svelte.dev/tutorial/transition

src/App.svelte
<script>
	import { fade } from 'svelte/transition';
	let visible = true;
</script>

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

{#if visible}
	<p transition:fade>
		Fades in and out
	</p>
{/if}

transition:fade と設定するだけで設定できる。

内部的にはタイマーと style 属性などを使っているのだろうか。

CSS でも同じようなことはできるのでどうやって使い分ければ良いのだろう?

ドキュメントはこちら。

https://svelte.dev/docs/svelte-transition

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Adding parameters

src/App.svelte
<script>
	import { fly } from 'svelte/transition';
	let visible = true;
</script>

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

{#if visible}
	<p transition:fly={{ y: 200, duration: 2000 }}>
		Flies in and out
	</p>
{/if}

fly を使うと下からニュッと出てくる遷移を実現できる。

遷移はリバーシブル。つまり、遷移の途中で状態が変化するとその位置や透明度から元の場所に戻る。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / In and out

https://learn.svelte.dev/tutorial/in-and-out

src/App.svelte
<script>
	import { fade, fly } from 'svelte/transition';
	let visible = true;
</script>

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

{#if visible}
	<p in:fly={{ y: 200, duration: 2000 }} out:fade>
		Flies in, fades out
	</p>
{/if}

transition: の代わりに in:out: を使うことで表示と非表示の遷移を切り替えることができる。

この場合は遷移はリバーシブルにはならない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Custom CSS transitions

https://learn.svelte.dev/tutorial/custom-css-transitions

src/App.svelte
<script>
	import { fade } from 'svelte/transition';
	import { elasticOut } from 'svelte/easing';

	let visible = true;

	function spin(node, { duration }) {
		return {
			duration,
			css: (t) => {
				const eased = elasticOut(t);

				return `
					transform: scale(${eased}) rotate(${eased * 1080}deg);
					color: hsl(
						${Math.trunc(t * 360)},
						${Math.min(100, 1000 * (1 - t))}%,
						${Math.min(50, 500 * (1 - t))}%
					);
				`;
			}
		};
	}
</script>

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

{#if visible}
	<div
		class="centered"
		in:spin={{ duration: 8000 }}
		out:fade
	>
		<span>transitions!</span>
	</div>
{/if}

<style>
	.centered {
		position: absolute;
		left: 50%;
		top: 50%;
		transform: translate(-50%, -50%);
	}

	span {
		position: absolute;
		transform: translate(-50%, -50%);
		font-size: 4em;
	}
</style>

transition の正体は 2 引数関数なので自分でも作ることができる。

第 1 引数は遷移が適用されるノード。

第 2 引数は遷移のパラメーター。

戻り値は下記のプロパティを持つオブジェクト

  • delay:遷移発生までの待機時間(ミリ秒)
  • duration:遷移開始から完了までの時間(ミリ秒)
  • easing:時間の経過に伴う変化率
  • css:ノードの style を返す 2 引数関数
  • tick:ノードに何らかの影響を与える 2 引数関数

https://easings.net/ja

tick よりも css を返す方がパフォーマンス的には良いようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Custom JS transitions

https://learn.svelte.dev/tutorial/custom-js-transitions

src/App.svelte
<script>
	let visible = false;

	function typewriter(node, { speed = 1 }) {
		const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;

		if (!valid) {
			throw new Error(`This transition only works on elements with a single text node child`);
		}

		const text = node.textContent;
		const duration = text.length / (speed * 0.01);

		return {
			duration,
			tick: (t) => {
				const i = Math.trunc(text.length * t);
				node.textContent = text.slice(0, i);
			}
		};
	}
</script>

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

{#if visible}
	<p transition:typewriter>
		The quick brown fox jumps over the lazy dog
	</p>
{/if}

遷移を実装するのにはできる限り css を使うことが推奨されるがエスケープハッチとして tick を使うことができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Transition events

https://learn.svelte.dev/tutorial/transition-events

src/App.svelte
<script>
	import { fly } from 'svelte/transition';

	let visible = true;
	let status = 'waiting...';
</script>

<p>status: {status}</p>

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

{#if visible}
	<p
		transition:fly={{ y: 200, duration: 2000 }}
		on:introstart={() => status = 'intro started'}
		on:outrostart={() => status = 'outro started'}
		on:introend={() => status = 'intro ended'}
		on:outroend={() => status = 'outro ended'}
	>
		Flies in and out
	</p>
{/if}

遷移を設定したノードに on:introstarton:outroend を使うことで遷移の開始時や終了時にコードを実行できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Global transitions

https://learn.svelte.dev/tutorial/global-transitions

src/App.svelte
<script>
	import { slide } from 'svelte/transition';

	let showItems = true;
	let i = 5;
	let items = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
</script>

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

<label>
	<input type="range" bind:value={i} max="10" />
</label>

{#if showItems}
	{#each items.slice(0, i) as item}
		<div transition:slide|global>
			{item}
		</div>
	{/each}
{/if}

<style>
	div {
		padding: 0.5em 0;
		border-top: 1px solid #eee;
	}
</style>

遷移は if や each の直下のノードが表示/非表示になる時のみ実行される。

直下でなくても遷移を実行したい場合は transition:slice|global のようにグローバルにする。

なお、Svelte 3 ではグローバルが標準になり、ローカルにしたい場合は |local と後置する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Key blocks

https://learn.svelte.dev/tutorial/key-blocks

src/App.svelte
<script>
	import { onMount } from 'svelte';
	import { typewriter } from './transition.js';
	import { messages } from './loading-messages.js';

	let i = -1;

	onMount(() => {
		const interval = setInterval(() => {
			i += 1;
			i %= messages.length;
		}, 2500);

		return () => {
			clearInterval(interval);
		};
	});
</script>

<h1>loading...</h1>

{#key i}
	<p in:typewriter={{ speed: 10 }}>
		{messages[i] || ''}
	</p>
{/key}

{#key expr}...{/key} を使うことで式が変化した時にノードを再生成することができる。

式が変化した時に遷移を実行したい場合に便利。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Transitions / Deferred transitions

https://learn.svelte.dev/tutorial/deferred-transitions

src/transition.js
import { crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';

export const [send, receive] = crossfade({
	duration: (d) => Math.sqrt(d * 200),

	fallback(node, params) {
		const style = getComputedStyle(node);
		const transform = style.transform === 'none' ? '' : style.transform;

		return {
			duration: 600,
			easing: quintOut,
			css: (t) => `
				transform: ${transform} scale(${t});
				opacity: ${t}
			`
		};
	}
});
src/TodoList.svelte
<script>
	import { send, receive } from './transition.js';

	export let store;
	export let done;
</script>

<ul class="todos">
	{#each $store.filter((todo) => todo.done === done) as todo (todo.id)}
		<li
			class:done
			in:receive={{ key: todo.id }}
			out:send={{ key: todo.id }}
		>
			<label>
				<input
					type="checkbox"
					checked={todo.done}
					on:change={(e) => store.mark(todo, e.currentTarget.checked)}
				/>

				<span>{todo.description}</span>

				<button on:click={() => store.remove(todo)} aria-label="Remove" />
			</label>
		</li>
	{/each}
</ul>

<style>
	label {
		width: 100%;
		height: 100%;
		display: flex;
	}

	span {
		flex: 1;
	}

	button {
		background-image: url(./remove.svg);
	}
</style>

Defered transitions を使うことで複数のノード間で遷移を調整できるようだ。

この機能を使うことで例えば Todo リストで未完了 ToDo 一覧から完了 ToDo 一覧へアイテムを移動させるような遷移を実現できる。

このような遷移を実現するには crossfade() 関数を使う。

crossfade() 関数は send()receive() の 2 つの配列を返す。

あるノードに対して send() が呼び出された時、receive() が呼び出されたノードを探し、送信側から受信側の位置へ移動しつつフェードアウトする遷移を実行する。

相手方が見つからない場合は fallback() の遷移が実行される。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Animations / The animate directive

https://learn.svelte.dev/tutorial/animate

src/TodoList.svelte(style 割愛)
<script>
	import { flip } from 'svelte/animate';
	import { send, receive } from './transition.js';

	export let store;
	export let done;
</script>

<ul class="todos">
	{#each $store.filter((todo) => todo.done === done) as todo (todo.id)}
		<li
			class:done
			in:receive={{ key: todo.id }}
			out:send={{ key: todo.id }}
			animate:flip={{ duration: 200 }}
		>
			<label>
				<input
					type="checkbox"
					checked={todo.done}
					on:change={(e) => store.mark(todo, e.currentTarget.checked)}
				/>

				<span>{todo.description}</span>

				<button on:click={() => store.remove(todo)} aria-label="Remove" />
			</label>
		</li>
	{/each}
</ul>

遷移しないノードに対して動きを適用するにはアニメーションを使う。

flip とは first, last, invert, play の略らしい。

ドキュメントによるとアニメーションはキーのある each ブロックの要素に変化がある時に実行される。

https://svelte.dev/docs/element-directives#animate-fn

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Actions / The use directive

https://learn.svelte.dev/tutorial/actions

src/App.svelte(一部)
<script>
	import Canvas from './Canvas.svelte';
	import { trapFocus } from './actions.js';

        // ...
</script>

<div class="menu" use:trapFocus>
src/actions.js(一部)
focusable()[0]?.focus();

node.addEventListener('keydown', handleKeydown);

return {
	destroy() {
		node.removeEventListener('keydown', handleKeydown);
		previous?.focus();
	}
};

アクションとはノードレベルのライフサイクル関数のことらしい。

他にもライフサイクル関数もあるが違いは何なのだろう?

戻り値を destroy プロパティを持つオブジェクトにすることでクリーンアップを実行できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Actions / Adding parameters

https://learn.svelte.dev/tutorial/adding-parameters-to-actions

src/App.svelte
<script>
	import tippy from 'tippy.js';
	import 'tippy.js/dist/tippy.css';
	import 'tippy.js/themes/material.css';

	let content = 'Hello!';

	function tooltip(node, options) {
		const tooltip = tippy(node, options);

		return {
			update(options) {
				tooltip.setProps(options);
			},
			destroy() {
				tooltip.destroy();
			}
		};
	}
</script>

<input bind:value={content} />

<button use:tooltip={{ content, theme: 'material' }}>
	Hover me
</button>

actions にはパラメーターを指定できる。

戻り値に update プロパティを指定することでパラメーターに変更があった時にコードを実行できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Contenteditable bindings

https://learn.svelte.dev/tutorial/contenteditable-bindings

src/App.svelte
<script>
	let html = '<p>Write some text!</p>';
</script>

<div bind:innerHTML={html} contenteditable />

<pre>{html}</pre>

<style>
	[contenteditable] {
		padding: 0.5em;
		border: 1px solid #eee;
		border-radius: 4px;
	}
</style>

contenteditable 属性が ON のタグについては textContet と innerHTML のバインディングが可能になる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Each block bindings

https://learn.svelte.dev/tutorial/each-block-bindings

src/App.svelte(抜粋)
{#each todos as todo}
	<li class:done={todo.done}>
		<input
			type="checkbox"
			bind:checked={todo.done}
		/>

		<input
			type="text"
			placeholder="What needs to be done?"
			bind:value={todo.text}
		/>
	</li>
{/each}

each ブロック内であっても通常と同じようにバインディングできる。

each ブロック内の input タグの value 属性にバインディングすると状態の置き換えではなく書き換えが行われる。

状態の置き換えの方が好ましい場合はイベントハンドラーを使った方が良い。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Media elements

https://learn.svelte.dev/tutorial/media-elements

src/AudioPlayer.svelte(style 割愛)
<script>
	export let src;
	export let title;
	export let artist;

	let time = 0;
	let duration = 0;
	let paused = true;

	function format(time) {
		if (isNaN(time)) return '...';

		const minutes = Math.floor(time / 60);
		const seconds = Math.floor(time % 60);

		return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
	}
</script>

<div class="player" class:paused>
	<audio
		{src}
		bind:currentTime={time}
		bind:duration
		bind:paused
		preload="metadata"
		on:ended={() => {
			time = 0;
		}}
	/>
	
	<button
		class="play"
		aria-label={paused ? 'play' : 'pause'}
		on:click={() => paused = !paused}
	/>

	<div class="info">
		<div class="description">
			<strong>{title}</strong> /
			<span>{artist}</span>
		</div>

		<div class="time">
			<span>{format(time)}</span>
			<div
				class="slider"
				on:pointerdown={e => {
					const div = e.currentTarget;
					
					function seek(e) {
						const { left, width } = div.getBoundingClientRect();

						let p = (e.clientX - left) / width;
						if (p < 0) p = 0;
						if (p > 1) p = 1;
						
						time = p * duration;
					}

					seek(e);

					window.addEventListener('pointermove', seek);

					window.addEventListener('pointerup', () => {
						window.removeEventListener('pointermove', seek);
					}, {
						once: true
					});
				}}
			>
				<div class="progress" style="--progress: {time / duration}%" />
			</div>
			<span>{duration ? format(duration) : '--:--'}</span>
		</div>
	</div>
</div>

audio や video などのメディアエレメントの場合、currentTime、duration、paused などへのバンディングが可能。

読み込み専用プロパティは duration, buffered, seekable, played, seeking, ended, readyState の 7 つ。

読み書き可能なプロパティは currentTime, playbackRate, paused, volume, muted の 5 つ。

動画の場合は videoWidth とvideoHeight の読み込み専用プロパティが利用可能。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Dimensions

https://learn.svelte.dev/tutorial/dimensions

src/App.svelte(style 割愛)
<script>
	let w;
	let h;
	let size = 42;
	let text = 'edit this text';
</script>

<label>
	<input type="range" bind:value={size} min="10" max="100" />
	font size ({size}px)
</label>

<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px" contenteditable>{text}</span>
	<span class="size">{w} x {h}px</span>
</div>

div などのブロックエレメントの場合は clientWidth や offsetWidth にバインド可能。

バインドする場合は読み込み専用になる。

オーバーヘッドがあるのであまり多くの要素の横幅や高さを取得することは推奨されない。

インラインエレメントや canvas などの要素を含めないノードには対応していないので、そのような場合はラッパーエレメントを作成する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / This

https://learn.svelte.dev/tutorial/bind-this

src/App.svelte(style 割愛)
<script>
	import { onMount } from 'svelte';
	import { paint } from './gradient.js';

	let canvas;

	onMount(() => {
		const context = canvas.getContext('2d');

		let frame = requestAnimationFrame(function loop(t) {
			frame = requestAnimationFrame(loop);
			paint(context, t);
		});

		return () => {
			cancelAnimationFrame(frame);
		};
	});
</script>

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

bind:this を使うことでノードをバインドすることができる。

マウントされるまでは undefined なので気を付ける必要がある。

TypeScript だと HTMLElement | undefined みたいな型を付ける必要があるのかな?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Component bindings

https://learn.svelte.dev/tutorial/component-bindings

src/App.svelte
<script>
	import Keypad from './Keypad.svelte';

	let pin;
	$: view = pin
		? pin.replace(/\d(?!$)/g, '•')
		: 'enter your pin';

	function handleSubmit() {
		alert(`submitted ${pin}`);
	}
</script>

<h1 style="opacity: {pin ? 1 : 0.4}">
	{view}
</h1>

<Keypad
	bind:value={pin}
	on:submit={handleSubmit}
/>
src/Keypad.svelte(style 割愛)
<script>
	import { createEventDispatcher } from 'svelte';

	export let value = '';

	const dispatch = createEventDispatcher();

	const select = (num) => () => (value += num);
	const clear = () => (value = '');
	const submit = () => dispatch('submit');
</script>

<div class="keypad">
	<button on:click={select(1)}>1</button>
	<button on:click={select(2)}>2</button>
	<button on:click={select(3)}>3</button>
	<button on:click={select(4)}>4</button>
	<button on:click={select(5)}>5</button>
	<button on:click={select(6)}>6</button>
	<button on:click={select(7)}>7</button>
	<button on:click={select(8)}>8</button>
	<button on:click={select(9)}>9</button>

	<button disabled={!value} on:click={clear}
		>clear</button
	>
	<button on:click={select(0)}>0</button>
	<button disabled={!value} on:click={submit}
		>submit</button
	>
</div>

コンポーネントのプロパティにも bind を使ってバインディングできる。

便利だが多用するとデータがあちこちに散らばって管理しにくくなる。

基本的にはストアかイベントを使った方が良さそう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Advanced bindings / Binding to component instances

https://learn.svelte.dev/tutorial/component-this

src/Canvas.svelte(抜粋)
export let color;
export let size;

export function clear() {
	context.clearRect(0, 0, canvas.width, canvas.height);
}
src/App.svelte(抜粋)
<script>
	import Canvas from './Canvas.svelte';
	import { trapFocus } from './actions.js';

	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
	let selected = colors[0];
	let size = 10;

	let showMenu = true;
	let canvas;
</script>

<div class="container">
	<Canvas bind:this={canvas} color={selected} size={size} />
	...
</div>
src/App.svelte(抜粋)
<div class="controls">
	<button class="show-menu" on:click={() => showMenu = !showMenu}>
		{showMenu ? 'close' : 'menu'}
	</button>

	<button on:click={() => canvas.clear()}>
		clear
	</button>
</div>

コンポーネントのインスタンスも bind:this を使って取得できる。

インスタンスを通じてエクスポートされたプロパティや関数にアクセスできる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Classes and styles / The class directive

https://learn.svelte.dev/tutorial/classes

src/App.svelte(style 割愛)
<script>
	let flipped = false;
</script>

<div class="container">
	Flip the card
	<button
		class="card"
		class:flipped={flipped}
		on:click={() => flipped = !flipped}
	>
		<div class="front">
			<span class="symbol"></span>
		</div>
		<div class="back">
			<div class="pattern"></div>
		</div>
	</button>
</div>

class:flipped={flipped} のように書くことで flipped が真の時に flipped クラスを有効化できる。

class="card {flipped ? 'flipped : ''}" のように書くこともできるがクラスディレクティブを使った方が楽。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Classes and styles / Shorthand class directive

https://learn.svelte.dev/tutorial/class-shorthand

src/App.svelte(style 割愛)
<script>
	let flipped = false;
</script>

<div class="container">
	Flip the card
	<button
		class="card"
		class:flipped
		on:click={() => flipped = !flipped}
	>
		<div class="front">
			<span class="symbol"></span>
		</div>
		<div class="back">
			<div class="pattern"></div>
		</div>
	</button>
</div>

class:flipped={flipped}class:flipped に省略できる。

クラス名と変数名はなるべく同じにしておいた方が良いのかな?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Classes and styles / The style directive

https://learn.svelte.dev/tutorial/styles

src/App.svelte(style 割愛)
<script>
	let flipped = false;
</script>

<div class="container">
	Flip the card
	<button
		class="card"
		style:transform={flipped ? 'rotate(0)' : ''}
		style:--bg-1="palegoldenrod"
		style:--bg-2="black"
		style:--bg-3="goldenrod"
		on:click={() => flipped = !flipped}
	>
		<div class="front">
			<span class="symbol"></span>
		</div>
		<div class="back">
			<div class="pattern"></div>
		</div>
	</button>
</div>

style:transform={flipped ? 'rotate(0)' : ''} のように書くことで flipped が真の時に style="transform: rotate(0)" になるようにできる。

class ディレクティブの場合は真偽値だが、style ディレクティブの場合は文字列なので若干異なる。

class の場合と同様に style ディレクティブも複数設けることができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2/ Classes and styles / Component styles

https://learn.svelte.dev/tutorial/component-styles

src/App.svelte
<script>
	import Box from './Box.svelte';
</script>

<div class="boxes">
	<Box --color="red" />
	<Box --color="green" />
	<Box --color="blue" />
</div>
src/Box.svelte
<div class="box" />

<style>
	.box {
		width: 5em;
		height: 5em;
		border-radius: 0.5em;
		margin: 0 0 1em 0;
		background-color: var(--color, #ddd);
	}
</style>

--color="red"のようにしてコンポーネントの CSS カスタムプロパティを指定できる。

下記が気になるので後から詳しく調べてみよう。

This feature works by wrapping each component in a <div style="display: contents">, where needed, and applying the custom properties to it.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Component composition / Named slots

https://learn.svelte.dev/tutorial/named-slots

Card.svelte(style 割愛)
<div class="card">
	<header>
		<slot name="telephone" />
		<slot name="company" />
	</header>

	<slot />

	<footer>
		<slot name="address" />
	</footer>
</div>
App.svelte(style 割愛)
<script>
	import Card from './Card.svelte';
</script>

<main>
	<Card>
		<span>Patrick BATEMAN</span>
		<span>Vice President</span>

		<span slot="telephone">212 555 6342</span>

		<span slot="company">
			Pierce &amp; Pierce
			<small>Mergers and Aquisitions</small>
		</span>
		
		<span slot="address">358 Exchange Place, New York, N.Y. 100099 fax 212 555 6390 telex 10 4534</span>
	</Card>
</main>

<slot name="..."> のように書くことで名前付きスロットを設けることができる。

名前付きスロットに要素を注入するには <span slot="..."> のように書く。

注入する要素にスタイルを適用したい場合は注入される側ではなく注入する側の style タグ内に記述する。

上記のソースコードの場合は App.svelte の方にスタイルを書く必要がある。

裏技として注入される側に下記のように書くこともできる。

Card.svelte(style のみ)
<style>
	/* ... */ 

	.card :global(small) {
		display: block;
		font-size: 0.6em;
		text-align: right;
	}
</style>
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Component composition / Slot fallbacks

https://learn.svelte.dev/tutorial/slot-fallbacks

Card.svelte(style 割愛)
<div class="card">
	<header>
		<slot name="telephone">
			<i>(telephone)</i>
		</slot>

		<slot name="company">
			<i>(company name)</i>
		</slot>
	</header>

	<slot>
		<i>(name)</i>
	</slot>
		
	<footer>
		<slot name="address">
			<i>(address)</i>
		</slot>
	</footer>
</div>

<slot> 内に子要素を書いておくと未指定の場合に表示されるようになる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Component composition / Slot props

https://learn.svelte.dev/tutorial/slot-props

FilterableList.svelte(style 割愛)
<script>
	export let data;
	export let field;

	let search = '';

	$: regex = search ? new RegExp(search, 'i') : null;
	$: matches = (item) => regex ? regex.test(item[field]) : true;
</script>

<div class="list">
	<label>
		Filter: <input bind:value={search} />
	</label>

	<div class="header">
		<slot name="header"/>
	</div>
	
	<div class="content">
		{#each data.filter(matches) as item}
			<slot {item} />
		{/each}
	</div>
</div>
App.svelte
<script>
	import FilterableList from './FilterableList.svelte';
	import { colors } from './colors.js';
</script>

<FilterableList
	data={colors}
	field="name"
	let:item={row}
>
	<header slot="header" class="row">
		<span class="color" />
		<span class="name">name</span>
		<span class="hex">hex</span>
		<span class="rgb">rgb</span>
		<span class="hsl">hsl</span>
	</header>

	<div class="row">
		<span class="color" style="background-color: {row.hex}" />
		<span class="name">{row.name}</span>
		<span class="hex">{row.hex}</span>
		<span class="rgb">{row.rgb}</span>
		<span class="hsl">{row.hsl}</span>
	</div>
</FilterableList>

注入される側のコンポーネントから注入する側のコンポーネントにデータを返すことができる。

データを返すには <slot item={item} /> のように書く、<slot {item}> にも省略可能。

注入する側の方でデータを受け取るには <FilterableList let:item={row}> のように書く、そうするとコンポーネント内で row 変数を使えるようになる。

名前付きスロットもプロパティを持つことができ、その場合は <span> などに let を書く。

Named slots can also have props; use the let directive on an element with a slot="..." attribute, instead of on the component itself.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Component composition / Checking for slot content

https://learn.svelte.dev/tutorial/optional-slots

FilterableList.svelte(抜粋)
	{#if $$slots.header}
		<div class="header">
			<slot name="header"/>
		</div>
	{/if}

スロットにコンテンツが注入されているかどうかでコンポーネントの表示を変えたい場合は $$slots を使う。

header スロットにコンテンツが注入されていない場合 $$slots.header は undefined になる。

https://svelte.dev/docs/special-elements#slot-$$slots

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Context API / setContext and getContext

App.svelte(抜粋)
<div class="container">
	<Canvas width={800} height={1200}>
		{#each Array(12) as _, c}
			{#each Array(22) as _, r}
				<Square
					x={180 + c * 40 + jitter(r * 2)}
					y={180 + r * 40 + jitter(r * 2)}
					size={40}
					rotate={jitter(r * 0.05)}
				/>
			{/each}
		{/each}
	</Canvas>
</div>
Canvas.svelte(抜粋)
	setContext('canvas', {
		addItem
	})

	function addItem(fn) {
		onMount(() => {
			items.add(fn);
			return () => items.delete(fn);
		});
		
		afterUpdate(async () => {
			if (scheduled) return;
			
			scheduled = true;
			await tick();
			scheduled = false;

			draw();
		});
	}
Square.svelte(抜粋)
	getContext('canvas').addItem(draw)

このページは色々と学ぶことが多い。

まず getContext() と setContext() を使って親コンポーネントから子孫コンポーネントにデータや関数を渡せる。

乱用は禁物だが、プロパティやイベントハンドラでは大変な時には便利そうだ。

また、{#each Array(12) as _, c} というコーディングの時に書き方は便利そうだ。

onMount() がトップレベル以外でも呼び出し可能なことも興味深い。

ただ、addItem() がトップレベルで呼び出されているのでよく考えたら一緒なのか。

Canvas API を使っているのに App.svelte では宣言的に書けているのがすごいなーと思う。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements/ svelte:self

https://learn.svelte.dev/tutorial/svelte-self

Folder.svelte(style 割愛)
<script>
	import File from './File.svelte';

	export let expanded = false;
	export let name;
	export let files;

	function toggle() {
		expanded = !expanded;
	}
</script>

<button class:expanded on:click={toggle}>{name}</button>

{#if expanded}
	<ul>
		{#each files as file}
			<li>
				{#if file.files}
					<svelte:self {...file}/>
				{:else}
					<File {...file} />
				{/if}
			</li>
		{/each}
	</ul>
{/if}

再帰構造などで自分を参照するときには <svelte:self> を使う。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:component

https://learn.svelte.dev/tutorial/svelte-component

App.svelte
<script>
	import RedThing from './RedThing.svelte';
	import GreenThing from './GreenThing.svelte';
	import BlueThing from './BlueThing.svelte';

	const options = [
		{ color: 'red', component: RedThing },
		{ color: 'green', component: GreenThing },
		{ color: 'blue', component: BlueThing }
	];

	let selected = options[0];
</script>

<select bind:value={selected}>
	{#each options as option}
		<option value={option}>{option.color}</option>
	{/each}
</select>

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

何らかの変数に代入したコンポーネントを描画したい時には <svelte:component> を使う。

コンポーネントは this プロパティに渡す。

ドキュメントによると this 以外のプロパティも渡せるようだ。

https://svelte.dev/docs/special-elements

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:element

https://learn.svelte.dev/tutorial/svelte-element

App.svelte
<script>
	const options = ['h1', 'h2', 'h3', 'p', 'marquee'];
	let selected = options[0];
</script>

<select bind:value={selected}>
	{#each options as option}
		<option value={option}>{option}</option>
	{/each}
</select>

<svelte:element this={selected}>
	I'm a <code>&lt;{selected}&gt;</code> element
</svelte:element>

<svelte:component> と同様に変数に格納された h1 などの文字列を HTML タグとして描画したい場合には <svelte:element> を使用する。

こちらも a タグなどの場合は href が使えそうだ。

ただよく考えるとタグ固有の属性を指定する場合は <#if ...> が必要になりそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:window

https://learn.svelte.dev/tutorial/svelte-window

App.svelte
<script>
	let key;
	let keyCode;

	function handleKeydown(event) {
		key = event.key;
		keyCode = event.keyCode;
	}
</script>

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

<div style="text-align: center">
	{#if key}
		<kbd>{key === ' ' ? 'Space' : key}</kbd>
		<p>{keyCode}</p>
	{:else}
		<p>Focus this window and press any key</p>
	{/if}
</div>

<svelte:window> にバインドすることで window オブジェクトにイベントハンドラなどを設定できる。

普通に script 内でも window.addEventListener() を呼び出しても良さそうだが、こちらの方が色々と便利かも知れない。

preventDefault などのモディファイヤーも使うことができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:window bindings

https://learn.svelte.dev/tutorial/svelte-window-bindings

App.svelte
<script>
	let y = 0;
</script>

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

<span>depth: {y}px</span>

<style>
	:global(body) {
		height: 400vw;
		background: url(./deepsea.webp);
		background-size: cover;
	}

	span {
		position: fixed;
		font-size: 2em;
		color: white;
		font-variant: tabular-nums;
	}
</style>

下記のプロパティにバインドできる。

  • innerWidth
  • innerHeight
  • outerWidth
  • outerHeight
  • scrollX
  • scrollY
  • online (window.navigator.onLine)

scrollX と scrollY 以外は読み込み専用。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:body

https://learn.svelte.dev/tutorial/svelte-body

App.svelte
<script>
	import kitten from './kitten.png';

	let hereKitty = false;
</script>

<svelte:body
	on:mouseenter={() => hereKitty = true}
	on:mouseleave={() => hereKitty = false}
/>

<!-- creative commons BY-NC http://www.pngall.com/kitten-png/download/7247 -->
<img
	class:curious={hereKitty}
	alt="Kitten wants to know what's going on"
	src={kitten}
/>

<style>
	img {
		position: absolute;
		left: 0;
		bottom: -60px;
		transform: translate(-80%, 0) rotate(-15deg);
		transform-origin: 100% 100%;
		transition: transform 0.4s;
	}

	.curious {
		transform: translate(-15%, 0) rotate(0deg);
	}

	:global(body) {
		overflow: hidden;
	}
</style>

<svelte:window> 同様、svelte:body にバインドすることで body 要素にイベントハンドラなどを設定できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:document

https://learn.svelte.dev/tutorial/svelte-document

App.svelte
<script>
	let selection = '';

	const handleSelectionChange = (e) => selection = document.getSelection();
</script>

<svelte:document on:selectionchange={handleSelectionChange} />

<h1>Select this text to fire events</h1>
<p>Selection: {selection}</p>

window / body と同様、<svelte:document> にバインドすることで document 要素にイベントハンドラなどを設定できる。

selectionchange などを設定する時に便利、初めて使ったなコレ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:head

https://learn.svelte.dev/tutorial/svelte-head

App.svelte
<script>
	const themes = ['margaritaville', 'retrowave', 'spaaaaace', 'halloween'];
	let selected = themes[0];
</script>

<svelte:head>
	<link rel="stylesheet" href="/stylesheets/{selected}.css" />
</svelte:head>

<h1>Welcome to my site!</h1>

<select bind:value={selected}>
	<option disabled>choose a theme</option>

	{#each themes as theme}
		<option>{theme}</option>
	{/each}
</select>

<svelte:head> を使うことで title や meta タグを動的に変更することができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:options

https://learn.svelte.dev/tutorial/svelte-options

App.svelte
<script>
	import Todo from './Todo.svelte';

	let todos = [
		{ id: 1, done: true, text: 'wash the car' },
		{ id: 2, done: false, text: 'take the dog for a walk' },
		{ id: 3, done: false, text: 'mow the lawn' }
	];

	function toggle(toggled) {
		todos = todos.map((todo) => {
			if (todo === toggled) {
				// return a new object
				return {
					id: todo.id,
					text: todo.text,
					done: !todo.done
				};
			}

			// return the same object
			return todo;
		});
	}
</script>

<div class="centered">
	<h1>todos</h1>

	<ul class="todos">
		{#each todos as todo (todo.id)}
			<Todo {todo} on:change={() => toggle(todo)} />
		{/each}
	</ul>
</div>

<style>
	.centered {
		max-width: 20em;
		margin: 0 auto;
	}
</style>
Todo.svelte
<svelte:options immutable />

<script>
	import { afterUpdate } from 'svelte';
	import flash from './flash.js';

	export let todo;

	let element;

	afterUpdate(() => {
		flash(element);
	});
</script>

<!-- the text will flash red whenever
     the `todo` object changes -->
<li bind:this={element}>
	<label>
		<input type="checkbox" checked={todo.done} on:change />
		{todo.text}
	</label>
</li>

コンポーネントの冒頭で <svelte:options> を使うことでコンパイラオプションを指定できる。

immutable オプションはプロパティが変化しないことを約束し、参照による等値チェックが行われるようにする。

デフォルトでは immutable は OFF になっており、少しでも変更された可能性があれば Svelte はコンポーネントを更新する。

immutable の他にも accessors などいくつかのオプションがある。

https://svelte.dev/docs/special-elements#svelte-options

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Special elements / svelte:fragment

https://learn.svelte.dev/tutorial/svelte-fragment

App.svelte(style 割愛)
<script>
	import Board from './Board.svelte';
	import { getWinningLine } from './utils.js';

	let squares = Array(9).fill('');
	let next = 'x';

	$: winningLine = getWinningLine(squares);
</script>

<div class="container">
	<Board size={3}>
		<svelte:fragment slot="game">
			{#each squares as square, i}
				<button
					class="square"
					class:winning={winningLine?.includes(i)}
					disabled={square}
					on:click={() => {
						squares[i] = next;
						next = next === 'x' ? 'o' : 'x';
					}}
				>
					{square}
				</button>
			{/each}
		</svelte:fragment>

		<div slot="controls">
			<button on:click={() => {
				squares = Array(9).fill('');
				next = 'x';
			}}>
				Reset
			</button>
		</div>
	</Board>
</div>

div や span の代わりに <svelte:fragment> を使うことでスロットの直接の子孫にできる。

React の Fragment と同じだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Module context / Sharing code

https://learn.svelte.dev/tutorial/sharing-code

AudioPlayer.svelte(抜粋)
<script context="module">
	let current;
</script>

...

<div class="player" class:paused>
	<audio
		src={src}
		bind:currentTime={time}
		bind:duration
		bind:paused
		on:play={(e) => {
			const audio = e.currentTarget;

			if (audio !== current) {
				current?.pause();
				current = audio;
			}
		}}
		on:ended={() => {
			time = 0;
		}}
	/>
	...
</div>

<script context="module"> を使うことで同じコンポーネント間で共有できる変数や関数を設けることができる。

通常の <script> はコンポーネントが初期化された時に 1 回だけ実行されるのに対し、<script context="module"> はモジュールが最初に評価される時(おそらく最初に読み込まれた時)に 1 回だけ実行される。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Module context / Exports

https://learn.svelte.dev/tutorial/module-exports

AudioPlayer.svelte(冒頭のみ)
<script context="module">
	let current;

	export function stopAll() {
		current?.pause();
	}
</script>
App.svelte(style 割愛)
<script>
	import AudioPlayer, { stopAll } from './AudioPlayer.svelte';
	import { tracks } from './tracks.js';
</script>

<div class="centered">
	{#each tracks as track}
		<AudioPlayer {...track} />
	{/each}

	<button on:click={stopAll}>
		stop all
	</button>
</div>

<script context="module"> 内で export した変数や関数は import できる。

default export はコンポーネント本体に決まっているので使うことはできない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 2 / Miscellaneous / The @debug tag

https://learn.svelte.dev/tutorial/debug

App.svelte
<script>
	let user = {
		firstname: 'Ada',
		lastname: 'Lovelace'
	};
</script>

<label>
	<input bind:value={user.firstname} />
	first name
</label>

<label>
	<input bind:value={user.lastname} />
	last name
</label>

{@debug user}

<h1>Hello {user.firstname}!</h1>

{@debug ...} を使うことで開発者ツールを開いている状態で到達すると停止できる。

コールスタックやローカル変数の値を表示できる。

停止が必要なければ console.log() を使っても良い。

チュートリアルでは iframe セキュリティ制限のため何も表示されない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Motion まとめ

https://zenn.dev/link/comments/8da07d78229bb0

Tweens

  • tween = between で補完するものを意味する。
  • writable ストアの代わりに tweened ストアを使うことで set() や update() を呼び出した時に指定値に即座に変化せず、現在値から指定値までゆっくり変化していく。
    どのように変化するかについては初期化時または set() / update() 時にオプションとして指定できる。
  • オプションは第 2 引数で指定する。

Springs

  • Springs とはバネのこと、バネのように収束していく動きを再現するのに便利。
  • stiffness と damping の 2 つのオプションを指定できる。
  • stiffness とは剛性を意味し、低いとビヨーンと伸びやすくなる。
  • damping は減衰を意味し、低いとブランブランしやすくなる。
  • stiffness を低くして damping を高くすると tween と似たような挙動になる。
  • 値だけではなくオブジェクトや配列を指定できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Transitions まとめ

https://zenn.dev/link/comments/51778c08bb14a1

The transition directive

  • transition:fade と設定するだけでフェードイン/フェードアウトするようになる。
  • 内部的にはタイマーと style 属性などを使っているのだろうか?

Adding parameters

  • fly を使うと下からニュッと出てくる遷移を実現できる。
  • 遷移はリバーシブル、つまり遷移の途中で状態が変化するとその位置や透明度から元の場所に戻る。

In and out

  • transition: の代わりに in:out: を使うことで表示と非表示の遷移を切り替えることができる。
  • この場合は遷移はリバーシブルにはならない。

Custom CSS transitions

  • transition の正体は 2 引数関数なので自分でも作ることができる。
  • 第 1 引数は遷移が適用されるノード、第 2 引数は遷移のパラメーター。
  • 戻り値については Custom CSS transitions の投稿に記載。
  • tick よりも css を返す方がパフォーマンス的には良いようだ。

Custom JS transitions

  • 遷移を実装するのにはできる限り css を使うことが推奨される。
  • エスケープハッチとして tick を使うことができる。

Transition events

  • 遷移を設定したノードに on:introstarton:outroend を使うことで遷移の開始時や終了時にコードを実行できる。
  • on:introendon:outrostart もある。

Global transitions

  • 遷移は if や each の直下のノードが表示/非表示になる時のみ実行される。
  • 直下でなくても遷移を実行したい場合は transition:slice|global のようにグローバルにする。
  • Svelte 3 ではグローバルが標準になり、ローカルにしたい場合は transition:slice|local のように書く。

Key blocks

  • {#key expr}...{/key} を使うことで式が変化した時にノードを再生成することができる。
  • 式が変化した時に遷移を実行したい場合に便利。

Deferred transitions

  • Defered transitions を使うことで複数のノード間で遷移を調整できるようだ。
  • この機能を使うことで例えば Todo リストで未完了 ToDo 一覧から完了 ToDo 一覧へアイテムを移動させるような遷移を実現できる。
  • このような遷移を実現するには crossfade() 関数を使う。
  • crossfade() 関数は send() と receive() の 2 つの配列を返す。
  • あるノードに対して send() が呼び出された時、receive() が呼び出されたノードを探し、送信側から受信側の位置へ移動しつつフェードアウトする遷移を実行する。
  • 相手方が見つからない場合は fallback() の遷移が実行される。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Animations まとめ

https://zenn.dev/link/comments/b6c568db8f91ca

The animate directive

  • 遷移しないノードに対して動きを適用するにはアニメーションを使う。
  • flip とは first, last, invert, play の略らしい。
  • ドキュメントによるとアニメーションはキーのある each ブロックの要素に変化がある時に実行される。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Actions まとめ

https://learn.svelte.dev/tutorial/actions

The use directive

  • アクションとはノードレベルのライフサイクル関数のことらしい。
  • 他にもライフサイクル関数もあるが違いは何なのだろう?
  • 戻り値を destroy プロパティを持つオブジェクトにすることでクリーンアップを実行できる。

Adding parameters

  • actions にはパラメーターを指定できる。
  • 戻り値に update プロパティを指定することでパラメーターに変更があった時にコードを実行できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Advanced bindings まとめ

https://zenn.dev/link/comments/be377fa7fcd516

Contenteditable bindings

  • contenteditable 属性が ON のタグについては textContet と innerHTML のバインディングが可能になる。
  • contenteditable 属性については MDN の下記ページが詳しい。

https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/contenteditable

contenteditable グローバル属性は、ユーザーによる要素の編集が可能かを示す列挙型属性です。可能である場合、ブラウザーは要素のウィジェットを編集可能なものに変更します。

Each block bindings

  • each ブロック内であっても通常と同じようにバインディングできる。
  • each ブロック内の input タグの value 属性にバインディングすると状態の置き換えではなく書き換えが行われる。
  • 状態の置き換えの方が好ましい場合はイベントハンドラーを使った方が良い。

Media elements

  • audio や video などのメディアエレメントの場合、currentTime、duration、paused などへのバンディングが可能。
  • 読み込み専用プロパティは duration, buffered, seekable, played, seeking, ended, readyState の 7 つ。
  • 読み書き可能なプロパティは currentTime, playbackRate, paused, volume, muted の 5 つ。
  • 動画の場合は videoWidth とvideoHeight の読み込み専用プロパティが利用可能。

Dimensions

  • div などのブロックエレメントの場合は clientWidth や offsetWidth にバインド可能。
  • バインドする場合は読み込み専用になる。
  • オーバーヘッドがあるのであまり多くの要素の横幅や高さを取得することは推奨されない。
  • インラインエレメントや canvas などの要素を含めないノードには対応していないので、そのような場合はラッパーエレメントを作成する。

This

  • bind:this を使うことでノードをバインドすることができる。
  • マウントされるまでは undefined なので気を付ける必要がある。
  • TypeScript だと HTMLElement | undefined みたいな型を付ける必要があるのかな?

Component bindings

  • コンポーネントのプロパティにも bind を使ってバインディングできる。
  • 便利だが多用するとデータがあちこちに散らばって管理しにくくなる。
  • 基本的にはストアかイベントを使った方が良さそう。

Binding to component instances

  • コンポーネントのインスタンスも bind:this を使って取得できる。
  • インスタンスを通じてエクスポートされたプロパティや関数にアクセスできる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Classes and styles まとめ

https://zenn.dev/link/comments/b1ae86c7aa8790

The class directive

  • class:flipped={flipped} のように書くことで flipped が真の時に flipped クラスを有効化できる。
  • class="card {flipped ? 'flipped : ''}" のように書くこともできるがクラスディレクティブを使った方が楽。

Shorthand class directive

  • class:flipped={flipped}class:flipped に省略できる。
  • クラス名と変数名はなるべく同じにしておいた方が良いのかな?

The style directive

  • style:transform={flipped ? 'rotate(0)' : ''} のように書くことで flipped が真の時に style="transform: rotate(0)" になるようにできる。
  • class ディレクティブの場合は真偽値だが、style ディレクティブの場合は文字列なので若干異なる。
  • class の場合と同様に style ディレクティブも複数設けることができる。

Component styles

  • --color="red" のようにしてコンポーネントの CSS カスタムプロパティを指定できる。
  • display: contents が関連しているので必要に応じて詳細を確認した法が良さそう。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Component composition まとめ

https://zenn.dev/link/comments/add449b91ce624

Slots

  • <slot> 要素を使うことでコンテンツを注入できる。
  • 注入するコンテンツはコンポーネントの子要素として記述する。

Named slots

  • <slot name="..."> のように書くことで名前付きスロットを設けることができる。
  • 名前付きスロットに要素を注入するには <span slot="..."> のように書く。
  • 注入する要素にスタイルを適用したい場合は注入される側ではなく注入する側の style タグ内に記述する。
  • 裏技として注入される側に下記のように書くこともできる。
注入される側
<style>
	/* ... */ 

	.card :global(small) {
		display: block;
		font-size: 0.6em;
		text-align: right;
	}
</style>

Slot fallbacks

  • <slot> 内に子要素を書いておくと未指定の場合に表示されるようになる。
  • 多分だけどスタイルを適用したい場合には注入される側のコンポーネントに書けば良さそう。

Slot props

  • 注入される側のコンポーネントから注入する側のコンポーネントにデータを返すことができる。
  • データを返すには <slot item={item} /> のように書く、<slot {item}> にも省略可能。
  • 注入する側の方でデータを受け取るには <FilterableList let:item={row}> のように書く、そうするとコンポーネント内で row 変数を使えるようになる。
  • 名前付きスロットもプロパティを持つことができ、その場合は <span> などに let を書く。

Checking for slot content

  • スロットにコンテンツが注入されているかどうかでコンポーネントの表示を変えたい場合は $$slots を使- う。
    header スロットにコンテンツが注入されていない場合 $$slots.header は undefined になる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Context API まとめ

https://zenn.dev/link/comments/318735f29de7d3

setContent and getContext

  • getContext() と setContext() を使って親コンポーネントから子孫コンポーネントにデータや関数を受け渡しできる。
  • 乱用は禁物だが、プロパティやイベントハンドラでは大変な時には便利そうだ。
  • {#each Array(12) as _, c} というコーディングの時に書き方は便利そうだ。
  • onMount() がトップレベル以外でも呼び出し可能。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Special elements まとめ

https://zenn.dev/link/comments/8d5d223935bed2

svelte:self

  • コンポーネントが自分自身をインポートすることはできない。
  • 再帰構造などで自分を参照するときには <svelte:self> を使う。

svelte:component

  • 何らかの変数に代入したコンポーネントを描画したい時には <svelte:component> を使う。
  • コンポーネントは this プロパティに渡す。
  • ドキュメントによると this 以外のプロパティも渡せる。

https://svelte.dev/docs/special-elements

svelte:element

  • 変数に格納された h1 などの文字列を HTML タグとして描画したい場合には <svelte:element> を使用する。
  • a タグなどの場合は href が使えそう。
  • タグ固有の属性を指定する場合は <#if ...> が必要になりそう。

svelte:window

  • <svelte:window> にバインドすることで window オブジェクトにイベントハンドラなどを設定できる。
  • 普通に script 内でも window.addEventListener() を呼び出しても良さそうだが、こちらの方が色々と便利かも知れない。
  • preventDefault などのモディファイヤーも使うことができる。

svelte:window bindings

下記のプロパティにバインドできる。

  • innerWidth
  • innerHeight
  • outerWidth
  • outerHeight
  • scrollX
  • scrollY
  • online (window.navigator.onLine)

scrollX と scrollY 以外は読み込み専用。

svelte:body

  • svelte:body にバインドすることで body 要素にイベントハンドラなどを設定できる。
  • 普通に script 内でも document.body.addEventListener() を呼び出しても良いが、removeEventListener() を行う必要が無さそう。
  • preventDefault などのモディファイヤーも使うことができる。

svelte:document

  • <svelte:document> にバインドすることで document 要素にイベントハンドラなどを設定できる。
  • selectionchange などを設定する時に便利。

svelte:head

  • <svelte:head> を使うことで title や meta タグを動的に変更することができる。
  • SSR する時はどうなるんだろう?

svelte:options

  • コンポーネントの冒頭で <svelte:options> を使うことでコンパイラオプションを指定できる。
  • immutable オプションはプロパティが変化しないことを約束し、参照による等値チェックが行われるようにする。
  • デフォルトでは immutable は OFF になっており、少しでも変更された可能性があれば Svelte はコンポーネントを更新する。
  • immutable の他にも accessors などいくつかのオプションがある。

https://svelte.dev/docs/special-elements#svelte-options

svelte:frament

  • div や span の代わりに svelte:fragment を使うことで複数のノードをスロットの直接の子孫にできる。
  • React の Fragment と同じような感じ。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Miscellaneous まとめ

The @debug tag

  • {@debug ...} を使うことで開発者ツールを開いている状態で到達すると停止できる。
  • コールスタックやローカル変数の値を表示できる。
  • 停止が必要なければ console.log() を使っても良い。
  • iframe 内ではセキュリティ制限のため何も表示されない。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

完了!

5 分経過した、累計 415 分。

Part 1 は 305 分だったので合わせると 720 分、ちょうど 12 時間だ。

次は SvelteKit について学んでいこう。

このスクラップは2023/10/31にクローズされました