Skip to main content
Basic Svelte
Introduction
Reactivity
Props
Logic
Events
Bindings
Classes and styles
Actions
Transitions
Advanced Svelte
Advanced reactivity
Reusing content
Motion
Advanced bindings
Advanced transitions
Context API
Special elements
<script module>
Next steps
Basic SvelteKit
Introduction
Routing
Loading data
Headers and cookies
Shared modules
Forms
API routes
$app/state
Errors and redirects
Advanced SvelteKit
Hooks
Page options
Link options
Advanced routing
Advanced loading
Environment variables
Conclusion

ユーザーはいたずら好きな集団であり、隙あらばあらゆる種類の無意味なデータを送信しようとします。彼らが混乱を引き起こすのを防ぐには、フォームデータを検証することが重要です。

第一防衛ラインは、ブラウザに組み込まれたフォームバリデーション(built-in form validation)で、これによって、例えば <input> を必須項目としてマークすることが簡単に行えます。

src/routes/+page
<form method="POST" action="?/create">
	<label>
		add a todo
		<input
			name="description"
			autocomplete="off"
			required
		/>
	</label>
</form>

<input> を空にしたまま Enter を押してみてください。

この種類のバリデーションは役に立ちますが、十分ではありません。<input> の属性では表現できないバリデーションルール (例えば一意性) もありますし、いずれの場合においても、ユーザーがエリートハッカーなら、ブラウザのデベロッパーツールを使用して属性を削除してしまうかもしれません。このようないたずらから防御するには、常にサーバーサイドのバリデーションを使用する必要があります。

src/lib/server/database.js で、description が存在すること、そして一意であることをバリデートします。

src/lib/server/database
export function createTodo(userid, description) {
	if (description === '') {
		throw new Error('todo must have a description');
	}

	const todos = db.get(userid);

	if (todos.find((todo) => todo.description === description)) {
		throw new Error('todos must be unique');
	}

	todos.push({
		id: crypto.randomUUID(),
		description,
		done: false
	});
}

重複した todo を送信してみてください。おっと! SvelteKit は不親切なエラーページを表示していますね。サーバーでは ‘todos must be unique’ エラーを見ることができますが、予期せぬエラーメッセージには機密情報が含まれる可能性があるため、SvelteKit ではそれをユーザーには隠しています。

同じページを表示させたまま何が問題だったのかを示し、ユーザーがそれを修正できるようにするほうが良いでしょう。これを行うために、fail 関数を使用して、action からデータと適切な HTTP ステータスコードを返すことができます。

src/routes/+page.server
import { fail } from '@sveltejs/kit';
import * as db from '$lib/server/database.js';

export function load({ cookies }) {...}

export const actions = {
	create: async ({ cookies, request }) => {
		const data = await request.formData();

		try {
			db.createTodo(cookies.get('userid'), data.get('description'));
		} catch (error) {
			return fail(422, {
				description: data.get('description'),
				error: error.message
			});
		}
	}

src/routes/+page.svelte では、form prop を介してその戻り値にアクセスすることができます。このプロパティはフォーム送信の後にのみ、値が入っています。

src/routes/+page
<script>
	let { data, form } = $props();
</script>

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

	{#if form?.error}
		<p class="error">{form.error}</p>
	{/if}

	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>
<script lang="ts">
	let { data, form } = $props();
</script>

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

	{#if form?.error}
		<p class="error">{form.error}</p>
	{/if}

	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>

fail でラップしなくても、action から値を返すことができます。例えば、データが保存されたときに ‘success!’ というメッセージを返すこともできます。それも form prop を介してアクセスすることができます。

Edit this page on GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<script>
	let { data } = $props();
</script>
 
<div class="centered">
	<h1>todos</h1>
 
	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				autocomplete="off"
			/>
		</label>
	</form>
 
	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<form method="POST" action="?/delete">
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete"></button>
				</form>
			</li>
		{/each}
	</ul>
</div>
 
<style>
	.centered {
		max-width: 20em;
		margin: 0 auto;
	}
 
	label {
		width: 100%;
	}
 
	input {
		flex: 1;
	}
 
	span {
		flex: 1;
	}
 
	button {
		border: none;
		background: url(./remove.svg) no-repeat 50% 50%;
		background-size: 1rem 1rem;
		cursor: pointer;
		height: 100%;
		aspect-ratio: 1;
		opacity: 0.5;
		transition: opacity 0.2s;
	}
 
	button:hover {
		opacity: 1;
	}
 
	.saving {
		opacity: 0.5;
	}
</style>