Svelte 公式チュートリアル Part 2: Advanced Svelte をやっていく
このスクラップについて
このスクラップでは Svelte 公式チュートリアル Part 2: Advanced Svelte を手を動かして学んでいく過程を記録する。
前のスクラップ
10/7 (土) はここから
今日は 20〜30 分やっていこう。
Part 2 / Motion / Tweens
<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 引数で指定する。
Part 2 / Motion / Springs
<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 と似たような挙動になる。
値だけではなくオブジェクトを指定できるのがすごい。
svelte/transition ドキュメント
学習時間
30 分経過した、累計時間は 30 分。
次は下記のページから。
10/8 (日) はここから
今日は 20〜30 分学んでいこう。
Part 2 / Transitions / The transition directive
<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 でも同じようなことはできるのでどうやって使い分ければ良いのだろう?
ドキュメントはこちら。
Part 2 / Transitions / Adding parameters
<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 を使うと下からニュッと出てくる遷移を実現できる。
遷移はリバーシブル。つまり、遷移の途中で状態が変化するとその位置や透明度から元の場所に戻る。
Part 2 / Transitions / In and out
<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:
を使うことで表示と非表示の遷移を切り替えることができる。
この場合は遷移はリバーシブルにはならない。
学習時間
20 分経過した、累計時間は 50 分。
次は下記のページから。
10/9 (月) はここから
25 分くらいあるので 1〜2 ページやっていこう。
Part 2 / Transitions / Custom CSS transitions
<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 引数関数
tick よりも css を返す方がパフォーマンス的には良いようだ。
学習時間
20 分経過した、累計時間は 70 分。
次は下記のページから。
再開
もう 20〜30 分やろう。
Part 2 / Transitions / Custom JS transitions
<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
を使うことができる。
Part 2 / Transitions / Transition events
<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:introstart
や on:outroend
を使うことで遷移の開始時や終了時にコードを実行できる。
Part 2 / Transitions / Global transitions
<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
と後置する。
Part 2 / Transitions / Key blocks
<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}
を使うことで式が変化した時にノードを再生成することができる。
式が変化した時に遷移を実行したい場合に便利。
学習時間
20 分経過した、累計時間は 90 分。
次は下記のページから。
10/10 (火) はここから
午前は 20 分くらい学ぼう。
Part 2 / Transitions / Deferred transitions
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}
`
};
}
});
<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()
の遷移が実行される。
10/11 (水) はここから
15 分くらい学んでいこう。
Part 2 / Animations / The animate directive
<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 ブロックの要素に変化がある時に実行される。
学習時間
15 分経過した、累計時間は 125 分。
次は下記のページから。
10 分くらい
スキマ時間があるので 1 ページだけやっていこう。
Part 2 / Actions / The use directive
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.js';
// ...
</script>
<div class="menu" use:trapFocus>
focusable()[0]?.focus();
node.addEventListener('keydown', handleKeydown);
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
previous?.focus();
}
};
アクションとはノードレベルのライフサイクル関数のことらしい。
他にもライフサイクル関数もあるが違いは何なのだろう?
戻り値を destroy プロパティを持つオブジェクトにすることでクリーンアップを実行できる。
10/13 (金) はここから
10 分くらいやっていこう。
Part 2 / Actions / Adding parameters
<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 プロパティを指定することでパラメーターに変更があった時にコードを実行できる。
学習時間
10 分経過した、前回は 15 分、累計時間は 150 分。
次は下記のページから。
10/15 (日) はここから
今日は 20〜30 分やっていこう。
Part 2/ Advanced bindings / Contenteditable bindings
<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 のバインディングが可能になる。
Part 2/ Advanced bindings / Each block bindings
{#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 属性にバインディングすると状態の置き換えではなく書き換えが行われる。
状態の置き換えの方が好ましい場合はイベントハンドラーを使った方が良い。
Part 2/ Advanced bindings / Media elements
<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 の読み込み専用プロパティが利用可能。
Part 2/ Advanced bindings / Dimensions
<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 などの要素を含めないノードには対応していないので、そのような場合はラッパーエレメントを作成する。
学習時間
30 分経過した、累計時間は 180 分。
次は下記のページから。
10/17 (火) はここから
今日は 20 分くらい学んでいく。
Part 2/ Advanced bindings / This
<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
みたいな型を付ける必要があるのかな?
Part 2/ Advanced bindings / Component bindings
<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}
/>
<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
を使ってバインディングできる。
便利だが多用するとデータがあちこちに散らばって管理しにくくなる。
基本的にはストアかイベントを使った方が良さそう。
Part 2/ Advanced bindings / Binding to component instances
export let color;
export let size;
export function clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
<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>
<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
を使って取得できる。
インスタンスを通じてエクスポートされたプロパティや関数にアクセスできる。
学習時間
20 分経過した、累計時間は 200 分。
次は下記のページから。
10/19 (木) はここから
今日は 15 分くらい学んでいこう。
Part 2/ Classes and styles / The class directive
<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 : ''}"
のように書くこともできるがクラスディレクティブを使った方が楽。
Part 2/ Classes and styles / Shorthand class directive
<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
に省略できる。
クラス名と変数名はなるべく同じにしておいた方が良いのかな?
Part 2/ Classes and styles / The style directive
<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 ディレクティブも複数設けることができる。
Part 2/ Classes and styles / Component styles
<script>
import Box from './Box.svelte';
</script>
<div class="boxes">
<Box --color="red" />
<Box --color="green" />
<Box --color="blue" />
</div>
<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.
学習時間
20 分経過した、累計時間は 220 分。
次は下記のページから。
10/20 (金) はここから
今日は 30 分くらい学べそうだ。
Part 2 / Component composition / Slots
<div class="card ">
<slot />
</div>
<Card>
<span>Patrick BATEMAN</span>
<span>Vice President</span>
</Card>
<slot>
要素を使うことでコンポーネントの子要素を表示できる。
Part 2 / Component composition / Named slots
<div class="card">
<header>
<slot name="telephone" />
<slot name="company" />
</header>
<slot />
<footer>
<slot name="address" />
</footer>
</div>
<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 & 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 の方にスタイルを書く必要がある。
裏技として注入される側に下記のように書くこともできる。
<style>
/* ... */
.card :global(small) {
display: block;
font-size: 0.6em;
text-align: right;
}
</style>
Part 2 / Component composition / Slot fallbacks
<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>
内に子要素を書いておくと未指定の場合に表示されるようになる。
Part 2 / Component composition / Slot props
<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>
<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.
学習時間
昨日は 30 分学んだ、累計時間は 250 分。
今日は下記のページから。
10/21 (土) はここから
今日は 20 分くらい学んでいこう。
Part 2 / Component composition / Checking for slot content
{#if $$slots.header}
<div class="header">
<slot name="header"/>
</div>
{/if}
スロットにコンテンツが注入されているかどうかでコンポーネントの表示を変えたい場合は $$slots
を使う。
header スロットにコンテンツが注入されていない場合 $$slots.header
は undefined になる。
Part 2 / Context API / setContext and getContext
<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>
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();
});
}
getContext('canvas').addItem(draw)
このページは色々と学ぶことが多い。
まず getContext() と setContext() を使って親コンポーネントから子孫コンポーネントにデータや関数を渡せる。
乱用は禁物だが、プロパティやイベントハンドラでは大変な時には便利そうだ。
また、{#each Array(12) as _, c}
というコーディングの時に書き方は便利そうだ。
onMount() がトップレベル以外でも呼び出し可能なことも興味深い。
ただ、addItem() がトップレベルで呼び出されているのでよく考えたら一緒なのか。
Canvas API を使っているのに App.svelte では宣言的に書けているのがすごいなーと思う。
学習時間
20 分経過した、累計時間は 270 分。
次回は下記のページから。
10/23 (月) はここから
今日は 30 分くらい学んでいこう。
Part 2 / Special elements/ svelte:self
<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>
を使う。
Part 2 / Special elements / svelte:component
<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 以外のプロパティも渡せるようだ。
Part 2 / Special elements / svelte:element
<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><{selected}></code> element
</svelte:element>
<svelte:component>
と同様に変数に格納された h1 などの文字列を HTML タグとして描画したい場合には <svelte:element>
を使用する。
こちらも a タグなどの場合は href が使えそうだ。
ただよく考えるとタグ固有の属性を指定する場合は <#if ...>
が必要になりそうだ。
Part 2 / Special elements / svelte:window
<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 などのモディファイヤーも使うことができる。
Part 2 / Special elements / svelte:window bindings
<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 以外は読み込み専用。
Part 2 / Special elements / svelte:body
<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 要素にイベントハンドラなどを設定できる。
Part 2 / Special elements / svelte:document
<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 などを設定する時に便利、初めて使ったなコレ。
学習時間
30 分経過した、累計時間は 300 分。
次回は下記のページから。
10/24 (火) はここから
今日は 30 分くらい学んでいこう。
Part 2 / Special elements / svelte:head
<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 タグを動的に変更することができる。
Part 2 / Special elements / svelte:options
<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>
<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
などいくつかのオプションがある。
Part 2 / Special elements / svelte:fragment
<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 と同じだ。
学習時間
30 分経過した、累計時間は 330 分。
次回は下記のページから。
10/25 (水) はここから
今日は 20 分くらい学んでいこう。
Part 2 / Module context / Sharing code
<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 回だけ実行される。
Part 2 / Module context / Exports
<script context="module">
let current;
export function stopAll() {
current?.pause();
}
</script>
<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 はコンポーネント本体に決まっているので使うことはできない。
学習時間
20 分経過した、累計時間は 350 分。
次回は下記のページから。
10/26 (木) はここから
今日は 20 分くらい学んでいこう。
Part 2 / Miscellaneous / The @debug tag
<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 セキュリティ制限のため何も表示されない。
Part 2 のチュートリアルが完了した
最後のページはおめでとうページだった。
これでようやく SvelteKit のチュートリアルに入れる。
その前にまとめをやっていこう。
- Motion
- Transitions
- Animations
- Actions
- Advanced bindings
- Classes and styles
- Component composition
- Context API
- Special elements
- Module context
- Miscellaneous
Motion まとめ
Tweens
- tween = between で補完するものを意味する。
- writable ストアの代わりに tweened ストアを使うことで set() や update() を呼び出した時に指定値に即座に変化せず、現在値から指定値までゆっくり変化していく。
どのように変化するかについては初期化時または set() / update() 時にオプションとして指定できる。 - オプションは第 2 引数で指定する。
Springs
- Springs とはバネのこと、バネのように収束していく動きを再現するのに便利。
- stiffness と damping の 2 つのオプションを指定できる。
- stiffness とは剛性を意味し、低いとビヨーンと伸びやすくなる。
- damping は減衰を意味し、低いとブランブランしやすくなる。
- stiffness を低くして damping を高くすると tween と似たような挙動になる。
- 値だけではなくオブジェクトや配列を指定できる。
Transitions まとめ
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:introstart
やon:outroend
を使うことで遷移の開始時や終了時にコードを実行できる。 -
on:introend
とon: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() の遷移が実行される。
学習時間
20 分経過した、累計時間は 370 分。
次回は Animations のまとめから。
10/27 (金) はここから
今日も 20 分くらいやっていこう。
Animations まとめ
The animate directive
- 遷移しないノードに対して動きを適用するにはアニメーションを使う。
- flip とは first, last, invert, play の略らしい。
- ドキュメントによるとアニメーションはキーのある each ブロックの要素に変化がある時に実行される。
Actions まとめ
The use directive
- アクションとはノードレベルのライフサイクル関数のことらしい。
- 他にもライフサイクル関数もあるが違いは何なのだろう?
- 戻り値を destroy プロパティを持つオブジェクトにすることでクリーンアップを実行できる。
Adding parameters
- actions にはパラメーターを指定できる。
- 戻り値に update プロパティを指定することでパラメーターに変更があった時にコードを実行できる。
Advanced bindings まとめ
Contenteditable bindings
- contenteditable 属性が ON のタグについては textContet と innerHTML のバインディングが可能になる。
- contenteditable 属性については MDN の下記ページが詳しい。
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 を使って取得できる。
- インスタンスを通じてエクスポートされたプロパティや関数にアクセスできる。
Classes and styles まとめ
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
が関連しているので必要に応じて詳細を確認した法が良さそう。
10/30 (月) はここから
今日は 20 分くらいやっていこう。
前回
20 分くらいやったようだ。
Component composition まとめ
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 になる。
Context API まとめ
setContent and getContext
- getContext() と setContext() を使って親コンポーネントから子孫コンポーネントにデータや関数を受け渡しできる。
- 乱用は禁物だが、プロパティやイベントハンドラでは大変な時には便利そうだ。
- {#each Array(12) as _, c} というコーディングの時に書き方は便利そうだ。
- onMount() がトップレベル以外でも呼び出し可能。
Special elements まとめ
svelte:self
- コンポーネントが自分自身をインポートすることはできない。
- 再帰構造などで自分を参照するときには
<svelte:self>
を使う。
svelte:component
- 何らかの変数に代入したコンポーネントを描画したい時には
<svelte:component>
を使う。 - コンポーネントは this プロパティに渡す。
- ドキュメントによると this 以外のプロパティも渡せる。
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
などいくつかのオプションがある。
svelte:frament
- div や span の代わりに svelte:fragment を使うことで複数のノードをスロットの直接の子孫にできる。
- React の Fragment と同じような感じ。
学習時間
20 分経過した、前回も 20 分なので累計時間は 410 分。
次回は Module context のまとめから。
10/31 (火) はここから
今日は 20 分くらいやっていこう。
Module context まとめ
Exports
-
<script context="module">
内で export した変数や関数は import できる。 - default export はコンポーネント本体で決まっているので使うことはできない。
Miscellaneous まとめ
The @debug tag
-
{@debug ...}
を使うことで開発者ツールを開いている状態で到達すると停止できる。 - コールスタックやローカル変数の値を表示できる。
- 停止が必要なければ
console.log()
を使っても良い。 - iframe 内ではセキュリティ制限のため何も表示されない。
完了!
5 分経過した、累計 415 分。
Part 1 は 305 分だったので合わせると 720 分、ちょうど 12 時間だ。
次は SvelteKit について学んでいこう。
次のスクラップ