あっくんブログ Written by Akihiro Tsuji

Vue3でメモアプリを作ろう!

Vue.js プログラミング

こんにちは、あっくんです。先日、栗拾いに行ってきました。幼稚園児のころに行って以来で、すごい楽しかったです。栗を拾うのが、お宝を見つける感覚でわくわくと満たされる感がありました。しかし、自分以外の来られている方が、栗は係の人が蒔いてると言ってはりました。たしかに栗やいがを見ると、栗はすごい綺麗で大きくて、いがは凄いボロボロでした。木になっている栗はよく見るとほぼ虫にたべられていました。つまり栗自体は山の生物の食料になっているので、きれいな形で保たれる時間が短いのではないかと感じました。栗を拾えて楽しいのと裏事情も知れたので2倍楽しめた感じでした。改めて自然は良いと感じました。

アプリケーション作成

Vuexでデータを保存できるようになりました。保存ができるとそれなりのアプリケーションが作れるようになります。今回はメモ書きアプリケーションを作っていきましょう。メモアプリは入力フォームと保存されているメモのリストです。フォームにメモのタイトルと内容を書いて、saveボタンで保存ができます。保存されているメモは1度に5個表示されます。リストのしたにprevとnextという表示があり、これをクリックすると前後のページに移動します。

Image from Gyazo

↑メモアプリの画面。フォームとメモのリスト。

Image from Gyazo

↑リストの項目をクリックすると、そのメモの内容が表示されます。

Image from Gyazo

↑タイトルフィールドにテキストを書き、「Find」ボタンを押すと、そのテキストをタイトルに含むメモを検索してリストを表示します。

プロジェクトを作る

memo_appをViteで作成してでサンプルを作っていきましょう。

開発するディレクトリでターミナルに↓のように記述してmemo_appを作って、npmをインストールします。

npm init vite-app memo_app

cd memo_app
npm install

Image from Gyazo

続いて、Vuexとvuex-persistedstateをインストールしていきましょう。

npm install vuex@next

npm install vuex-persistedstate

index.htmlの作成

memo_appのindex.htmlを書き換えましょう

index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Memo App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" /> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script> </head> <body> <h1 class="bg-secondary text-white h4 p-3">Memo_app</h1> <div class="container"> <div id="app"></div> </div> <script type="module" src="/src/main.js"></script> </body> </html>
Code language: HTML, XML (xml)

main.jsとApp.vueの作成

main.jsを作成します。

main.js

import { createApp } from "vue"; import App from "./App.vue"; import "./index.css"; import { store } from "./store"; const app = createApp(App); app.use(store); app.mount("#app");
Code language: JavaScript (javascript)

App.vueを作成します。

App.vue

<template> <Memo /> </template> <script> import Memo from "./components/Memo.vue"; export default { name: "App", components: { Memo, }, }; </script>
Code language: HTML, XML (xml)

store.jsの作成

Vuexのストアを用意するstore.jsを作成しましょう。「src」フォルダ内にstore.jsというファイルを作成していきましょう。

store.js

import { createStore } from "vuex"; import createPersistedState from "vuex-persistedstate"; export const store = createStore({ state: () => { return { memo: [], page: 0, }; }, mutations: { insert: (state, obj) => { const d = new Date(); const fmt = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " " + d.getHours() + ":" + d.getMinutes(); state.memo.unshift({ title: obj.title, content: obj.content, created: fmt, }); }, set_page: (state, p) => { state.page = p; }, remove: (state, obj) => { for (let i = 0; i < state.memo.length; i++) { const ob = state.memo[i]; if ( ob.title == obj.title && ob.content == obj.content && ob.created == obj.created ) { alert("remove it! --" + ob.title); state.memo.splice(i, 1); return; } } }, }, plugins: [createPersistedState()], });
Code language: JavaScript (javascript)

insert: (state, obj)

新しいメモを追加するためのものです。引数のobjにはメモのタイトルとコンテンツをまとめたものが渡されます。これらの内容と投稿された日時のテキストを新たに追加したものをmemoステートの最初に追加します。

set_page: (state,p)

表示するページの移動です。引数pは整数値で指定します。

remove: (state, obj)

メモの削除を行うためのものです。引数のobjには、削除するメモのオブジェクトが渡されます。メモのオブジェクトには、タイトル、コンテンツ、作成日時の情報が保管されています。memoの中からこれらすべてが完全一致する項目を調べそれを削除します。

Memo.Vueの作成

Memoのコンポーネントを作成しましょう。HelloWorld.vueを名前の変更でMemo.vueに変更して記述していきましょう。

Memo.vue

<template> <section class="alert alert-primary"> <div class="form-control-group row"> <label class="col-12 text-left h5">Title</label> <input type="text" name="title" class="form-control col-9 ml-2" v-model="data.title" @focus="set_flg" /> <button @click="find" class="btn btn-primary col-2 ml-2">find</button> </div> <div class="form-control-group mt-3"> <label class="col-12 text-left h5">Memo</label> <textarea name="content" class="form-control" v-model="data.content" ></textarea> </div> <div> <button class="btn btn-info m-2" @click="insert">save</button> <transition name="del"> <button class="btn btn-info m-2" v-if="data.sel_flg != false" @click="remove" > delete </button> </transition> </div> <ul class="list-group"> <li v-for="item in page_items" @click="select(item)" class="list-group-item list-group-item-action text-left" > {{ item.title }} ({{ item.created }}) </li> </ul> <hr /> <div> <span class="btn btn-secondary mr-2" @click="prev">&lt; prev</span> <span class="btn btn-secondary ml-2" @click="next">next &gt;</span> </div> </section> </template> <script> import { ref, reactive, computed, onMounted } from "vue"; import { useStore } from "vuex"; export default { setup(props) { // リアクティブデータ const data = reactive({ title: "", content: "", num_per_page: 5, find_flg: false, sel_flg: false, sel_item: null, store: useStore(), }); // フラグの初期化 const set_flg = () => { if (data.find_flg.value || data.sel_flg != false) { data.find_flg = false; data.sel_flg = false; data.title = ""; data.content = ""; } }; // 項目の選択 const select = (item) => { data.find_flg = false; data.sel_flg = true; data.title = item.title; data.content = item.content; data.sel_item = item; }; // 検索の設定 const find = () => { data.sel_flg = false; data.find_flg = true; }; // メモの追加 const insert = () => { data.store.commit("insert", { title: data.title, content: data.content }); data.title = ""; data.content = ""; }; // 選択項目の削除 const remove = () => { if (data.sel_flg) { data.store.commit("remove", data.sel_item); set_flg(); } }; // 次のページ const next = () => { page.value++; }; // 前のページ const prev = () => { page.value--; }; // メモ全体 const memo = computed(() => data.store.state.memo); // ページの表示項目 const page_items = computed(function () { if (data.find_flg) { var arr = []; var rec = data.store.state.memo; rec.forEach((element) => { console.log(); if ( element.title.toLowerCase().indexOf(data.title.toLowerCase()) >= 0 ) { arr.push(element); } }); return arr; } else { return data.store.state.memo.slice( data.num_per_page * data.store.state.page, data.num_per_page * (data.store.state.page + 1) ); } }); //表示ページを表す値 const page = computed({ get: () => { return data.store.state.page; }, set: (p) => { var pg = p > (data.store.state.memo.length - 1) / data.num_per_page ? Math.ceil( (data.store.state.memo.length - 1) / data.num_per_page ) - 1 : p; pg = pg < 0 ? 0 : pg; data.store.commit("set_page", pg); }, }); // マウント時の処理 onMounted(() => { data.store.commit("set_page", 0); }); // 戻り値 return { data, set_flg, select, find, insert, remove, next, prev, memo, page_items, page, }; }, }; </script>
Code language: HTML, XML (xml)

Memo.vueのtemplateをチェック

フォーム関連(テキストの入力コントロール2つとボタンが3つ)とメモを表示するリスト、ページを移動するリンクが用意されています。

タイトルの入力フィールド

<input type="text" name="title" class="form-control col-9 ml-2" v-model="data.title" @focus="set_flg" />
Code language: JavaScript (javascript)

v-model=”data.title”でtitleデータに値をバインドしています。@focus = “set_flg”で、この入力フィールドにフォーカスが移ったら、set_flgを実行します。

検索ボタン

<button @click="find" class="btn btn-primary col-2 ml-2">find</button>
Code language: HTML, XML (xml)

@click = “find”として、クリックしたらfindを実行するようにしてあります。

コンテンツのテキストエリア

<textarea name="content" class="form-control" v-model="data.content" ></textarea>
Code language: HTML, XML (xml)

v-model=”data.content”としてcontentデータにバインドしてあります。

保存ボタン

<button class="btn btn-info m-2" @click="insert">save</button>
Code language: HTML, XML (xml)

@click=”insert”として、insertを実行しています。

削除ボタン

<button class="btn btn-info m-2" v-if="data.sel_flg != false" @click="remove">    delete</button>
Code language: JavaScript (javascript)

削除ボタンは、リストから項目をクリックして、メモの内容がフォームに表示されたときに使えるようにします。v-if=”data.sel_flg != false”と指定し、data.sel_flgの値がfalseでない場合に表示し、falseの時は非表示になるようにします。@click=”remove”はクリックしたら、removeを実行します。

メモのリスト

<ul class="list-group"> <li v-for="item in page_items" @click="select(item)" class="list-group-item list-group-item-action text-left" > {{ item.title }} ({{ item.created }}) </li> </ul>
Code language: HTML, XML (xml)

<li>に,v-for=”item in page_items”を用意して、page_itemsから順に値をitemに取り出して繰り返し表示を行います。@click=”select(item)”を指定し、クリックしたら、selectを実行します。表示する内容は{{item.title}}({{item.create}})の値を使っています。

ページの移動リンク

<span class="btn btn-secondary mr-2" @click="prev">< prev</span> <span class="btn btn-secondary ml-2" @click="next">next ></span>

@clickでprevとnextを実行する設定をしています。

Memo.vueのスクリプト

dataプロパティ

const data = reactive({ title: "", content: "", num_per_page: 5, find_flg: false, sel_flg: false, sel_item: null, store: useStore(), });
Code language: JavaScript (javascript)

titleとcontentは入力された値が保管されます。入力フィールドとテキストエリアの値をv-modelでバインドしています。
num_per_pageは1ページあたりの表示数を示します。ここでは5にして、1度に5つのメモを表示しています。
find_flgは検索実行中を示す変数です。これがtrueならば、検索中、falseならばそうでないことを示します。
sel_flgは項目を選択した状態かどうかを示す変数です。これは、選択状で態ない場合はfalseになり、選択した場合はそのメモをオブジェクトが設定されます。
sel_itemは項目が選択されているときに、選択された項目のオブジェクトを保管します。

ストアの扱い

sotre: useStore(),

Vuexのストア(store.js)を代入しておくためのものです。これで、data.storeとして、ストアを利用できるようになります。

setup内のメソッド定義

set_flg

フラグの設定を行うものです。find_flgがtrueで、sel_flgがfalseでない場合にはこれらをfalseに戻し、titleとcontentを空にします。つまり、検索や選択中の状態から元の状態に戻す処理です。

select

項目を選択し、その内容をフォームに表示する処理です。find_flgをfalseにし、sel_flgに選択するメモのオブジェクトを代入し、titleとcontentにメモのタイトルと内容をそれぞれ設定します。

find

検索を実行します。sel_flgをfalseにし、find_flgをtrueにします。

insert

メモの追加です。data.store.commitを使い、memoストアのinsertを実行します。引数には、{title: data.title, content: data.content}というようにしてメモのタイトルと内容をオブジェクトにまとめたものを用意しています。

remove

メモの削除を行うものです。削除は、sel_flgがfalseでないかチェックしています。falseでないということは、何かのメモが選択された状態ということです。removeは選択したメモを削除するものなので、選択されてないと実行できません。つまり、削除処理は、data.store.commitでmemoストアのremoveを実行するだけです。このとき、引数にsel_itemを渡します。メモが選択されている時は、このsel_itemに選択したメモのオブジェクトが入っているので、それをremoveに渡します。そして、set_flgを呼び出して元の状態に戻します。

next/prev

前後ページに移動する処理です。pageの値を1増やした理減らしたりします。pageは算術プロパティで、そのままストアのpageを変更します。それにより、page_itemsで表示する項目が変更され、画面のリストが更新されます。

算術プロパティ

getのみの場合

<!-- wp:paragraph --> <p> const 定数 = computed(()=&gt;{<br>…….処理……</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>})</p> <!-- /wp:paragraph -->
Code language: HTML, XML (xml)

get/setの場合

const page = computed({ get: ()=> { …….処理 }, set: (p)=> { ……..処理 }, })
Code language: JavaScript (javascript)

computedという関数の中に、getとsetの処理を行う関数が用意されています。getのみの場合はそのまま関数を引数に指定します。get/setを行う場合は、{}内にオブジェクトとしてgetとsetというプロパティを用意しておきます。

page_items

page_itemsは現在、リストに表示されるメモの配列を表すプロパティです。これは、そのときの状態によって取り出す値が変わります。

if (data.find_flg) { ……検索時の表示時の表示 } else { ……それ以外のときの表示 }
Code language: JavaScript (javascript)

find_flgをチェックし、検索時とそうでないと消え異なるメモ配列を返すようにしています。
検索時は、memo配列のすべてのメモについて繰り返し処理をしています。取り出したメモのtitleの中に、入力フィールドに記入したテキストが含まれているかどうかを調べ、含まれていたならそれを別の変数に取り出していきます。 
検索時でない場合は、表示ページ番号data.store.state.pageとページあたりの項目数data.num_per_pageを使って、現在のページに表示する項目をmemoから取り出して返します。コンポーネントのスクリプト内からステートを利用する場合は、「data.store.state○○」というように、data.store.stateの後にステート名をつけて指定します。

page

pageは現在のページ数を示す値です。この値は、data.store.state.memo.pageの値をそのまま返せばOKです。値の変更は、data.store.commitメソッドで、memoにあるset_pageミューテーションを呼び出して行っています。

onMouted

最後にonMountedも用意しています。これはイベントフックと呼ばれる関数です。これは、import部分でvueパッケージからインポートすることで利用可能になります。
Composition APIでは、vueパッケージから必要なイベントフックの関数をインポートすることで、setup内にその値を用意できるよいうになります。。関数名は「onイベント」というように変更されています。mountedなら「onMounted」となります。
onMountedはコンポーネントがマウントされる際に実行する処理です。

onMounted(()=>{ data.store.commit('set_page', 0) })
Code language: JavaScript (javascript)

引数にはアロー関数を用意します。そのなかで、マウント時の処理を用意します。data.store.commitを使い、memoストアのset_pageを呼び出して、表示ページをゼロにしています。ストアの値は、ローカルストレージに保存されますから、次にアクセスしたときには最後に表示していたページが表示されるようになります。

Composition API利用の注意点

Composition APIはthisは使わない。

ここまで読んでいただいてありがとうございます。
Memoアプリを書籍とともに作っていきましたが、Memo.vueの記述が本当に難しかったです。よくわかっていない部分がありますので、JavaScriptの基本もしっかりやっていきます。
掌田津耶乃さんのVue.js3超入門で勉強をしています。語りかけるような文章ですごいわかりやすいです。本当におすすめです。