見出し画像

Vue.js勉強記録その28 Vue3を更にパワーアップしよう 5-4

こちらの書籍で勉強中です。

今回は、メモアプリを作ります。少し長い記事になっちゃいそうです。。。さらに、テキストから少しだけ自分なりにアレンジした内容になってます。

■まずは各ファイルを連携させる

いきなり全てのコードを全部書いてしまうと、動きがわからないので、少しずつ書いていきます。

まずは、ベースになるHTMLをちゃんと表示させるところまで。

Vuexと、vuex-persistedstateを使いたいので、npmでインストールします。

npm install vuex@next
npm install vuex-persistedstate


index.html、main.js、App.vue、store.js、memo.vueを書いていきます。App.vueが親で、memo.vueが子になります。

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>Vite App</title>
 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
   integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
 <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
   integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
   crossorigin="anonymous"></script>
 <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
   integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
   crossorigin="anonymous"></script>
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js"
   integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"
   crossorigin="anonymous"></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>

bootstrapのcdnと見出しなどを書く。


main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import { store } from './store'

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

Vuexを使いたいので、storeを使う記述を書く


App.vue

<template>
 <memo />
</template>

<script>
import Memo from "./components/Memo.vue";

export default {
 name: "App",
 components: {
   Memo,
 },
};
</script>

まずは、Memoコンポーネントを出力する記述のみ


store.js

import { createStore } from 'vuex'
import createPersistedState from "vuex-persistedstate"

export const store = createStore({
   state: () => {
       return {
           memo: [],
           page: 0
       }
   },
   mutations: {

   },
   plugins: [
       createPersistedState(),
   ]
})

stateにmemoとpageを追加し、各々空の配列と0を定義する。
mutationは、いずれ書くが、ひとまず準備するだけにする。
pluginsにローカルストレージを使う記述を。


Memo.vue

<template>
 <section class="alert alert-primary">
   <div class="form-control-group row">
     <label for="col-12 text-left h5">Title</label>
     <input type="text" name="title" class="form-control col-9 ml-2" />
     <button 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"></textarea>
   </div>
   <div>
     <button class="btn btn-info m-2">save</button>
     <transition name="del">
       <button class="btn btn-info m-2">delete</button>
     </transition>
   </div>
   <ul>
     <li>各メモ</li>
   </ul>
   <hr />
   <div>
     <span class="btn btn-secondary mr-2">&lt; prev</span>
     <span class="btn btn-secondary ml-2">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({});
   return { data };
 },
};
</script>

memo.vueには、入力フォームとメモが出力される場所を。setupなどはとりあえず最低限の物を書いておきます。


スクリーンショット 2022-03-16 11.17.43

ブラウザで見るとこんな感じ。ここから少しずつ機能を付けていきます。


■フォームに入力された内容を配列に格納

フォームに入力された値は、store.jsのmemoの配列に追加されるようにします。

store.jsのmutationsにinsertを追加します。処理内容は、メモのタイトル、メモの内容、今の日付をオブジェクトにして、配列memoに格納する処理です。

store.js

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
       })
       console.log(state.memo);
   }
},

このmutationを、memo.vueからタイトルと内容を引数にしつつ呼び出します。

確認用で、console.logを入れています。(後で消します。)


次に、Memo.vueにinsertのmutationを呼び出す仕組みを作ります。

まずは、inputとtextareaの値とsetupの中のdataの値を、v-modelを使って紐付けます。

Memo.vue

<input
 type="text"
 name="title"
 class="form-control col-9 ml-2"
 v-model="data.title"
/>
<textarea
 name="content"
 class="form-control"
 v-model="data.content"
></textarea>


そして、ボタンが押された時の処理をbuttonタグに追記します。

<button class="btn btn-info m-2" @click="insert">save</button>
setup(props) {
 const data = reactive({
   title: "",
   content: "",
   store: useStore(),
 });
 const insert = () => {
   data.store.commit("insert", { title: data.title, content: data.content });
   data.title = "";
   data.content = "";
 };
 return { data, insert };
},

setupの中を書き換えます。title、content、storeをreactiveの中で用意します。storeには、useStore()を使って、storeの中をこのプロパティで参照できるようにしています。
メソッドinsertを定義します。メソッドの処理内容は、insertのmutationを呼び出す事です。(この辺、同じ名前だとわかりにくいですね。。。)

スクリーンショット 2022-03-16 11.27.49

titleとMemoに値を入力してsaveボタンを押すと、storeの配列memoに値が入り、console.logで出力される事が確認できました。

(確認できたら、console.logは消します)


■5件表示

次に、画面上に5件表示できるようにします。

Memo.vue

<ul class="list-group">
 <li
   v-for="item in page_items"
   v-bind:key="item.title"
   class="list-group-item list-group-item-action test-left"
 >
   {{ item.title }}({{ item.created }})
 </li>
</ul>

liタグに繰り返し処理を追加します。page_itemsの分だけ繰り返すようにします。page_itemは、後ほど算術プロパティとして定義します。


const data = reactive({
 title: "",
 content: "",
 num_per_page: 5,
 store: useStore(),
});

num_per_page:5をdataに追記します。この値で、同時に出力される件数を決めることが出来るようにします。


const page_items = computed(() => {
 return data.store.state.memo.slice(
   data.num_per_page * data.store.state.page,
   data.num_per_page * (data.store.state.page + 1)
 );
});

算術プロパティpage_itemsを定義します。

storeの中にあるmemoの内、5件を取得し、returnします。ここの処理が、少しわかりにくかったです。。

今の時点では、data.num_per_pageは5が、data.store.state.pageは、0が入っています。よって、sliceの中の計算をすると、

return data.store.state.memo.slice(0,5)

となり、配列memoから0から5をsliceしてreturnします。
arr.slice(start,end)は、配列arrを、startからendのひとつ前まで(endは含まれない)切り取る。


returnにpage_itemsを追加する。(このreturnする処理、いつも忘れる。。)

return { data, insert, page_items};

スクリーンショット 2022-03-16 13.06.52

見てみるとこんな感じに、5件表示される。


■prev、nextのボタンを動くようにする

次に、prevとnextのボタンが動くようにします。

基本的には、stateにあるpageの値を変更すればOKです。

ただ、配列memoの中の値の数や、0より小さくならないようにする処理が少しややこしいです。

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

まずは、各ボタンにイベントを設定します。


//次のページ
const next = () => {
 page.value++;
};

//前へページ
const prev = () => {
 page.value--;
};

各々の処理は、後ほど算術プロパティで定義するpageの値をインクリメント、デクリメントする内容です。

算術プロパティpageを定義します。今回は、getとsetで処理を分けます。

getは、単純にstateからmemoの値を取得します。

setは、処理が少しややこしいです。
最終的には、set_pageというmutationを、変数pgを引数にして呼び出します。

//表示ページを表す値
const page = computed({
 get: () => {
   return data.store.state.page;
 },
 set: (p) => {
   let 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);
 },
});

まず、

Math.ceil((data.store.state.memo.length - 1) / data.num_per_page) - 1

この記述で、pageに入るべき最大の大きさを、配列memoの数から計算している。

例えば、配列memoの数が8件なら、

Math.ceil((8-1) / 5)-1
=Math.ceil(1.4)-1
=2-1
=1

こんな感じ。

そして、setに渡されたpの数が、最大値より超えるなら計算した最大値を、そうじゃないなら、pの数をそのまま変数pgに代入する。

次に、

pg = pg < 0 ? 0 : pg;

 この処理でpgの値が0より小さいなら0を、そうじゃないならpgの値をそのまま 再度pgに代入している。


マウントされたら、set_pageに0を代入する

    //マウント時の処理
   onMounted(() => {
     data.store.commit("set_page", 0);
   });


nextとprevをreturnに追加

return { data, insert, next, prev, page_items };


これでnextとprevが動く

スクリーンショット 2022-03-16 23.09.17

スクリーンショット 2022-03-16 23.09.48


■検索

検索の機能をつける。

<button class="btn btn-primary col-2 ml-2" @click="find">find</button>

findボタンに、クリックした時の処理を追記する。


//検索の設定
const find = () => {
 data.find_flg = true;
};

findメソッドを追加する。処理内容は、後述するfind_flgをtrueにすること。


const data = reactive({
 title: "",
 content: "",
 num_per_page: 5,
 find_flg: false,
 store: useStore(),
});

dataの中に、find_flgを追加


//ページの表示項目
   const page_items = computed(() => {
     if (data.find_flg) {
       let arr = [];
       let rec = data.store.state.memo;
       rec.forEach((element) => {
         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)
       );
     }
   });

算術プロパティのpage_itemsが、最終的に出力される配列を決めているので、ここの処理を分岐させていく。

data.find_flgがtrueだったら、配列memoを一度全部繰り返し処理し、その中で、titleがdata.titleと等しいものだけを別の配列にpushして、その配列をreturnする。

最後にreturnにfindを追加する

return { data, find, insert, next, prev, page_items };


■削除

今度は、選んだメモを削除する仕組みを作る。

表示されるメモをクリックした時の処理を追加する。

<li
 v-for="item in page_items"
 @click="select(item)"
 v-bind:key="item.title"
 class="list-group-item list-group-item-action test-left"
>
 {{ item.title }}({{ item.created }})
</li>


必要なプロパティを、dataに追加する。

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


クリックした時の処理。

//項目の選択
const select = (item) => {
 data.find_flg = false;
 data.sel_flg = true;
 data.title = item.title;
 data.content = item.content;
 data.sel_item = item;
};

フラグ用に用意したプロパティや、titleなどを変更する。

sel_itemにitem毎代入し、最終的にこの値をmutationに渡すことになる。


削除ボタンを作る。

<transition name="del">
 <button
   class="btn btn-info m-2"
   v-if="data.sel_flg != false"
   @click="remove"
 >
   delete
 </button>
</transition>

data.sel_flgがtrueの時だけ、削除ボタンが表示されるようにする。

クリックした時の処理。

//選択項目の削除
const remove = () => {
 if (data.sel_flg) {
   data.store.commit("remove", data.sel_item);
 }
 set_flg();
};

mutation、removeをdata.sel_itemを引数にして呼び出す。先ほどの処理で、data.sel_itemには、選択したmemoの値が入っている。


store.js

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!--' + obj.title);
           state.memo.splice(i, 1);
           return
       }
   }
}

store.jsにremoveのmutationを追加する。内容は、memoの配列を調べて送られたobjの値(sel_item)のtitle、content、createdが等しいものをmemoから削除するようになっている。


<input
 type="text"
 name="title"
 class="form-control col-9 ml-2"
 v-model="data.title"
 @focus="set_flg"
/>

Memo.vueのtitleのinputをフォーカスした時に、フラグの値を初期値に戻す処理を追加する。

//フラグの初期化
const set_flg = () => {
 if (data.find_flg.value || data.sel_flg != false) {
   data.find_flg = false;
   data.sel_flg = false;
   data.title = "";
   data.content = "";
 }
};

ひとまずこれで完成。


■完成版のコード

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>Vite App</title>
 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
   integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
 <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
   integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
   crossorigin="anonymous"></script>
 <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
   integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
   crossorigin="anonymous"></script>
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js"
   integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"
   crossorigin="anonymous"></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>


main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import { store } from './store'

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


App.vue

<template>
 <memo />
</template>

<script>
import Memo from "./components/Memo.vue";

export default {
 name: "App",
 components: {
   Memo,
 },
};
</script>


memo.vue

<template>
 <section class="alert alert-primary">
   <div class="form-control-group row">
     <label for="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 class="btn btn-primary col-2 ml-2" @click="find">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)"
       v-bind:key="item.title"
       class="list-group-item list-group-item-action test-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 select = (item) => {
     data.find_flg = false;
     data.sel_flg = true;
     data.title = item.title;
     data.content = item.content;
     data.sel_item = item;
   };
   //フラグの初期化
   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 find = () => {
     data.find_flg = true;
     data.sel_flg = false;
   };
   //メモの追加
   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 page_items = computed(() => {
     if (data.find_flg) {
       let arr = [];
       let rec = data.store.state.memo;
       rec.forEach((element) => {
         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: () => {
       console.log("aaa");
       return data.store.state.page;
     },
     set: (p) => {
       console.log(p);
       let 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, select, find, insert, remove, next, prev, page_items };
 },
};
</script>


■まとめ

今回は、めちゃめちゃ長い記事になっちゃいました。。。。まぁ大半はコードですけど。

コードを全部一気に書いちゃうと、何がどのように関係するのかわからないので、なるべく少しずつ書くのが良いですね。

今回のサンプルで、各々の動きの確認が出来ました。

出来上がったアプリ自体も、まぁまぁ使えそうですw

この記事が気に入ったらサポートをしてみませんか?