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">< prev</span>
<span class="btn btn-secondary ml-2">next ></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などはとりあえず最低限の物を書いておきます。
ブラウザで見るとこんな感じ。ここから少しずつ機能を付けていきます。
■フォームに入力された内容を配列に格納
フォームに入力された値は、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を呼び出す事です。(この辺、同じ名前だとわかりにくいですね。。。)
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};
見てみるとこんな感じに、5件表示される。
■prev、nextのボタンを動くようにする
次に、prevとnextのボタンが動くようにします。
基本的には、stateにあるpageの値を変更すればOKです。
ただ、配列memoの中の値の数や、0より小さくならないようにする処理が少しややこしいです。
<span class="btn btn-secondary mr-2" @click="prev">< prev</span>
<span class="btn btn-secondary ml-2" @click="next">next ></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が動く
■検索
検索の機能をつける。
<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">< prev</span>
<span class="btn btn-secondary ml-2" @click="next">next ></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
この記事が気に入ったらサポートをしてみませんか?