2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue.js初学者が書籍片手にtodoアプリをつくる 後篇:実装

Last updated at Posted at 2024-08-26

はじめに

todoアプリ開発の続きです。
前回は環境構築してVITEでローカルサーバを起動するところまで進めました。

今回はいよいよ実装を進めます。

参考書籍

『Vue.js 超入門 v3.4+: 最初に手にしてよかった本』

実際にアプリを作っていく

実現する機能

画面イメージ
image.png

  • TODOを追加できるテキスト入力欄
  • 残TODOが一覧で見える一覧表示欄
  • 内容編集する機能
  • 完了チェックで打消線がつく機能
  • TODOを削除する機能

実装内容

ローカルサーバを起動してindex.htmlを呼び出すところから実装を見ていきます。

index.html

index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

ここでは、main.tsを呼び出しているだけです。
次はmain.tsを見てみます。

main.ts

main.ts
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

このコードでは、createApp関数を使ってVueアプリケーションのインスタンスを作成し、App.vueというルートコンポーネントを指定して、HTMLの#app要素にマウントします。

ルートコンポーネントは他のすべてのコンポーネントを包含する「土台」として考えることができます。これにより、アプリケーションの各部分が独立して機能しつつ、全体として統合された動作を実現します。Vue3では、ルートコンポーネントを通じて、状態管理やルーティングなどの機能を一元的に管理することが可能です。1

App.vueは単一ファイルコンポーネントと呼ばれるscript, HTML, CSS を1つのファイルに書く構造となっています。

<script>タグで制御を記載する。
<template>タグで部品配置などHTMLに相当する部分を記載する。
<style>タグでデザインを記載する。

今回のアプリでは具体的に以下のように記載しました。

App.vue

App.vue
<script setup lang="ts">
import MainTodo from '@/components/MainTodo.vue' // メイン部の呼び出し
import TheFooter from '@/components/TheFooter.vue' // ヘッダー部の呼び出し
import TheHeader from '@/components/TheHeader.vue' // フッター部の呼び出し
</script>

<template>
  <div class="warp">
    <TheHeader /> <!-- ヘッダー部 -->
    <main class="main"><MainTodo /></main> <!-- メイン部 -->
    <TheFooter /> <!-- フッター部 -->
  </div>
</template>

<sytle>はTheHeader、MainTodo、TheFooterでそれぞれ記載しているので、ここでは書いていません。

ヘッダー部、フッター部はそれぞれ次のように記載しています。

TheHeader.vue

TheHeader.vue
<template>
  <h1 class="title">TODO</h1>
</template>

<style scoped>
.title {
  width: 100%;
  padding: 16px 0;
  margin: 0;
  text-align: center;
  background-color: #fff;
}
</style>

TheFooter.vue

TheFooter.vue
<template>
  <footer class="footer">&copy; yorocovich</footer>
</template>

<style scoped>
.footer {
  width: 100%;
  height: 30px;
  text-align: center;
  box-shadow: 0 1px 1px #ddd;
}
</style>

<style scoped>のようにscopedを記載することで、スタイルの適用範囲を同vueファイル内に限定することができます。

メイン部のMainTodoは次のようになっています。

MainTodo.vue

MainTodo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useTodoList } from '@/composables/useTodoList'
const { todoList, add, edit, check } = useTodoList()
const todo = ref<string | undefined>()

const addTodo = () => {
  if (!todo.value) return
  add(todo.value)
  todo.value = ''
}
const editTodo = (_id: number) => {
  edit(_id)
}
const changeCheck = (_id: number) => {
  check(_id)
}
</script>

<template>
  <div>
    <input
      type="text"
      class="todo_input"
      v-model="todo"
      @keyup.enter="addTodo"
      placeholder="TODOを入力"
    />
  </div>
  <div class="box_list">
    <div class="todo_list" v-for="todo in todoList" :key="todo.id">
      <div class="todo" :class="{ fin: todo.checked }">
        <input
          type="checkbox"
          class="check"
          @change="changeCheck(todo.id)"
          :checked="todo.checked"
        />
        <input
          v-show="!todo.checked"
          type="text"
          class="task"
          @keyup.enter="editTodo(todo.id)"
          @blur="editTodo(todo.id)"
          v-model="todo.task"
        />
        <label v-show="todo.checked">{{ todo.task }}</label>
      </div>
    </div>
  </div>
</template>

<style scoped>
.todo_input {
  width: 250px;
  padding: 6px 8px;
  margin-right: 8px;
  font-size: 18px;
  border: 1px solid #aaa;
  border-radius: 6px;
}

.box_list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 4px;
}

.todo_list {
  display: flex;
  gap: 5px;
  align-items: center;
}

.todo {
  width: 300px;
  padding: 4px 1px;
}

.check {
  margin-right: 12px;
  transform: scale(1.6);
}

.task {
  width: 120px;
  font-size: 15px;
  border: 0px;
}

.fin {
  font-size: 15px;
  color: #777;
  text-decoration: line-through;
  background-color: #ddd;
}
</style>

以下軽く解説
import { ref } from 'vue' ref は Vue3 の Composition API で導入された関数で、リアクティブな値を作成するために使用されます。更新を画面をリアルタイムで反映してくれるスグレモノです。

<input type="text" class="todo_input" v-model="todo" @keyup.enter="addTodo" placeholder="TODOを入力" />

新規TODOを入力するテキストボックスです。
EnterキーでaddTodoを呼び出してLocalStorageにTODOテキストを保存しています。

<div class="todo_list" v-for="todo in todoList" :key="todo.id">

TODOリストを作成しているところです。
LocalStorageにあるtodoListを1件ずつ配列で取得しています。
v-forをつかうことでループ処理を実現しています。

<input type="checkbox" class="check" @change="changeCheck(todo.id)" :checked="todo.checked" />

チェックボックスです。
チェックするとchangeCheckを呼び出してチェック処理します。

<input v-show="!todo.checked" type="text" class="task" @keyup.enter="editTodo(todo.id)" @blur="editTodo(todo.id)" v-model="todo.task" />

TODOリストのテキスト部です。
テキストを変更するとeditTodoを呼び出して編集しています。フォーカスアウトのときも同じ動作します。

最後に、これらの挙動を制御するスクリプト部です。

useTodoList.ts

useTodoList.ts
import { ref } from 'vue'

export const useTodoList = () => {
  const todoList = ref<{ id: number; task: string; checked: boolean }[]>([])
  const ls = localStorage.todoList

  // ローカルストレージにtodoListが存在していればparseし、
  // なければ空配列をセット
  todoList.value = ls ? JSON.parse(ls) : []

  const findById = (_id: number) => {
    return todoList.value.find((todo) => todo.id === _id)
  }

  const findIndexById = (_id: number) => {
    return todoList.value.findIndex((todo) => todo.id === _id)
  }

  const add = (_task: string) => {
    const id = new Date().getTime()
    todoList.value.push({ id: id, task: _task, checked: false })
    localStorage.todoList = JSON.stringify(todoList.value)
  }

  const edit = (_id: number) => {
    const todo = findById(_id)
    const idx = findIndexById(_id)
    if (!todo) return
    if (todo?.task === '') {
      del(_id)
    } else {
      todoList.value.splice(idx, 1, todo) // splice関数で_idxを元にTODOを置換
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  const del = (_id: number) => {
    const todo = findById(_id)
    if (todo) {
      const idx = findIndexById(_id)
      todoList.value.splice(idx, 1)
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  const check = (_id: number) => {
    const todo = findById(_id)
    const idx = findIndexById(_id)

    if (todo) {
      todo.checked = !todo.checked //反転
      todoList.value.splice(idx, 1, todo)
      localStorage.todoList = JSON.stringify(todoList.value)
    }
  }

  return { todoList, add, edit, check }
}

return { todoList, add, edit, check }で作成した関数を戻しています。
MainTodo.vueのスクリプト部でconst { todoList, add, edit, check } = useTodoList()としてキャッチしています。

さいごに

初めてVueに触れましたが、すごく進化していて楽しかったです。
業務でWeb開発(JSP+Java)していたのが2010年頃なので、まったく違う体験をしました。「今ってこうなってるのーー!」という衝撃を受けました。

どこまで書いていいのか、どう書いていいのか、初めてのことばかりで文章が雑だと感じています。
内容の推敲は今後時間があるときに行っていきたいと思います。

書籍「Vue.js 超入門3.4+」を片手(iPad)に開発をしてきましたが、
本書の手順に忠実に従うことで、スムーズにアプリを完成させることができました。
説明が適切なタイミングで、適切な分量で出てくるので、読み進めやすかったです。
Vue初学者の方はぜひともご一読してみてはいかがでしょうか。

  1. https://ja.vuejs.org/guide/essentials/application#the-application-instance

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?