0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SvelteでTodoアプリを作る

Last updated at Posted at 2024-12-29

完成図

ScreenShot 2024-12-30 11.06.00.png

miseでBunセットアップ

$ mise use -g bun@1.1.42
$ bun -v

Todoプロジェクトの作成

$ bunx sv create svelte-todo-app
┌  Welcome to the Svelte CLI! (v0.6.10)
│
◆  Which template would you like?
│  # プロジェクトテンプレートをどうするか。今回は minimal を選択する
│  ● SvelteKit minimal (barebones scaffolding for your new app)
│  ○ SvelteKit demo
│  ○ Svelte library
│
◇  Add type checking with Typescript?
│  # TypeScriptを使うか? 特に使わない選択肢はないと思うのでYes
│  ● Yes, using Typescript syntax
│  ○ Yes, using Javascript with JSDoc comments
│  ○ No
│
◇  What would you like to add to your project? (use arrow
keys / space bar)
|  # プロジェクトに追加したいツールはあるか
|  # デモアプリに必要ないと思うが、コードフォーマッターと静的解析ツールを選択
│  ◼ prettier (formatter - https://prettier.io) # コードフォーマッター
│  ◼ eslint # JavaScript/TypeScript用の静的解析ツール
│  ◻ vitest # ユニットテストフレームワーク
│  ◻ playwright # エンドツーエンド(E2E)テスト用フレームワーク
│  ◻ tailwindcss # CSSフレームワーク
│  ◻ sveltekit-adapter # SvelteKitプロジェクトのビルド先を指定するためのアダプター
│  ◻ drizzle # TypeScript対応の軽量ORM
│  ◻ lucia # 認証ライブラリ
│  ◻ mdsvex # MarkdownとSvelteを統合するためのプラグイン
│  ◻ paraglide # Svelteアプリ用の国際化(i18n)ライブラリ
│  ◻ storybook # UIコンポーネントを個別に開発・テストするためのツール
│
◆  Which package manager do you want to install dependencies
with?
|  # パッケージマネージャを選択。今回はbunを使用する
│  ○ None
│  ○ npm
│  ○ yarn
│  ○ pnpm
│  ● bun
│  ○ deno
│
◆  Successfully setup add-ons
│
◆  Successfully installed dependencies
│
◇  Successfully formatted modified files
│
◇  Project next steps ─────────────────────────────────────────────────────╮
│                                                                          │
│  1: cd svelte-todo-app                                                   │
│  2: git init && git add -A && git commit -m "Initial commit" (optional)  │
│  3: bun run dev --open                                                   │
│                                                                          │
│  To close the dev server, hit Ctrl-C                                     │
│                                                                          │
│  Stuck? Visit us at https://svelte.dev/chat                              │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯
│
└  You're all set!

SvelteKitを起動する

$ cd svelte-todo-app
$ git init && git add -A && git commit -m "Initial commit"
$ bun dev --open

ScreenShot 2024-12-30 4.23.09.png

インデントをタブからスペースに変更

個人の好みですが、インデントはスペース派なので変更してます。

.prettierrc
{
-	"useTabs": true,
+	"useTabs": false,
	"singleQuote": true,
	"trailingComma": "none",
	"printWidth": 100,
	"plugins": ["prettier-plugin-svelte"],
	"overrides": [
		{
			"files": "*.svelte",
			"options": {
				"parser": "svelte"
			}
		}
	]
}
$ bun run format

便利コマンド一覧

$ bun run dev # Viteのローカル開発用サーバーを起動。自動リロード機能
$ bun run build # プロジェクトを本番用にビルド。distディレクトリにビルド成果物が出力
$ bun run preview # ビルド済みプロジェクトのプレビュー
$ bun run check # プロジェクトの型や設定の整合性をチェック
$ bun run check:watch # 型チェックの監視モードで実行
$ bun run format # コードフォーマットを実行
$ bun run lint # コードの静的解析とフォーマットチェックを実行

シンプルなTodoアプリを作る

src/routes/+page.svelte ファイルに次のコードを記述します。

src/routes/+page.svelte
<script lang="ts">
  type Todo = {
    text: string;
    done: boolean;
  };

  let todos: Todo[] = $state([
    { text: 'Apple', done: false },
    { text: 'パン', done: false },
    { text: '牛乳', done: true },
    { text: 'Coffee', done: false }
  ]);
  let newTodo: string = $state('');

  function resetInput() {
    newTodo = '';
  }

  function addTodo() {
    if (newTodo.trim()) {
      todos.push({ text: newTodo, done: false });
      resetInput();
    }
  }

  function deleteTodo(index: number) {
    todos.splice(index, 1);
  }
</script>

<main>
  <h1>Todo App</h1>
  <input
    type="text"
    bind:value={newTodo}
    placeholder="New task..."
    onkeydown={(e) => {
      if (e.key === 'Enter' && !e.isComposing) {
        addTodo();
      }
    }}
  />
  <button onclick={addTodo} disabled={!newTodo.trim()}>Add</button>

  <ul>
    {#each todos as todo, i}
      <li>
        <input type="checkbox" bind:checked={todo.done} />
        <span class={todo.done ? 'done' : ''}>{todo.text}</span>
        <button onclick={() => deleteTodo(i)}>Delete</button>
      </li>
    {/each}
  </ul>
</main>

<style>
  span.done {
    text-decoration: line-through;
  }

  button[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }
</style>

ScreenShot 2024-12-30 6.17.40.png

Todo型の切り出し

Todo型を別ファイルに切り出して再利用可能にすることで、将来的な拡張性を高められます。
src/types/todo.ts と新しく型ファイルを作成してインポートしてみます。

src/types/todo.ts
export type Todo = {
  text: string;
  done: boolean;
};
src/routes/+page.svelte
<script lang="ts">
-  type Todo = {
-    text: string;
-    done: boolean;
-  };
+  import type { Todo } from '../types/todo';
...

データの永続化

現状ではTodoリストがメモリ内にのみ保持され、ページをリロードするとTodoリストがリセットされます。

ブラウザのlocalStorageを使ってデータを永続化します。

src/routes/+page.svelte
 <script lang="ts">
   import type { Todo } from '../types/todo';
+  import { onMount } from 'svelte';
 
-  let todos: Todo[] = $state([
-    { text: 'Apple', done: false },
-    { text: 'パン', done: false },
-    { text: '牛乳', done: true },
-    { text: 'Coffee', done: false }
-  ]);
+  let todos = $state([] as Todo[]);
   let newTodo: string = $state('');
+  let isInitialized: boolean = $state(false);
+
+  onMount(() => {
+    if (typeof window !== 'undefined') {
+      try {
+        const savedTodos = localStorage.getItem('todos');
+        if (savedTodos) {
+          todos = JSON.parse(savedTodos);
+        }
+      } catch (e) {
+        console.error('Failed to load todos from localStorage:', e);
+      } finally {
+        isInitialized = true;
+      }
+    }
+  });
+
+  $effect(() => {
+    if (isInitialized && typeof window !== 'undefined') {
+      try {
+        localStorage.setItem('todos', JSON.stringify(todos));
+      } catch (e) {
+        console.error('Failed to save todos to localStorage:', e);
+      }
+    }
+  });

   function resetInput() {
     newTodo = '';
   }
  • onMountを使い、localStorageからデータを復元
  • $stateでtodos、newTodo、isInitializedをリアクティブな状態として管理
  • $effectを利用して、todosが変更されるたびにlocalStorageに保存
  • 初期化完了(isInitializedがtrue)後にのみ保存処理を実行
  • ハードコードな初期値を削除

スタイルの強化

Tailwind CSSフレームワークを導入してTodoアプリの見た目を少しオシャレにします。

$ bun install -d tailwindcss postcss autoprefixer
$ bunx tailwindcss init -p
$ bun run format

svelte.config.js は特に変更ありません。
次のデフォルトの内容です。

svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://svelte.dev/docs/kit/integrations
  // for more information about preprocessors
  preprocess: vitePreprocess(),

  kit: {
    // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
    // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
    // See https://svelte.dev/docs/kit/adapters for more information about adapters.
    adapter: adapter()
  }
};

export default config;

tailwind.config.js を修正します。

tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
-  content: [],
+  content: ['./src/**/*.{html,js,svelte,ts}'],
  theme: {
    extend: {}
  },
  plugins: []
};

src/app.css を新たに追加します。

src/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

src/routes/+layout.svelte を新たに追加します。

src/routes/+layout.svelte
<script>
  import '../app.css';
</script>

<slot />

開発サーバーを再起動します。

$ bun dev --open

補足: 開発サーバーでエラー

$ bun dev --open
failed to load config from /Users/your-name/svelte-todo-app/vite.config.ts
error when starting dev server:
Error: The service was stopped
    at /Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:968:34
    at responseCallbacks.<computed> (/Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:622:9)
    at Socket.afterClose (/Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:613:28)
    at Socket.emit (node:events:519:35)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
error: script "dev" exited with code 1

私が試した時、 node_modules ファイルが壊れたようでこのエラーが発生しました。

$ rm -rf node_modules
$ bun install
$ bun dev --open

上記のコマンドでエラーが解消されました。

src/routes/+page.svelte
<script lang="ts">
  import type { Todo } from '../types/todo';
  import { onMount } from 'svelte';

  let todos = $state([] as Todo[]);
  let newTodo: string = $state('');
  let isInitialized: boolean = $state(false);

  onMount(() => {
    if (typeof window !== 'undefined') {
      try {
        const savedTodos = localStorage.getItem('todos');
        if (savedTodos) {
          todos = JSON.parse(savedTodos);
        }
      } catch (e) {
        console.error('Failed to load todos from localStorage:', e);
      } finally {
        isInitialized = true;
      }
    }
  });

  $effect(() => {
    if (isInitialized && typeof window !== 'undefined') {
      try {
        localStorage.setItem('todos', JSON.stringify(todos));
      } catch (e) {
        console.error('Failed to save todos to localStorage:', e);
      }
    }
  });

  function resetInput() {
    newTodo = '';
  }

  function addTodo() {
    if (newTodo.trim()) {
      todos.push({ text: newTodo, done: false });
      resetInput();
    }
  }

  function deleteTodo(index: number) {
    todos.splice(index, 1);
  }
</script>

<main class="min-h-screen bg-gray-100 flex flex-col items-center py-10">
  <h1 class="text-4xl font-bold text-gray-800 mb-8">Todo App</h1>

  <div class="flex items-center gap-4 mb-6">
    <input
      type="text"
      bind:value={newTodo}
      placeholder="New task..."
      onkeydown={(e) => {
        if (e.key === 'Enter' && !e.isComposing) {
          addTodo();
        }
      }}
      class="w-64 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
    />
    <button
      onclick={addTodo}
      disabled={!newTodo.trim()}
      class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
    >
      Add
    </button>
  </div>

  <ul class="w-full max-w-md">
    {#each todos as todo, i}
      <li class="flex items-center justify-between bg-white p-4 rounded-lg shadow-sm mb-2">
        <div class="flex items-center gap-3">
          <input
            type="checkbox"
            bind:checked={todo.done}
            class="h-5 w-5 text-blue-500 focus:ring-blue-500 border-gray-300 rounded"
          />
          <span class={`text-lg ${todo.done ? 'line-through text-gray-400' : 'text-gray-700'}`}
            >{todo.text}</span
          >
        </div>
        <button onclick={() => deleteTodo(i)} class="text-red-500 font-semibold hover:underline">
          Delete
        </button>
      </li>
    {/each}
  </ul>
</main>

ScreenShot 2024-12-30 10.05.04.png

見た目が変わるとテンション上がりますね!
こんな感じにSvelteでTodoアプリを作ってみました。

GitHubに今回のTodoアプリのコードを置いてます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?