SvelteとFirebaseのFirestoreでTodoリストを作成する方法

Svelteをインストール

まず、以下のコマンドでVite + Svelteをインストールして作業環境を作成します。

npm create vite@latest svelte-todo -- --template svelte
cd svelte-todo
npm install

HTML, CSSのコーディング

まずapp.cssの内容をすべて削除して、App.svelteを以下の内容に変更する。

<script>

</script>

<main>
  <input type="text" value=""> <button>追加</button>
  <hr>
  <ul class="todo">
    <li>
      <button>削除</button>
      <span>foo</span>
    </li>
    <li>
      <button>削除</button>
      <span>bar</span>
    </li>
  </ul>
</main>

<style>
  .todo > li + li {
    margin-top: 0.5em;
  }
</style>

npm run devを実行してブラウザで確認すると下図のように表示されます。

テキストを入力して「追加」を押すとTodoリストに追加され、「削除」を押すと削除されるようにします。

liタグ部分を配列から出力

liタグで表示している部分を配列にして {#each} を使用して表示します。

<script>
  let id = 1
  let todos = [
    {id: id++, text: 'foo'},
    {id: id++, text: 'bar'},
  ]
</script>

<main>
  <input type="text" value="">
  <button>追加</button>
  <hr>
  <ul class="todo">
    {#each todos as todo (todo.id)}
      <li>
        <button>削除</button>
        <span>{todo.text}</span>
      </li>
    {/each}
  </ul>
</main>

追加と削除の処理を追加

追加と削除の処理を入れます。

Enterキーでも追加処理ができるようformタグも追加しました。

<script>
  let id = 1
  let todos = [
    {id: id++, text: 'foo'},
    {id: id++, text: 'bar'},
  ]
  let todoText = ''

  function addTodo() {
    if (todoText.trim()) {
      todos = todos.concat({id: id++, text: todoText})
      todoText = ''
    }
  }

  function deleteTodo(todoId) {
    todos = todos.filter(todo => todo.id !== todoId)
  }

  function handleSubmit(e) {
    e.preventDefault()
  }
</script>

<main>
  <form on:submit={handleSubmit}>
    <input type="text" bind:value={todoText}>
    <button on:click={addTodo}>追加</button>
  </form>
  <hr>
  <ul class="todo">
    {#each todos as todo (todo.id)}
      <li>
        <button on:click={() => deleteTodo(todo.id)}>削除</button>
        <span>{todo.text}</span>
      </li>
    {/each}
  </ul>
</main>

以上で追加と削除の処理はできましたが、これだとデータを保存することができないので、FirebaseのFirestoreにTodoリストのデータの読み込み・追加・削除などができるようにします。

FirebaseのFirestoreから読み込み

まずFirebaseを使用するために以下のコマンドでfirebaseをインストールします。

npm i -D firebase

次にFirebaseのConsoleにアクセスしてプロジェクトを追加します。

https://console.firebase.google.com/u/0/?hl=ja

Firebaseプロジェクトの作成 (手順 1/3)

Googleアナリティクスは使わないのでチェックをはずしてください。

Firebaseプロジェクトの作成 (手順 2/3)

プロジェクトを追加したらアプリの追加(ウェブ)ボタンを押します。

Firebase アプリの追加(ウェブ)ボタン

アプリのニックネームの入力欄が表示されるので、「svelte-todo」と入力して「アプリを登録」を押します。

アプリを登録すると「Firebase SDK の追加」が表示されるので、赤枠のfirebaseConfigの値だけコピーしてFirebaseのConsoleに戻ります。

Consoleに戻ったらCloud Firestore (Firestore Database)に移動して「データベースの作成」を押します。

Cloud Firestoreのルールについて

「データベースの作成」を押したあと「本番環境モードで開始する」を押して、Cloud Firestoreのロケーションを「asia-northeast1 (Tokyo)」にして「有効にする」を押せばデータベースの作成は完了です。

Cloud Firestoreの本番のルールはデフォルトだと読み書き不可になっているため、読み込む場合は以下のようにルールのコードを変更してreadとwriteを許可する必要があります。

※下記のルールは単純にtrueにしてあるが、実際は条件式を入れる。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

コレクションを開始して残りのコードを記述

最後にコレクションを開始して残りのコードを記述する。

コレクションは「todo」にして、フィールドはidとtextを入れる。

あとはfirebaseから必要なものをimportして、以下のように読み込み、書き込み、削除の処理を書けばSvelteとFirebaseのFirestoreでTodoリストを作成できます。

<script>
  import { initializeApp } from 'firebase/app'
  import { getFirestore, collection, getDocs, addDoc, doc, deleteDoc } from 'firebase/firestore'

  const firebaseConfig = {
    apiKey: 'AIzaSyBfkn2r93JbZz8htZbQy2tsl1CZglJEtZk',
    authDomain: 'svelte-todo-d2e38.firebaseapp.com',
    projectId: 'svelte-todo-d2e38',
    storageBucket: 'svelte-todo-d2e38.appspot.com',
    messagingSenderId: '1040707526372',
    appId: '1:1040707526372:web:587d38291c804ecf4ad801'
  }

  const app = initializeApp(firebaseConfig)
  const db = getFirestore(app)
  const todoData = collection(db, 'todo')
  let id
  let todos = []
  let todoText = ''

  getDocs(todoData).then((query) => {
    query.forEach((doc) => {
      todos.push({ docId: doc.id, ...doc.data() })
    });
    todos = todos.sort((a, b) => a.id - b.id)
    id = Math.max(...todos.map(todo => todo.id))
    id = isFinite(id) ? id : 0
  })

  function addTodo(id) {
    if (todoText.trim()) {
      const data = {id: id++, text: todoText}
      todos = todos.concat(data)
      addDoc(todoData, data)
      todoText = ''
    }
  }

  function deleteTodo(docId) {
    todos = todos.filter(todo => todo.docId !== docId)
    const todoRef = doc(db, 'todo', docId)
    deleteDoc(todoRef)
  }

  function handleSubmit(e) {
    e.preventDefault()
  }
</script>

<main>
  <form on:submit={handleSubmit}>
    <input type="text" bind:value={todoText}>
    <button on:click={() => addTodo(++id)}>追加</button>
  </form>
  <hr>
  <ul class="todo">
    {#each todos as todo (todo.id)}
      <li>
        <button on:click={() => deleteTodo(todo.docId)}>削除</button>
        <span>{todo.text}</span>
      </li>
    {/each}
  </ul>
</main>

<style>
  .todo > li + li {
    margin-top: 0.5em;
  }
</style>