Svelte 公式チュートリアル Part 1: Basic Svelte をやっていく
このスクラップについて
下記の記事を読んで興味が湧いたので Svelte に入門してみる。
読むドキュメント
公式チュートリアルを読んで手を動かして学んでいく
Part 1 / Introduction / Your First Component
<script>
let name = 'Svelte';
</script>
<h1>Hello {name.toUpperCase()}!</h1>
script タグで括ると変数が宣言できる。
HTML 内で {}
でくくると JavaScript の式を評価できる。
Part 1 / Introduction / Dynamic Attributes
<script>
let src = '/image.gif';
let name = 'A man';
</script>
<img {src} alt="{name} dances." />
React 同様 src={src}
と書けるが {src}
と省略できる。
なんと文字列の中で {}
を使って JavaScript を評価できる。
Part 1 / Introduction / Styling
<p>This is a paragraph.</p>
<style>
p {
color: goldenrod;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
style タグを使うと CSS を使って外観を調整できる。
影響はコンポーネント内に閉じられている。
チュートリアルページでコピペしようとしたらメッセージが表示された、よくできていると敬服した。
Part 1 / Introduction / Nested components
<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
とタイプしてしまう。
Part 1 / Introduction / HTML tags
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
{@html ...}
を使うことでエスケープを無効にできる。
まったくエスケープされなくなるので XSS に注意する必要がある。
Part 1 / Introduction が終了
手を動かせるしくみがあるので楽しい。
あと内容がわかりやすいのも素晴らしい。
もう既に Svelte が好きになってしまった。
ここまでの学習時間
今日の最初の投稿時間を見ると 20 分のようだ。
9/12 (火) はここから
Part 1 / Reactivity から始める。
Part 1 / Reactivity / Assignments
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicked {count}
{count === 1 ? 'time' : 'times'}
</button>
Svelte でイベントハンドラを設定するには on:click
などを使用する。
Part 1 / Reactivity / Declarations
<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>
$:
を使うことで計算結果を保存できる。
多分良い感じに依存関係とかは解決してくれるのだろう。
Part 1 / Reactivity / Statements
<script>
let count = 0;
$: if (count >= 10) {
alert('count is dangerously high!');
count = 0;
}
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count}
{count === 1 ? 'time' : 'times'}
</button>
式だけではなく文も実行できる。
学習時間
ここまで 15 分で、累計 35 分、次は下記のページから始める。
9/15 (金) はここから
今日も 10 分くらい学ぼう。
Part 1 / Reactivity / Updating arrays and objects
<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() などを使っても更新されないので代入する必要がある。
左辺に状態の変数が含まれている必要がある。
Part 1 / Props / Declaring props
<script>
export let answer;
</script>
<p>The answer is {answer}</p>
export
を使うことで import した側から <Nested answer={42} />
のようにプロパティを指定できるようになる。
Part 1 / Props / Default values
<script>
export let answer = 'a mystery';
</script>
<p>The answer is {answer}</p>
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42} />
<Nested />
export した状態に初期値を与えることでプロパティを指定しなかった時のデフォルト値を設定できる。
学習時間
ここまで 10 分で、累計 45 分、次は下記のページから始める。
9/19 (火) はここから
5 分くらいしかないので 1 ページだけやろう。
Part 1 / Props / Spread Props
<script>
import PackageInfo from './PackageInfo.svelte';
const pkg = {
name: 'svelte',
speed: 'blazing',
version: 4,
website: 'https://svelte.dev'
};
</script>
<PackageInfo {...pkg} />
<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 と同じなので覚えやすい。
Part 1 / Logic / If blocks
<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 && (...)}
のように書くのに比べるとわかりやすいかもしれない。
学習時間
ここまで 5 分で、累計 50 分、次は下記のページから始める。
9/20 (水) はここから
今日も 10 分くらい学習しよう。
Part 1 / Logic / Else blocks
<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}
{#...}
はブロック開始タグ、{:...}
はブロック継続タグ、{/...}
はブロック終了タグを意味するようだ。
Part 1 / Logic / Else-if blocks
<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 は地味に面倒なので嬉しいかも知れない。
Part 1 / Logic / Each blocks
<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]
のようにしてマッピングできる。
学習時間
ここまで 10 分で、累計 60 分、次は下記のページから始める。
9/23 (土) はここから
今日はいつもと違って 30 分くらいのまとまった学習時間が取れそうだ。
Part 1 / Logic / Keyed each blocks
<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}
<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 ...}
を使う場合は可能な時はいつでもキーを指定した方が効率が良さそうだ、不具合も減りそう。
Part 1 / Logic / Await blocks
<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}
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}
Svelte のシンタックスハイライト
html:src/App.svelte
としておくのが良さそう。
Part 1 / Events / DOM events
<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
のように書く、これは直感的で良い。
Part 1 / Events / Inline handlers
<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 がよろしくやってくれるので無視しても大丈夫とのこと。
学習時間
ここまで 30 分で、累計 90 分、次は下記のページから始める。
9/24 (月) はここから
今日も 30 分くらい学習していこう。
Part 1 / Events / Event modifiers
<button on:click|once={() => alert('clicked')}>
Click me
</button>
on:click
などの後に once
などをつけることでよくあるパターンを処理できる。
個人的に一番使いそうなのは preventDefault
だ。
Part 1 / Events / Component events
<script>
import Inner from './Inner.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage} />
<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
に格納されるようだ。
Svelte のチュートリアルが動かない
昨日まで動いていたのになんでだろう?
キャッシュやクッキーを削除してみたけどダメだった。
Part 1 / Events / Event forwarding
<script>
import Outer from './Outer.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Outer on:message={handleMessage} />
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message />
コンポーネントイベントは DOM イベントとは異なりバブルされない。
これはより親より上のコンポーネントにイベントを通知したい場合は通知の転送が必要になることを意味する。
これを簡単にするために <Inner on:message />
の省略記法が使える。
下記をコーディングしたのと同じ効果が得られる。
<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}/>
Part 1 / Events / DOM event forwarding
<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} />
<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 など)も転送できる。
Part 1 / Bindings / Text inputs
<script>
let name = 'world';
</script>
<input bind:value={name} />
<h1>Hello {name}!</h1>
Svelte ではバインディングは親 → 子の単方向だが bind:
を使うことで双方向にできる。
内部的には on:input
を使って状態を更新しているようだ。
学習時間
ここまで 30 分で、累計 120 分、次は下記のページから始める。
9/26 (火) はここから
昨日できなかったので 10 分だけでもやろう。
動くようになっている
Svelte のチュートリアルの実行結果が動くようになっている、良かった。
Part 1 / Bindings / Numeric inputs
<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 の場合に文字列 → 数値へ自動的に変換してくれる。
Part 1 / Bindings / Checkbox inputs
<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:
を前置する。
Part 1 / Bindings / Select bindings
<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 だけではなくオブジェクトも使用できる。
学習時間
ここまで 15 分で、累計 135 分、次は下記のページから始める。
9/30 (土) はここから
60 分くらいあるのでじっくり学べそうだ。
Part 1 / Bindings / Group inputs
<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 属性は同じではなくても大丈夫そうだがなるべく合わせておいた方が良いかも。
Part 1 / Bindings / Select multiple
<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
を使う。
キーボードだけで複数選択する場合はどうすれば良いんだろう。
Part 1 / Bindings / Textarea inputs
<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
に省略できる。
Part 1 / Lifecycle / onMount
<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>
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() とかを使う時に気をつけないとな。
Part 1 / Lifecycle / beforeUpdate and afterUpdate
<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 をバインドできるようだ。
Part 1 / Lifecycle / tick
<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 キー押下時にカーソルが末尾にジャンプしないようにしている。
学習時間
ここまで 50 分で、累計 185 分、次は下記のページから始める。
Part 1 / Stores / Writable stores
<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 />
<script>
import { count } from './stores.js';
function decrement() {
count.update((n) => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>
<script>
import { count } from './stores.js';
function increment() {
count.update((n) => n + 1);
}
</script>
<button on:click={increment}>
+
</button>
<script>
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>
import { writable } from 'svelte/store';
export const count = writable(0);
Store とは subscribe() メソッドを持つオブジェクトのこと。
Store を使うことで階層の異なるコンポーネント間で状態を共有できる。
React でいう所の Context に似ている。
Store を使うことで親から子へ渡すプロパティを削減できそう。
ただ乱用すると依存関係が無茶苦茶になりそうなの用法と用量を正しく見極める必要がありそう。
Svelte では writable と呼ばれるストアが用意されていて subscribe() に加えて set() と update() の 2 つのメソッドを持つ。
set() は値を受け取り、update() は更新関数を受け取る。
Part 1 / Stores / Auto-subscriptions
<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 部分でも使うことができる。
代償として $
から始まる変数を使うことはできないがこれはとても便利だ。
Part 1 / Stores / Readable stores
<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>
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 のようにもっと良い方法があると良いけど。
10/2 (月) はここから
今日は 30 分くらい学んでいこう。
前回は追加で 30 分ほど学んだので学習時間に足しておこう。
Part 1 / Stores / Derived stores
<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>
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 引数は加工する関数でその引数たちは下記の通り。
- ストアの値、変数名は
$
から始める、複数の場合は配列にする。 - set() 関数、setTimeout() や await を使う場合に便利。
- update() 関数、set() 関数と同様。
set() や update() を使う場合は unsubscriber を返すことができる。
第 3 引数は初期値。
詳しくはこのページに書いてある。
Part 1 / Stores / Custom stores
<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>
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 などをベースに作ることも可能。
Part 1 / Stores / Store bindings
<script>
import { name, greeting } from './stores.js';
</script>
<h1>{$greeting}</h1>
<input bind:value={$name} />
<button on:click={() => $name += '!'}>
Add exclamation mark!
</button>
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 + '!')
と等価。
Part 1: Basic Svelte が終わった
このまま Part 2 に入りたいけど少し時間をとって Part 1 を振り返ろう。
- Introduction
- Reactivity
- Props
- Logic
- Events
- Bindings
- Lifecycle
- Stores
長くなりそうだからスクラップを分けた方が良さそうだ。
学習時間
ここまで 30 分で、累計 245 分、次は Introduction の振り返りから始める。
Introduction のまとめ
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 に注意。
Reactivity のまとめ
Assignments
- イベントハンドラを設定するには
on:click
を使用する。 - 間違えて
onclick
と書いたらどうなるんだろう。
Declarations
-
$:
を使うことで状態に依存する計算結果を宣言できる。 -
$:
は Svelte 特有の記法に見えるが一応有効な JavaScript コード。
Statements
-
$:
は式だけではなくて文も実行できる。 -
$:
の文は依存する状態が更新される度に実行される。 - たしか必ず状態が含まれている必要があるはず。
Updating arrays and objects
- 配列は push() や shift() を使っても状態は更新されないがその後に代入すれば更新される。
- おすすめはスプレッド構文を使う方法、例:
numbers = [...numbers, numbers.length + 1];
Props のまとめ
Declaring props
-
export
された変数はプロパティになる。 - プロパティには外部のコンポーネントから値を指定できる。
Default values
-
export
された変数に初期値を与えるとプロパティのデフォルト値となる。 - 何も指定しないと多分 undefined になる。
Spread props
- React のように
<PackageInfo {...pkg} />
と書ける。 -
pkg
はオブジェクトである必要がある。
Logic
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}
のようにも書ける。
学習時間
ここまで 30 分で、累計 275 分、次は Events の振り返りから始める。
Events
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
などのパラメーターを省略することでイベントを転送できる。
10/5 (木) はここから
今日は 20 分くらいあるので Bindings をまとめる。
Bindings のまとめ
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
に省略できる。
Lifecycle のまとめ
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 に反映される訳ではなく、パフォーマンス向上のためある程度まとめてバッチで処理される。
Stores
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 + '!')
と等価。
学習時間
ここまで 20 分だが一昨日 10 分くらいやったので累計 305 分、次のパートのスクラップを作ろう。
次のスクラップ