前回作成した Vueアプリケーションに、表示対象の月を変更できる機能を追加しました。また、合わせて下記機能も追加し、少し躓いた部分があるのでこの記事に書き残しておきます。
- ナビゲーションバーの月毎/日毎のページ
- ホーム画面のダッシュボードに月選択できる機能を追加
- ダッシュボードの折れ線グラフが動的に更新されるよう修正
- ユーザー数/プレビュー数の値が動的に更新されるよう修正(グラフの合計値)
サイドバーのダッシュボード以外のページと、サーバーサイドの処理については、この記事では未実装です。
現時点では、グラフのデータはランダムで作成しています。
*参考
- Vue.js でダッシュボードを作成③(Vue-chart.js でのグラフ描画編)
- Vue.js でダッシュボードを作成②(SVGアイコン作成編)
- Vue.js でダッシュボードを作成①(レイアウト作成〜Sass導入編)
*作ったもの
画像をクリックすると実際のアプリケーションが起動します。
*環境
- MacOS
- vue/cli 3.8.4
- vue-router 3.0.6
- npm 6.9.0
*目次
- 月毎/日毎のダッシュボード
- 月選択の矢印を追加
- 子コンポーネントの変更を親で受け取る
- 補足
- Herokuで公開
- 所感
*月毎/日毎のダッシュボード
ホーム画面のダッシュボードと同様に、月毎と日毎のダッシュボードのコンポーネントをそれぞれ追加します。- 既存:src/components/pages/Dashboard.vue
- 追加:src/components/pages/MonthlyDashboard.vue
- 追加:src/components/pages/DailyDashboard.vue
次にナビゲーションバーの項目をクリックすると、その項目がアクティブになり項目名の色が変わるようにします。そのためには
Navbar.vue
側でクリックされた項目を判別し、その項目のstyle
をCSS
で変更します。まず、ダッシュボード画面を区別できるよう
<navbar>
にbaseName
の値をprops
で渡します。<src/components/pages/Dashboard.vue>
<template>
<div>
<navbar baseName="daily"></navbar>
<sidebar></sidebar>
...
</div>
</template>
...
<src/components/pages/MonthlyDashboard.vue>
<template>
<div>
<navbar baseName="monthly"></navbar>
<sidebar></sidebar>
...
</template>
<src/components/pages/DailyDashboard.vue>
<template>
<div>
<navbar baseName="daily"></navbar>
<sidebar></sidebar>
...
</template>
Navbar.vue
のコンポーネントにsetActive()
を追加してクラスとバインドさせ、props
として渡された値が一致したli
タグのクラスを変更するようにします。<src/components/organisms/Navbar.vue>
<template>
<nav class="nav-header">
<ul class="nav-header__menu">
<app-logo value="Web Analysis"></app-logo>
<!---- ↓setActive()を追加 ---->
<li :class="setActive('home')"><a href="/">ホーム</a></li>
<li :class="setActive('monthly')"><a href="/monthly">月毎</a></li>
<li :class="setActive('daily')"><a href="/daily">日毎</a></li>
<login-link value="admin"></login-link>
</ul>
</nav>
</template>
<script>
import AppLogo from "../atoms/AppLogo";
import LoginLink from "../atoms/LoginLink";
export default {
components: {
AppLogo,
LoginLink
},
props: {
baseName: String
},
methods: {
setActive(item) {
return this.baseName === item ? "current" : "";
}
}
};
</script>
<style scoped lang="scss">
.nav-header {
text-align: center;
height: 67px;
background-color: white;
position: fixed;
width: 100%;
user-select: none;
}
.nav-header__menu {
padding: 20px 0;
margin: 0;
text-align: left;
& li {
list-style: none;
display: inline-block;
width: 200px;
min-width: 90px;
text-align: center;
}
& li a {
text-decoration: none;
color: #333;
}
& li.current a {
color: #3280f5;
}
& li a:hover {
color: #b1b6be;
}
}
</style>
*月選択の矢印を追加
新しく
SelectMonth.vue
というコンポーネントを作成し、ダッシュボード画面から呼び出すようにします。月の前後を選択する矢印はCSS
で作成しています。また、選択している年月は下記はまだ固定にしていますが、後に動的に修正します。<src/components/molecules/SelectMonth.vue>
<template>
<div>
<div class="select-date">
<a class="arrow back"></a>
<div class="month">2019/07</div>
<a class="arrow next"></a>
</div>
</div>
</template>
<script>
export default {
};
</script>
<style scoped lang="scss">
.select-date {
text-align: center;
max-width: 840px;
}
.month {
font-size: 35px;
color: grey;
font-weight: bold;
margin-top: 40px;
display: inline-block;
user-select: none;
}
.arrow {
position: relative;
display: inline-block;
padding: 0 0 0 24px;
color: #000;
vertical-align: middle;
text-decoration: none;
}
.arrow::before,
.arrow::after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
margin: auto;
content: "";
vertical-align: middle;
}
.back::before {
bottom: 11px;
width: 8px;
height: 8px;
border-top: 3px solid #00000052;
border-left: 3px solid #00000052;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.next::before {
left: 10px;
bottom: 11px;
width: 8px;
height: 8px;
border-top: 3px solid #00000052;
border-right: 3px solid #00000052;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
</style>
ダッシュボード画面から月選択コンポーネントを読み込みます。
<src/components/pages/Dashboard.vue>
<template>
<div>
<navbar baseName="home"></navbar>
<sidebar></sidebar>
<div class="top">
<div>
<!---- ↓追加 ---->
<select-month></select-month>
<value-card name="ユーザー数 合計" :value="userTotal"></value-card>
<value-card name="プレビュー数 合計" :value="previewTotal"></value-card>
</div>
<graph-card name="ユーザー数 推移" :data="userData" :label="labelData" :min="200" :max="600"></graph-card>
<graph-card name="プレビュー数 推移" :data="previewData" :label="labelData" :min="200" :max="600"></graph-card>
<list-card name="データ一覧"></list-card>
</div>
</div>
</template>
<script>
import Navbar from "../organisms/Navbar";
import Sidebar from "../organisms/Sidebar";
import ValueCard from "../molecules/ValueCard";
import GraphCard from "../molecules/GraphCard";
import ListCard from "../molecules/ListCard";
//---- ↓追加 ----
import SelectMonth from "../molecules/SelectMonth";
export default {
components: {
Navbar,
Sidebar,
ValueCard,
GraphCard,
ListCard,
SelectMonth // ← 追加
},
...
};
</script>
<style scoped lang="scss">
.top {
padding: 66px 0 0 200px;
}
</style>
*子コンポーネントの変更を親で受け取る
月選択の表示はSelectMonth.vue
で行いますが、DashBoard.vue
のグラフデータも選択した月のデータに更新されるようにします。DashBoard.vue <--年月-- SelectMonth.vue
DashBoard.vue --年月--> GraphCard.vue --> LineChart.vue
子の変更を親で受け取るには
$emit
を使います。子コンポーネントで変数名と値を$emit
に登録すると、親コンポーネントでその値を受け取ることができます。別の名前で複数登録することも可能です。- 使い方
<script>
export default {
data() {
return {
realDate: null
}
},
methods: {
set() {
// $emit(名前,値)に登録
this.$emit("date", this.realDate);
}
}
}
</script>
親コンポーンネントでは、子のカスタムタグに
v-on
ディレクティブ(省略形@
も可)を追加して$emit
の値を受け取るようにします。こうすることで、子から渡された値が更新されたときにsetMonthData()
が呼び出され、$emit
で登録した値を引数として受け取ることができます。<親コンポーネント>
<template>
<select-month @date="setMonthData"></select-month>
</template>
<script>
export default {
...
methods: {
setMonthData(date) {
this.daysNum = this.getDays(date);
}
}
}
</script>
実際のコードに追加します。
矢印をクリックしたときに月を変更し、その値を
$emit
に登録すれば親コンポーネントに年月を渡すことができます。クリックしたときの処理は、タグに@click
を追加し動作させたいメソッドを指定します。<src/components/molecules/SelectMonth.vue>
<template>
<div>
<div class="select-date">
<!---- ↓「<」をクリックすると beforeMonth()を呼び出し ---->
<a class="arrow back" @click="beforeMonth"></a>
<div class="month">{{ dispDate }}</div>
<!---- ↓「>」をクリックすると afterMonth()を呼び出し ---->
<a class="arrow next" @click="afterMonth"></a>
</div>
</div>
</template>
<script>
export default {
created() {
this.selectDate = this.thisMonth;
this.dispDate = this.getYearMonth(this.selectDate);
},
data() {
return {
realDate: null,
selectDate: null,
dispDate: null
};
},
computed: {
thisMonth() {
this.realDate = new Date();
const date = new Date();
date.setMonth(date.getMonth() + 1);
return date;
}
},
methods: {
getYearMonth(date) {
if (date.getMonth() === 0) {
return date.getFullYear() - 1 + "/12";
}
return date.getFullYear() + "/" + ("0" + date.getMonth()).slice(-2);
},
beforeMonth() {
this.realDate.setDate(1);
this.realDate.setMonth(this.realDate.getMonth() - 1);
this.selectDate.setDate(1);
this.selectDate.setMonth(this.selectDate.getMonth() - 1);
this.dispDate = this.getYearMonth(this.selectDate);
this.$emit("date", this.realDate);
},
afterMonth() {
this.realDate.setDate(1);
this.realDate.setMonth(this.realDate.getMonth() + 1);
this.selectDate.setDate(1);
this.selectDate.setMonth(this.selectDate.getMonth() + 1);
this.dispDate = this.getYearMonth(this.selectDate);
this.$emit("date", this.realDate);
}
}
};
</script>
<style scoped lang="scss">
.select-date {
text-align: center;
max-width: 840px;
}
.month {
font-size: 35px;
color: grey;
font-weight: bold;
margin-top: 40px;
display: inline-block;
user-select: none;
}
.arrow {
position: relative;
display: inline-block;
padding: 0 0 0 24px;
color: #000;
vertical-align: middle;
text-decoration: none;
}
.arrow::before,
.arrow::after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
margin: auto;
content: "";
vertical-align: middle;
}
.back::before {
bottom: 11px;
width: 8px;
height: 8px;
border-top: 3px solid #00000052;
border-left: 3px solid #00000052;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.next::before {
left: 10px;
bottom: 11px;
width: 8px;
height: 8px;
border-top: 3px solid #00000052;
border-right: 3px solid #00000052;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
</style>
月が更新されたら、ダッシュボード画面からチャートのデータを更新する処理を追加します。
setMonthData()
でグラフのデータとラベルを設定しています。<src/components/pages/Dashboard.vue>
<template>
<div>
<navbar baseName="home"></navbar>
<sidebar></sidebar>
<div class="top">
<div>
<select-month @date="setMonthData"></select-month>
<value-card name="ユーザー数 合計" :value="userTotal"></value-card>
<value-card name="プレビュー数 合計" :value="previewTotal"></value-card>
</div>
<graph-card name="ユーザー数 推移" :data="userData" :label="labelData" :min="200" :max="600"></graph-card>
<graph-card name="プレビュー数 推移" :data="previewData" :label="labelData" :min="200" :max="600"></graph-card>
<list-card name="データ一覧"></list-card>
</div>
</div>
</template>
<script>
import Navbar from "../organisms/Navbar";
import Sidebar from "../organisms/Sidebar";
import ValueCard from "../molecules/ValueCard";
import GraphCard from "../molecules/GraphCard";
import ListCard from "../molecules/ListCard";
import SelectMonth from "../molecules/SelectMonth";
export default {
components: {
Navbar,
Sidebar,
ValueCard,
GraphCard,
ListCard,
SelectMonth
},
data() {
return {
daysNum: 0,
userData: [],
previewData: [],
labelData: []
};
},
created() {
this.setMonthData(new Date());
},
computed: {
userTotal() {
return this.userData.reduce((prev, current) => prev + current);
},
previewTotal() {
return this.previewData.reduce((prev, current) => prev + current);
}
},
methods: {
getUserData() {
// TODO: fix getting from server data
return [...Array(this.daysNum)].map(
() => 300 + Math.floor(Math.random() * Math.floor(500 - 300))
);
},
getPreviewData() {
// TODO: fix getting from server data
return [...Array(this.daysNum)].map(
() => 300 + Math.floor(Math.random() * Math.floor(500 - 300))
);
},
getDays(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
},
getLabelList(date) {
date.setDate(0);
return [...Array(this.daysNum)].map(() => {
date.setDate(date.getDate() + 1);
return date.toLocaleDateString();
});
},
setMonthData(date) {
this.daysNum = this.getDays(date);
this.userData = this.getUserData();
this.previewData = this.getPreviewData();
this.labelData = this.getLabelList(date);
}
}
};
</script>
<style scoped lang="scss">
.top {
padding: 66px 0 0 200px;
}
</style>
*補足
- Date型
JavaScript
のDate
について補足です。getMonth()
は 1〜12月 が 0〜11 の値で取得されるので、今月の月を取得したい場合はgetMonth() + 1
をします。const date = new Date();
date.setMonth(date.getMonth() + 1);
また、月を0埋めの文字列で取得したかったので、
0
を連結してslice()
を使って最後の2桁を取得するようにしました。const date = new Date();
const month = ("0" + date.getMonth()).slice(-2);
- Vue-chartのチャートが更新されないとき
Chart.js
にはリアクティブでデータの更新をする機能が備わっていないため、vue-chart
についても再描画してくれません。このため、
vue-chart
からmixins
をインポートし、mixins.reactiveData
をミックスインすることでデータを監視し、リアクティブに再描画してくれるようになります。reactiveData
を使う際はchartData
という変数名が定義されるので、この変数名を使ってprops
で親から値を受け取り、renderChart()
に渡す必要があります。<script>
import { Line, mixins } from "vue-chartjs";
export default {
extends: Line,
mixins: [mixins.reactiveProp],
props: ["chartData", "options"],
mounted() {
this.renderChart(this.chartData, this.options);
}
};
</script>
- 数値リストの合計値を取得
reduce()
を使うと、各要素の値を1つの累積した値にすることができます。[1, 2, 3].reduce((prev, current) => {prev + current})
とすることで、リストの合計値を算出できます。下記ではリストの値をランダムで作成し、その値を合計しています。
<script>
export default {
data() {
return {
userData: [],
previewData: []
};
},
computed: {
getUserData() {
return [...Array(this.daysNum)].map(
() => 300 + Math.floor(Math.random() * Math.floor(500 - 300))
);
},
userTotal() {
return this.userData.reduce((prev, current) => prev + current);
}
}
};
</script>
- 3桁区切りの数値
Number
オブジェクトのtoLocaleString()
を使うことで、3桁区切りのカンマフォーマットにすることができます。const num = 123456;
console.log(num.toLocaleString())
=> 123,456
*Herokuで公開
こちらの記事を参考にさせていただきました。.gitignore
の/dist
を削除しておきます。また、
express
をインストールしておきます。$ npm install --save express
package.json
のscript
に"start": "node server.js"
を追加します。{
"name": "manage-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"start": "node server.js"
},
...
プロジェクト直下に
server.js
を新規作成します。コンパイルしたファイルを
dict
ディレクトリに作成するようにしています。<server.js>
const express = require("express");
const port = process.env.PORT || 8080;
const app = express();
app.use(express.static(__dirname + "/dist/"));
app.get(/.*/, function(req, res) {
res.sendfile(__dirname + "/dist/index.html");
});
app.listen(port);
console.log("Server is up!!");
下記コマンドを実行してコンパイルすると
dict
ディレクトリが作成されます。$ npm run build
ちなみに私の環境ではビルド時にエラーになったため、下記コマンドを実行してからビルドする必要がありました。(`terser`ライブラリに作り込まれたバグが原因なようです)
$npm i terser --save
Herokuにアプリを作成します。
$ heroku create {任意のアプリ名}
ここまでをコミットしてから、下記コマンドを実行して Heroku にデプロイします。
$ git push heroku master
アプリケーションの削除は下記コマンドを実行します。
$ heroku apps:destroy --app {アプリ名} --confirm {アプリ名}
*所感
computed
にグラフデータを作成したメソッドを定義したら、前の月と日数が変わらない場合は実行されない事象が発生しました。computed()
は使うデータに変更がないと実行せず、無駄な処理を行わないようになっているようです。今回はデータの変更ありなしに関わらず実行させたかったので、methods
にメソッドを定義することで解決しました。Vue.js は学習コストが比較的低く簡単に実装できますが、機能を理解していないと重大なバグにつながったりするので、よく理解して使えるようにしたいです。
Sign up here with your email
ConversionConversion EmoticonEmoticon