Closed78

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

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

Part 1 / Introduction / Styling

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

src/App.svelte
<p>This is a paragraph.</p>

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

style タグを使うと CSS を使って外観を調整できる。

影響はコンポーネント内に閉じられている。

チュートリアルページでコピペしようとしたらメッセージが表示された、よくできていると敬服した。

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

Part 1 / Introduction / Nested components

https://learn.svelte.dev/tutorial/nested-components

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

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

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

import 文を使って他のコンポーネントを利用できる。

コンポーネント間に親子関係があっても style の影響は及ばない。

Next.js ばかり使っているせいか Nested とタイプするときにどうしても Nexted とタイプしてしまう。

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

Part 1 / Introduction が終了

手を動かせるしくみがあるので楽しい。

あと内容がわかりやすいのも素晴らしい。

もう既に Svelte が好きになってしまった。

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

Part 1 / Reactivity / Declarations

https://learn.svelte.dev/tutorial/reactive-declarations

src/App.svelte
<script>
	let count = 0;
	$: doubled = count * 2;

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

<button on:click={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

<p>{count} doubled is {doubled}</p>

$: を使うことで計算結果を保存できる。

多分良い感じに依存関係とかは解決してくれるのだろう。

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

Part 1 / Reactivity / Updating arrays and objects

https://learn.svelte.dev/tutorial/updating-arrays-and-objects

src/App.svelte
<script>
	let numbers = [1, 2, 3, 4];

	function addNumber() {
		numbers = [...numbers, numbers.length + 1];
	}

	$: sum = numbers.reduce((total, currentNumber) => total + currentNumber, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>
	Add a number
</button>

push() や shift() などを使っても更新されないので代入する必要がある。

左辺に状態の変数が含まれている必要がある。

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

Part 1 / Props / Default values

https://learn.svelte.dev/tutorial/default-values

src/Nexted.svelte
<script>
	export let answer = 'a mystery';
</script>

<p>The answer is {answer}</p>
src/App.svelte
<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42} />
<Nested />

export した状態に初期値を与えることでプロパティを指定しなかった時のデフォルト値を設定できる。

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

Part 1 / Props / Spread Props

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

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

	const pkg = {
		name: 'svelte',
		speed: 'blazing',
		version: 4,
		website: 'https://svelte.dev'
	};
</script>

<PackageInfo {...pkg} />
src/PackageInfo.svelte
<script>
	export let name;
	export let version;
	export let speed;
	export let website;

	$: href = `https://www.npmjs.com/package/${name}`;
</script>

<p>
	The <code>{name}</code> package is {speed} fast. Download version {version} from
	<a {href}>npm</a> and <a href={website}>learn more here</a>
</p>

オブジェクトは {...pkg} のように書くことで name={pkg.name} のように個々に指定しなくて大丈夫。

この辺りは React と同じなので覚えやすい。

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

Part 1 / Logic / If blocks

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

src/App.svelte
<script>
	let count = 0;

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

<button on:click={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

{#if count > 10}
	<p>{count} is greater than 10</p>
{/if}

{#if ...}{/if} で囲むことで条件を満たす場合にのみ描画できる。

React だと {condition && (...)} のように書くのに比べるとわかりやすいかもしれない。

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

Part 1 / Logic / Else blocks

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

src/App.svelte
<script>
	let count = 0;

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

<button on:click={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

{#if count > 10}
	<p>{count} is greater than 10</p>
{:else}
	<p>{count} is between 0 and 10</p>
{/if}

{#...} はブロック開始タグ、{:...} はブロック継続タグ、{/...} はブロック終了タグを意味するようだ。

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

Part 1 / Logic / Else-if blocks

https://learn.svelte.dev/tutorial/else-if-blocks

src/App.svelte
<script>
	let count = 0;

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

<button on:click={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

{#if count > 10}
	<p>{count} is greater than 10</p>
{:else if count < 5}
	<p>{count} is less than 5</p>
{:else}
	<p>{count} is between 5 and 10</p>
{/if}

React では else if は地味に面倒なので嬉しいかも知れない。

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

Part 1 / Logic / Each blocks

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

src/App.svelte
<script>
	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
	let selected = colors[0];
</script>

<h1 style="color: {selected}">Pick a colour</h1>

<div>
	{#each colors as color, i}
		<button
			aria-current={selected === color}
			aria-label={color}
			style="background: {color}"
			on:click={() => selected = color}
		>{i + 1}</button>
	{/each}
</div>

<style>
	h1 {
		transition: color 0.2s;
	}

	div {
		display: grid;
		grid-template-columns: repeat(7, 1fr);
		grid-gap: 5px;
		max-width: 400px;
	}

	button {
		aspect-ratio: 1;
		border-radius: 50%;
		background: var(--color, #fff);
		transform: translate(-2px,-2px);
		filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
		transition: all 0.1s;
	}

	button[aria-current="true"] {
		transform: none;
		filter: none;
		box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
	}
</style>

{#each ...} を使うことで Array ライクなオブジェクトをマッピングできる。

Array ライクとは length プロパティを持っているオブジェクトのようだ。

React で array.map((el) => <div>{el}</div>) のようにやっていたのに比べるとわかりやすい。

{#each array as el, i} のようにしてインデックスも取得できる。

イテラブルについては [...iterable] のようにしてマッピングできる。

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

Part 1 / Logic / Keyed each blocks

https://learn.svelte.dev/tutorial/keyed-each-blocks

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

	let things = [
		{ id: 1, name: 'apple' },
		{ id: 2, name: 'banana' },
		{ id: 3, name: 'carrot' },
		{ id: 4, name: 'doughnut' },
		{ id: 5, name: 'egg' }
	];

	function handleClick() {
		things = things.slice(1);
	}
</script>

<button on:click={handleClick}>
	Remove first thing
</button>

{#each things as thing (thing.id)}
	<Thing name={thing.name} />
{/each}
src/Thing.svelte
<script>
	const emojis = {
		apple: '🍎',
		banana: '🍌',
		carrot: '🥕',
		doughnut: '🍩',
		egg: '🥚'
	};

	// the name is updated whenever the prop value changes...
	export let name;

	// ...but the "emoji" variable is fixed upon initialisation
	// of the component because it uses `const` instead of `$:`
	const emoji = emojis[name];
</script>

<p>{emoji} = {name}</p>

{#each ...} を普通に使うと要素が増えたり減ったりした場合に末尾に DOM が追加されたり最後の DOM が削除されたりする。

子コンポーネントで const を使っている場合は値が更新されずに困るケースがある。

(thing.id) のようにキーを指定することで要素を識別できるようになり、DOM が挿入されたり要素に対応する DOM が削除されるようになる。

Svelte は要素と DOM の対応管理に内部的には Map を使っているのでキーはオブジェクトであればなんでも良いが API からデータを取得することなどを考えると数値や文字列が安全。

{#each ...} を使う場合は可能な時はいつでもキーを指定した方が効率が良さそうだ、不具合も減りそう。

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

Part 1 / Logic / Await blocks

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

src/App.svelte
<script>
	import { getRandomNumber } from './utils.js';

	let promise = getRandomNumber();

	function handleClick() {
		promise = getRandomNumber();
	}
</script>

<button on:click={handleClick}>
	generate random number
</button>

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
src/utils.js
export async function getRandomNumber() {
    // Fetch a random number between 0 and 100
    // (with a delay, so that we can see it)
    const res = await fetch('/random-number');

    if (res.ok) {
        return await res.text();
    } else {
        // Sometimes the API will fail!
        throw new Error('Request failed');
    }
}

{#await ...} ブロックを使うと Promise 解決前、解決後、エラー時の表示内容を指定できる。

これは便利。

{:catch ...} は省略できる。

Promise 解決前の表示内容が無ければ下記のように書くこともできる。

コード例
{#await promise then number}
	<p>The number is {number}</p>
{/await}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 1 / Events / DOM events

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

src/App.svelte
<script>
	let m = { x: 0, y: 0 };

	function handleMove(event) {
		m.x = event.clientX;
		m.y = event.clientY;
	}
</script>

<div on:pointermove={handleMove}>
	The pointer is at {m.x} x {m.y}
</div>

<style>
	div {
		position: fixed;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		padding: 1rem;
	}
</style>

on:pointermove のように書く、これは直感的で良い。

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

Part 1 / Events / Inline handlers

https://learn.svelte.dev/tutorial/inline-handlers

src/App.svelte
<script>
	let m = { x: 0, y: 0 };
</script>

<div
	on:pointermove={(e) => {
		m = { x: e.clientX, y: e.clientY };
	}}
>
	The pointer is at {m.x} x {m.y}
</div>

<style>
	div {
		position: fixed;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		padding: 1rem;
	}
</style>

イベントハンドラはインラインで書くこともできる。

エディタによっては警告される場合があるが Svelte がよろしくやってくれるので無視しても大丈夫とのこと。

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

Part 1 / Events / Component events

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

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

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Inner on:message={handleMessage} />
src/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>

createEventDispatcher() を使うことで親コンポーネントにイベントを通知できる。

パラメーターは event.detail に格納されるようだ。

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

Svelte のチュートリアルが動かない

昨日まで動いていたのになんでだろう?

キャッシュやクッキーを削除してみたけどダメだった。

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

Part 1 / Events / Event forwarding

https://learn.svelte.dev/tutorial/event-forwarding

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

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Outer on:message={handleMessage} />
src/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>
src/Outer.svelte
<script>
	import Inner from './Inner.svelte';
</script>

<Inner on:message />

コンポーネントイベントは DOM イベントとは異なりバブルされない。

これはより親より上のコンポーネントにイベントを通知したい場合は通知の転送が必要になることを意味する。

これを簡単にするために <Inner on:message /> の省略記法が使える。

下記をコーディングしたのと同じ効果が得られる。

src/Outer.svelte
<script>
	import Inner from './Inner.svelte';
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	function forward(event) {
		dispatch('message', event.detail);
	}
</script>

<Inner on:message={forward}/>
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 1 / Events / DOM event forwarding

https://learn.svelte.dev/tutorial/dom-event-forwarding

src/App.svelte
<script>
	import BigRedButton from './BigRedButton.svelte';
	import horn from './horn.mp3';

	const audio = new Audio();
	audio.src = horn;

	function handleClick() {
		audio.play();
	}
</script>

<BigRedButton on:click={handleClick} />
src/BigRedButton.svelte
<button on:click>
	Push
</button>

<style>
	button {
		font-size: 1.4em;
		width: 6em;
		height: 6em;
		border-radius: 50%;
		background: radial-gradient(circle at 25% 25%, hsl(0, 100%, 50%) 0, hsl(0, 100%, 40%) 100%);
		box-shadow: 0 8px 0 hsl(0, 100%, 30%), 2px 12px 10px rgba(0,0,0,.35);
		color: hsl(0, 100%, 30%);
		text-shadow: -1px -1px 2px rgba(0,0,0,0.3), 1px 1px 2px rgba(255,255,255,0.4);
		text-transform: uppercase;
		letter-spacing: 0.05em;
		transform: translate(0, -8px);
		transition: all 0.2s;
	}

	button:active {
		transform: translate(0, -2px);
		box-shadow: 0 2px 0 hsl(0, 100%, 30%), 2px 6px 10px rgba(0,0,0,.35);
	}
</style>

コンポーネントイベントだけではなく DOM イベント(on:click など)も転送できる。

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

Part 1 / Bindings / Numeric inputs

https://learn.svelte.dev/tutorial/numeric-inputs

src/App.svelte
<script>
	let a = 1;
	let b = 2;
</script>

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

<label>
	<input type="number" bind:value={b} min="0" max="10" />
	<input type="range" bind:value={b} min="0" max="10" />
</label>

<p>{a} + {b} = {a + b}</p>

基本は string だが、input の type が number / range の場合に文字列 → 数値へ自動的に変換してくれる。

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

Part 1 / Bindings / Checkbox inputs

https://learn.svelte.dev/tutorial/checkbox-inputs

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

<label>
	<input type="checkbox" bind:checked={yes} />
	Yes! Send me regular email spam
</label>

{#if yes}
	<p>
		Thank you. We will bombard your inbox and sell
		your personal details.
	</p>
{:else}
	<p>
		You must opt in to continue. If you're not
		paying, you're the product.
	</p>
{/if}

<button disabled={!yes}>Subscribe</button>

チェックボックスの場合は value ではなく checked に bind: を前置する。

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

Part 1 / Bindings / Select bindings

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

src/App.svelte
<script>
	let questions = [
		{
			id: 1,
			text: `Where did you go to school?`
		},
		{
			id: 2,
			text: `What is your mother's name?`
		},
		{
			id: 3,
			text: `What is another personal fact that an attacker could easily find with Google?`
		}
	];

	let selected;

	let answer = '';

	function handleSubmit() {
		alert(
			`answered question ${selected.id} (${selected.text}) with "${answer}"`
		);
	}
</script>

<h2>Insecurity questions</h2>

<form on:submit|preventDefault={handleSubmit}>
	<select
  	bind:value={selected}
		on:change={() => (answer = '')}
	>
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>

	<input bind:value={answer} />

	<button disabled={!answer} type="submit">
		Submit
	</button>
</form>

<p>
	selected question {selected
		? selected.id
		: '[waiting...]'}
</p>

select の場合は option ではなく select に bind:value する。

option のバインドには {#each ...} を使い、value やテキストを設定する。

value には string だけではなくオブジェクトも使用できる。

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

Part 1 / Bindings / Group inputs

https://learn.svelte.dev/tutorial/group-inputs

src/App.svelte
<script>
	let scoops = 1;
	let flavours = [];

	const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
</script>

<h2>Size</h2>

{#each [1, 2, 3] as number}
	<label>
		<input
			type="radio"
			name="scoops"
			value={number}
			bind:group={scoops}
		/>

		{number} {number === 1 ? 'scoop' : 'scoops'}
	</label>
{/each}

<h2>Flavours</h2>

{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
	<label>
		<input
			type="checkbox"
			name="flavours"
			value={flavour}
			bind:group={flavours}
		/>

		{flavour}
	</label>
{/each}

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {formatter.format(flavours)}
	</p>
{/if}

ラジオボタンの場合は bind:group を使って選択された値を取得できる。

チェックボックスの場合は bind:group を使って選択された値の配列を取得できる。

name 属性は同じではなくても大丈夫そうだがなるべく合わせておいた方が良いかも。

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

Part 1 / Bindings / Select multiple

https://learn.svelte.dev/tutorial/multiple-select-bindings

src/App.svelte
<script>
	let scoops = 1;
	let flavours = [];

	const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
</script>

<h2>Size</h2>

{#each [1, 2, 3] as number}
	<label>
		<input
			type="radio"
			name="scoops"
			value={number}
			bind:group={scoops}
		/>

		{number} {number === 1 ? 'scoop' : 'scoops'}
	</label>
{/each}

<h2>Flavours</h2>

<select multiple bind:value={flavours}>
	{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
		<option>{flavour}</option>
	{/each}
</select>

{#if flavours.length === 0}
	<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
	<p>Can't order more flavours than scoops!</p>
{:else}
	<p>
		You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
		of {formatter.format(flavours)}
	</p>
{/if}

めったに使うことはないけど multiple な select タグの場合は bind:group ではなく bind:value を使う。

キーボードだけで複数選択する場合はどうすれば良いんだろう。

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

Part 1 / Bindings / Textarea inputs

https://learn.svelte.dev/tutorial/textarea-inputs

src/App.svelte
<script>
	import { marked } from 'marked';
	let value = `Some words are *italic*, some are **bold**\n\n- lists\n- are\n- cool`;
</script>

<div class="grid">
	input
	<textarea bind:value></textarea>

	output
	<div>{@html marked(value)}</div>
</div>

<style>
	.grid {
		display: grid;
		grid-template-columns: 5em 1fr;
		grid-template-rows: 1fr 1fr;
		grid-gap: 1em;
		height: 100%;
	}

	textarea {
		flex: 1;
		resize: none;
	}
</style>

テキストエリアにバインドするには bind:value={value} を使う。

bind:value={value}bind:value に省略できる。

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

Part 1 / Lifecycle / onMount

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

src/App.svelte
<script>
	import { onMount } from 'svelte';
	import { paint } from './gradient.js';

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

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

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

<canvas
	width={32}
	height={32}
/>

<style>
	canvas {
		position: fixed;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		background-color: #666;
		mask: url(./svelte-logo-mask.svg) 50% 50% no-repeat;
		mask-size: 60vmin;
		-webkit-mask: url(./svelte-logo-mask.svg) 50% 50% no-repeat;
		-webkit-mask-size: 60vmin;
	}
</style>
src/gradient.js
export function paint(context, t) {
	const { width, height } = context.canvas;
	const imageData = context.getImageData(0, 0, width, height);

	for (let p = 0; p < imageData.data.length; p += 4) {
		const i = p / 4;
		const x = i % width;
		const y = (i / width) >>> 0;

		const red = 64 + (128 * x) / width + 64 * Math.sin(t / 1000);
		const green = 64 + (128 * y) / height + 64 * Math.cos(t / 1000);
		const blue = 128;

		imageData.data[p + 0] = red;
		imageData.data[p + 1] = green;
		imageData.data[p + 2] = blue;
		imageData.data[p + 3] = 255;
	}

	context.putImageData(imageData, 0, 0);
}

このチュートリアルは色々と学びが多い。

まず onMount() を使うことで DOM が描画された後にコードを 1 回だけ実行できる。

DOM 取得に document.querySelector() が使われているが、これはあまり良い方法ではなく後からもっと良い方法を学べるようだ。

onMount() で実行するコードの最後で関数を返すことで DOM 破棄時にクリーンアップ処理を実行できる。

多分 onMount() は複数実行できるので関係するコードだけを細かくまとめた方が良さそうだ。

React の useEffect() で第 2 引数を [] にする場合の使い方とほぼ同じと考えて良さそうだ。

となると setInterval() とかを使う時に気をつけないとな。

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

Part 1 / Lifecycle / beforeUpdate and afterUpdate

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

src/App.svelte(長いので冒頭のみ)
<script>
	import Eliza from 'elizabot';
	import {
		beforeUpdate,
		afterUpdate
	} from 'svelte';

	let div;
	let autoscroll = false;

	beforeUpdate(() => {
		if (div) {
			const scrollableDistance = div.scrollHeight - div.offsetHeight;
			autoscroll = div.scrollTop > scrollableDistance - 20;
		}
	});

	afterUpdate(() => {
		if (autoscroll) {
			div.scrollTo(0, div.scrollHeight);
		}
	});

        // ...
</script>

beforeUpdate() と afterUpdate() を使うことで DOM 更新前後にコードを実行できる。

スクロールポジションの更新などに重宝するようだ。

beforeUpdate() はマウント前(onMount() が実行される前)に実行されるので DOM などに依存している場合は注意が必要。

ちなみに bind:this を使うことで変数に DOM をバインドできるようだ。

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

Part 1 / Lifecycle / tick

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

src/App.svelte
<script>
	import { tick } from 'svelte';
	
	let text = `Select some text and hit the tab key to toggle uppercase`;

	async function handleKeydown(event) {
		if (event.key !== 'Tab') return;

		event.preventDefault();

		const { selectionStart, selectionEnd, value } = this;
		const selection = value.slice(selectionStart, selectionEnd);

		const replacement = /[a-z]/.test(selection)
			? selection.toUpperCase()
			: selection.toLowerCase();

		text =
			value.slice(0, selectionStart) +
			replacement +
			value.slice(selectionEnd);

		// this has no effect, because the DOM hasn't updated yet
		await tick();
		this.selectionStart = selectionStart;
		this.selectionEnd = selectionEnd;
	}
</script>

<textarea
	value={text}
	on:keydown={handleKeydown}
/>

<style>
	textarea {
		width: 100%;
		height: 100%;
		resize: none;
	}
</style>

tick() は他のライフサイクル系の関数とは異なり、いつでも呼び出すことができる。

tick() は Promise を返し、Promise はコンポーネントの状態が DOM に反映された時に解決される。

コンポーネントの状態が DOM に反映されている時は即座に解決される。

Svelte ではコンポーネントの状態が変化する都度、DOM に反映される訳ではないらしい。

パフォーマンスの観点からある程度まとめてバッチで処理されるようだ。

上記のコードではテキストエリアの selectionStart と selectionEnd を更新する前に tick() を呼び出して DOM 更新を待つことで TAB キー押下時にカーソルが末尾にジャンプしないようにしている。

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

Part 1 / Stores / Writable stores

https://learn.svelte.dev/tutorial/writable-stores

src/App.svelte
<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';

	let count_value;

	count.subscribe((value) => {
		count_value = value;
	});
</script>

<h1>The count is {count_value}</h1>

<Incrementer />
<Decrementer />
<Resetter />
src/Decrementer.svelte
<script>
	import { count } from './stores.js';

	function decrement() {
		count.update((n) => n - 1);
	}
</script>

<button on:click={decrement}>
	-
</button>
src/Incrementer.svelte
<script>
	import { count } from './stores.js';

	function increment() {
		count.update((n) => n + 1);
	}
</script>

<button on:click={increment}>
	+
</button>
src/Resetter.svelte
<script>
	import { count } from './stores.js';

	function reset() {
		count.set(0);
	}
</script>

<button on:click={reset}>
	reset
</button>
src/stores.js
import { writable } from 'svelte/store';

export const count = writable(0);

Store とは subscribe() メソッドを持つオブジェクトのこと。

Store を使うことで階層の異なるコンポーネント間で状態を共有できる。

React でいう所の Context に似ている。

Store を使うことで親から子へ渡すプロパティを削減できそう。

ただ乱用すると依存関係が無茶苦茶になりそうなの用法と用量を正しく見極める必要がありそう。

Svelte では writable と呼ばれるストアが用意されていて subscribe() に加えて set() と update() の 2 つのメソッドを持つ。

set() は値を受け取り、update() は更新関数を受け取る。

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

Part 1 / Stores / Auto-subscriptions

https://learn.svelte.dev/tutorial/auto-subscriptions

src/App.svelte
<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

<Incrementer />
<Decrementer />
<Resetter />

subscribe() の戻り値は unsubscribe() でこれを onDestroy() で呼び出すことでメモリリークを防ぐことができる。

これを地道に書いても良いが Svelte ではストアに $ を前置することで省略できる。

$ の前置は HTML 部分だけではなくて script 部分でも使うことができる。

代償として $ から始まる変数を使うことはできないがこれはとても便利だ。

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

Part 1 / Stores / Readable stores

https://learn.svelte.dev/tutorial/readable-stores

src/App.svelte
<script>
	import { time } from './stores.js';

	const formatter = new Intl.DateTimeFormat(
		'en',
		{
			hour12: true,
			hour: 'numeric',
			minute: '2-digit',
			second: '2-digit'
		}
	);
</script>

<h1>The time is {formatter.format($time)}</h1>
src/stores.js
import { readable } from 'svelte/store';

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

	return function stop() {
		clearInterval(interval);
	};
});

必ずしも書き込みを必要としないストアもある。

例としてはマウス座標やモバイルデバイスの緯度軽度などがある。

このようなストアには readable ストアが適している。

readable ストアは 2 つの引数を持つ。

1 つ目の引数は初期値である。

2 つ目の引数は 1 引数関数であり、start() 関数と呼ばれる。

start() 関数の 第 1 引数は set() コールバックである。

start() 関数は最初のサブスクライバーが subscribe() した時に呼び出される。

start() 関数は stop() 関数を返し、stop() 関数は最後のサブスクライバーが unsubscribe() した時に呼び出される。

writable ストアほどは使わなそうだが、readble ストアも使い所によっては便利そうだ。

パッと思いつくのは API への GET リクエストだろうか。

React の SWR のようにもっと良い方法があると良いけど。

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

10/2 (月) はここから

今日は 30 分くらい学んでいこう。

前回は追加で 30 分ほど学んだので学習時間に足しておこう。

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

Part 1 / Stores / Derived stores

https://learn.svelte.dev/tutorial/derived-stores

src/App.svelte
<script>
	import { time, elapsed } from './stores.js';

	const formatter = new Intl.DateTimeFormat(
		'en',
		{
			hour12: true,
			hour: 'numeric',
			minute: '2-digit',
			second: '2-digit'
		}
	);
</script>

<h1>The time is {formatter.format($time)}</h1>

<p>
	This page has been open for
	{$elapsed}
	{$elapsed === 1 ? 'second' : 'seconds'}
</p>
src/stores.js
import { readable, derived } from 'svelte/store';

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

	return function stop() {
		clearInterval(interval);
	};
});

const start = new Date();

export const elapsed = derived(
	time,
	($time) => Math.round(($time - start) / 1000)
);

derived を使ってあるストアから派生するストアを作ることができる。

ストアに共通のロジックを埋め込みたい場合に便利そうだ。

第 1 引数は依存するストア、複数の場合は配列にする。

第 2 引数は加工する関数でその引数たちは下記の通り。

  1. ストアの値、変数名は $ から始める、複数の場合は配列にする。
  2. set() 関数、setTimeout() や await を使う場合に便利。
  3. update() 関数、set() 関数と同様。

set() や update() を使う場合は unsubscriber を返すことができる。

第 3 引数は初期値。

詳しくはこのページに書いてある。

https://svelte.dev/docs/svelte-store#derived

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

Part 1 / Stores / Custom stores

https://learn.svelte.dev/tutorial/custom-stores

src/App.svelte
<script>
	import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>

<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>
src/stores.js
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();

ストアとは subscribe() メソッドを持つオブジェクトなので簡単に実装できる。

一から実装しなくても writable などをベースに作ることも可能。

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

Part 1 / Stores / Store bindings

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

src/App.svelte
<script>
	import { name, greeting } from './stores.js';
</script>

<h1>{$greeting}</h1>
<input bind:value={$name} />

<button on:click={() => $name += '!'}>
	Add exclamation mark!
</button>
src/stores.js
import { writable, derived } from 'svelte/store';

export const name = writable('world');

export const greeting = derived(name, ($name) => `Hello ${$name}!`);

ストアは bind:value などにバインドできる、バインドする時は $ の前置が必要。

$name += '!' のように代入することも可能、こちらは name.set($name + '!') と等価。

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

Part 1: Basic Svelte が終わった

このまま Part 2 に入りたいけど少し時間をとって Part 1 を振り返ろう。

  • Introduction
  • Reactivity
  • Props
  • Logic
  • Events
  • Bindings
  • Lifecycle
  • Stores

長くなりそうだからスクラップを分けた方が良さそうだ。

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

Introduction のまとめ

https://zenn.dev/link/comments/18b44817ec1813

Your First Component

  • Svelte では .svelte ファイルがコンポーネントになる。
  • .svelte ファイルには HTML を記述する。
  • script タグ内のコードがコンポーネント生成時に実行される。
  • script タグ内のトップレベルで宣言した変数は状態になる。
  • HTML では {} を使って状態にアクセスできる。

Dynamic attributes

  • タグの属性やコンポーネントのプロパティに src={src} のようにバインドできる。
  • src={src}{src} に省略できる。
  • "My name is {name}" のように書ける、{'My name is' + name} のように書かなくても良い。

Styling

  • .svelte ファイル内で style タグを使って外観を調整できる。
  • style タグの影響はコンポーネント内だけに及ぶ。

Nested components

  • import 文を使って他ファイルのコンポーネントを読み込める。
  • 親コンポーネントの style の影響は子コンポーネントには及ばない。

HTML tags

  • {...} は何もしなくてもエスケープされる。
  • エスケープを無効にするには {@html ...} と書く。
  • XSS に注意。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Reactivity のまとめ

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

Assignments

  • イベントハンドラを設定するには on:click を使用する。
  • 間違えて onclick と書いたらどうなるんだろう。

Declarations

  • $: を使うことで状態に依存する計算結果を宣言できる。
  • $: は Svelte 特有の記法に見えるが一応有効な JavaScript コード。

Statements

  • $: は式だけではなくて文も実行できる。
  • $: の文は依存する状態が更新される度に実行される。
  • たしか必ず状態が含まれている必要があるはず。

Updating arrays and objects

  • 配列は push() や shift() を使っても状態は更新されないがその後に代入すれば更新される。
  • おすすめはスプレッド構文を使う方法、例:numbers = [...numbers, numbers.length + 1];
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Props のまとめ

https://zenn.dev/link/comments/37bac9a1da54ba

Declaring props

  • export された変数はプロパティになる。
  • プロパティには外部のコンポーネントから値を指定できる。

Default values

  • export された変数に初期値を与えるとプロパティのデフォルト値となる。
  • 何も指定しないと多分 undefined になる。

Spread props

  • React のように <PackageInfo {...pkg} /> と書ける。
  • pkg はオブジェクトである必要がある。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Logic

https://zenn.dev/link/comments/1b513a5eafbed9

If blocks

  • {#if ...}{/if} で囲むことで条件を満たす場合にのみ描画できる。
  • 覚えることは増えるが React で {condition && (...)} のように書くのに比べるとわかりやすい。

Else blocks

  • {:else} を使うと条件が満たされない場合にのみ描画できる。
  • {#...} はブロック開始タグ、{:...} はブロック継続タグ、{/...} はブロック終了タグを意味する。

Else-if blocks

  • JavaScript のように {:else if ...} も使える。
  • React では else if は地味に面倒なので嬉しい。

Each blocks

  • {#each colors as color, i} ... {/each} のように Array ライクなオブジェクトをマッピングできる。
  • Array ライクとは length プロパティを持っているオブジェクトのこと。
  • React で array.map((el) => <div>{el}</div>) のようにやっていたのに比べるとわかりやすい。
  • イテラブルについては [...iterable] のようにしてマッピングできる。

Keyed each blocks

  • {#each ...} を普通に使うと要素が増えたり減ったりした場合に末尾に DOM が追加されたり最後の DOM が削除されたりする。
  • (thing.id) のようにキーを指定することで要素を識別できるようになり、DOM が挿入されたり要素に対応する DOM が削除されたりするようになる。
  • キーはオブジェクトであれば何でも良いが数値や文字列が安全。
  • 可能な時はいつでもキーを指定した方が良さそう。

Await blocks

  • {#await promise} {:then number} {:catch error} {/await} のように書いてPromise 解決前、解決後、エラー時の表示内容を指定できる。
  • {:catch} は省略できる、{:then} も省略できるのかな。
  • 解決前の表示内容が無ければ {#await promise then number} のようにも書ける。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Events

https://zenn.dev/link/comments/44d93ded833d24

DOM events

  • タグやコンポーネントに on:pointermove でバインドしてイベントハンドラを登録できる。
  • マジで onpointermove にバインドしたらどうなるんだろう?

Inline handlers

  • イベントハンドラは script 内に定義した関数だけではなく匿名関数でも良い。
  • 名前を考えなくても良いから良いね。
  • 処理が多い場合は関数を定義した方が良さそう。
  • エディタによっては警告される場合があるが無視しても大丈夫。

Event modifiers

  • on:click|once のようにすることで 1 回だけ実行できる。
  • preventDefault が一番使いそう。

Component events

  • コンポーネントにイベントハンドラを登録できるようにするには createEventDispatcher をインポートして使う。
  • イベントハンドラ側では event.detail にアクセスしてパラメーターを取得できる。

Event forwarding

  • コンポーネントイベントは DOM イベントとは異なりバブルされない。
  • 親より上のコンポーネントにイベントを通知したい場合は通知の転送が必要になる。
  • <Inner on:message /> のように書くと Svelte が残りの部分を書いてくれる。

DOM event forwarding

  • DOM でも on:click などのパラメーターを省略することでイベントを転送できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Bindings のまとめ

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

Text inputs

  • Svelte ではバインディングは親 → 子の単方向だが bind: を使うことで双方向にできる。
  • 内部的には on:input を使って状態を更新しているようだ。

Numeric inputs

  • バインディングには基本的に string が使われる。
  • input の type が number / range の場合に文字列 → 数値へ自動的に変換してくれる。

Checkbox inputs

  • チェックボックスの場合は value ではなく checked に bind: を前置する。
  • バインドする変数には boolean を使う。

Select bindings

  • select の場合は option ではなく select に bind:value する。
  • option のバインドには {#each ...} を使い、value やテキストを設定する。
  • value には string だけではなくオブジェクトも使用できる。

Group inputs

  • ラジオボタンの場合は bind:group を使って選択された値を取得できる。
  • チェックボックスの場合は bind:group を使って選択された値の配列を取得できる。
  • name 属性は同じではなくても大丈夫そうだがなるべく合わせておいた方が良い。

Select multiple

  • multiple な select タグの場合は bind:group ではなく bind:value を使う。
  • キーボードだけで複数選択する場合はどうすれば良いんだろう?

Textarea inputs

  • テキストエリアにバインドするには bind:value={value} を使う。
  • bind:value={value}bind:value に省略できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Lifecycle のまとめ

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

onMount

  • onMount() を使うことで DOM が描画された後にコードを 1 回だけ実行できる。
  • onMount() で実行するコードの最後で関数を返すことで DOM 破棄時にクリーンアップ処理を実行できる。
  • 多分 onMount() は複数実行できるので関係するコードだけを細かくまとめた方が良さそう。
  • React の useEffect() で第 2 引数を [] にする場合の使い方とほぼ同じと考えて良さそう。
  • setInterval() とかを使う時に気をつけた方が良さそう。

beforeUpdate and afterUpdate

  • beforeUpdate() と afterUpdate() を使うことで DOM 更新前後にコードを実行できる。
  • beforeUpdate() はマウント前(onMount() が実行される前)に実行されるので DOM などに依存している場合は注意が必要。
  • bind:this を使うことで変数に DOM をバインドできるようだ。

tick

  • tick() は他のライフサイクル系の関数とは異なり、いつでも呼び出すことができる。
  • tick() は Promise を返し、Promise はコンポーネントの状態が DOM に反映された時に解決される。
  • コンポーネントの状態が DOM に反映済みの場合は即座に解決される。
  • Svelte ではコンポーネントの状態が変化する都度 DOM に反映される訳ではなく、パフォーマンス向上のためある程度まとめてバッチで処理される。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Stores

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

Writable stores

  • Store とは subscribe() メソッドを持つオブジェクトのこと。
  • Store を使うことで階層の異なるコンポーネント間で状態を共有できる。
  • React でいう所の Context に似ている。
  • Store を使うことで親から子へ渡すプロパティを削減できるが、乱用すると依存関係が無茶苦茶になりそうなので適度に使用する。
  • Svelte では writable と呼ばれるストアが用意されていて subscribe() に加えて set() と update() の 2 つのメソッドを持つ。
  • set() は値を受け取り、update() は更新関数を受け取る。

Auto-subscriptions

  • subscribe() の戻り値は unsubscribe() でこれを onDestroy() で呼び出すことでメモリリークを防ぐことができる。
  • これを地道に書いても良いが Svelte ではストアに $ を前置することで省略できる。
  • $ の前置は HTML 部分だけではなくて script 部分でも使うことができる。
  • 代償として $ から始まる変数を使うことはできない。

Readable stores

  • 必ずしも書き込みを必要としないストアもあり、例としてはマウス座標やモバイルデバイスの緯度軽度などがある。
  • このようなストアには readable ストアが適している。
  • readable ストアは 2 つの引数を持ち、1 つ目の引数は初期値、2 つ目の引数は 1 引数関数で start() 関数と呼ばれる。
  • start() 関数の 第 1 引数は set() コールバックである。
  • start() 関数は最初のサブスクライバーが subscribe() した時に呼び出される。
  • start() 関数は stop() 関数を返し、stop() 関数は最後のサブスクライバーが unsubscribe() した時に呼び出される。
  • 引数なしの API への GET リクエストに利用できるかも知れない。

Derived stores

  • derived を使ってあるストアから派生するストアを作ることができる。
  • ストアに共通のロジックを埋め込みたい場合に便利そうだ。
  • 第 1 引数は依存するストア、複数の場合は配列にする。
  • 第 2 引数は加工する関数で 3 つの引数を受け取る。
  • 第 1 引数はストアの値、変数名は $ から始める、複数の場合は配列にする。
  • 第 2 引数は set() 関数、setTimeout() や await を使う場合に便利。
  • 第 3 引数は update() 関数、set() 関数と同様。
  • set() や update() を使う場合は unsubscriber を返すことができる。
  • derived の第 3 引数は初期値。

Custom stores

  • ストアとは subscribe() メソッドを持つオブジェクトなので簡単に実装できる。
  • 一から実装しなくても writable などをベースに作ることも可能。

Store bindings

  • ストアは bind:value などにバインドできる、バインドする時は $ の前置が必要。
  • $name += '!' のように代入することも可能、こちらは name.set($name + '!') と等価。
このスクラップは2023/10/07にクローズされました