今回は JavaScript フレームワークに Vue.js を使い、コンポーネント設計は Atomic Design を取り入れてみました。
*作ったもの
下記の画面を作成しました。まだ画面遷移や動的な表示といった処理を行うことはできません。
*環境
- MacOS
- vue/cli 3.8.4
- vue-router 3.0.6
- node-sass 4.12.0
- sass-loader 7.1.0
- axios 0.19.0
*環境構築
下記記事の「Vue CLI 3 のインストール 〜 Vue router の導入」を行い、Vueプロジェクトを作成します。*Sass の導入
今回は Sass を使いたいので下記コマンドを実行して Vue ファイルから Sass を使えるようにします。$ npm install --save node-sass sass-loader axios
プロジェクト作成と同時に生成される
HelloWorld.vue
で Sass の動作確認をしてみます。<style scoped>
にlang="scss"
を追加し、style
にhello__footer
クラスを追加します。次に
<template>
のなかにhello__footer
クラスを適用したdiv
要素を追加してアプリケーションを起動すると、画面下に「footer」の文字が青色で表示されます。<HelloWorld.vue>
<template>
...
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
<!---- ↓追加 ---->
<div class="hello__footer">footer</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
}
};
</script>
<!---- ↓ langを追加 ---->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
// ---- ↓追加 ----
.hello {
&__footer {
color: #0000ff;
}
}
</style>
*コンポーネント設計
Atomic Design を参考に下記構成にしました。より最適な設計があるかもしれないです。
manage-app
├── README.md
├── babel.config.js
├── node_modules
├── package-lock.json
├── package.json
├── public
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── atoms
│ │ ├── AppLogo.vue
│ │ ├── CardNumber.vue
│ │ ├── CardTitle.vue
│ │ └── LoginLink.vue
│ ├── molecules
│ │ ├── GraphCard.vue
│ │ ├── ListCard.vue
│ │ └── ValueCard.vue
│ ├── organisms
│ │ ├── Navbar.vue
│ │ └── Sidebar.vue
│ ├── pages
│ │ ├── Dashboard.vue
│ │ └── HelloWorld.vue
│ └── templates
├── main.js
└── router
└── index.js
*ダッシュボード画面
メインとなるダッシュボード画面を実装します。この画面を親コンポーネントとし、ナビゲーションバーなどの子コンポーネントを呼び出します。ナビゲーションバーとサイドバーはスクロールしても画面から外れないよう固定表示にしたので、コンテンツ表示部はその分を
padding
で調整しています。子コンポーネントを使う際は、
<script>
内で対象ファイルをimport
し、components
オプションとして登録します。そうすることで登録したコンポーネントをタグとして使うことができるようになります。(タグとして使うときはケバブケースにします)また、プロパティを使って子コンポーネントに値を渡すこともできます。
<src/components/pages/Dashboard.vue>
<template>
<div>
<navbar></navbar>
<sidebar></sidebar>
<div class="top">
<div>
<value-card name="ユーザー数 合計" value="256"></value-card>
<value-card name="プレビュー数 合計" value="21,234"></value-card>
</div>
<graph-card name="ユーザー数 推移"></graph-card>
<graph-card name="プレビュー数 推移"></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";
export default {
components: {
Navbar,
Sidebar,
ValueCard,
GraphCard,
ListCard
}
};
</script>
<style scoped lang="scss">
.top {
padding: 66px 0 0 200px;
}
</style>
プロジェクト作成時のままだと http://localhost:8080 にアクセスした際に
HelloWorld.vue
が呼び出されてしまうので、DashBoard.vue
が呼ばれるよう index.js
を修正します。<src/router/index.js>
import Vue from "vue";
import Router from "vue-router";
import Dashboard from "@/components/pages/Dashboard";
Vue.use(Router);
export default new Router({
mode: "history",
routes: [
{
path: "/",
name: "Dashboard",
component: Dashboard
}
]
});
*ナビゲーションバー
画面の上部に表示するナビゲーションバーを実装します。スクロールしてもナビゲーションバーがついてくるよう
position: fixed
を使って固定しています。メニューについては箇条書きに使うul, li
タグを使い、display: inline-block
を使って要素を横並びにしています。このままだとli
タグに箇条書きの「・」がついたままになってしまうので、list-style: none
を使って消しています。また、ロゴとログインユーザー名は別コンポーネントとして読み込んでいます。
<src/components/organisms/Navbar.vue>
<template>
<nav class="nav-header">
<ul class="nav-header__menu">
<app-logo value="Web Analysis"></app-logo>
<li class="current"><a href="#">ホーム</a></li>
<li><a href="#">月毎</a></li>
<li><a href="#">日毎</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
}
};
</script>
<style scoped lang="scss">
.nav-header {
text-align: center;
height: 67px;
background-color: white;
position: fixed;
width: 100%;
}
.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>
ナビゲーションバー左上のロゴは
atoms
コンポーネントとして切り出しました。props
で受け取った値をロゴ名として表示しています。<src/components/atoms/AppLogo.vue>
<template>
<li class="logo-label">{{ value }}</li>
</template>
<script>
export default {
props: ["value"]
};
</script>
<style scoped lang="scss">
.logo-label {
font-size: 20px;
font-weight: bold;
width: 200px;
vertical-align: center;
color: #3373d3;
border-right: 2px solid #ddd;
}
</style>
ナビゲーションバー右のログインユーザー名を表示する部分は、
props
で親からユーザー名を受け取って表示するようにしています。また、ユーザー名をクリックするとログアウトできるようアコーディオンメニューにしました。これはチェックボックスを隠し持っておき、ユーザー名がクリックされたタイミングで表示/非表示の切り替えを行います。
<src/components/atoms/LoginLink.vue>
<template>
<li class="login">
<input id="login-check1" class="login__check" type="checkbox">
<label class="login__label" for="login-check1">{{ value }}</label>
<div class="login__content">
<div class="login__menu">
<a class="login__link" href="#">ログアウト</a>
</div>
</div>
</li>
</template>
<script>
export default {
props: ["value"]
};
</script>
<style scoped lang="scss">
.login {
width: 200px;
position: absolute;
right: 0;
border-left: 2px solid #ddd;
height: 27px;
&__content {
box-shadow: 0 5px 3px -3px #ddd;
background-color: white;
height: 0;
opacity: 0;
padding: 0 12px;
transition: 0.5s;
visibility: hidden;
border-radius: 4px;
}
&__menu {
padding: 10px;
transition: 0.8s;
}
&__link {
color: #333;
text-decoration: none;
}
&__check {
display: none;
}
&__label {
width: 200px;
display: inline-block;
cursor: pointer;
}
&__check:checked + &__label + &__content {
height: 40px;
opacity: 1;
padding: 10px;
visibility: visible;
}
}
</style>
*サイドバー
画面の左に表示するサイドバーを実装します。項目にカーソルを当てると色が変わるようにしています。
<src/components/organisms/Sidebar.vue>
<template>
<ul class="side-menu">
<li><a href="#">ダッシュボード</a></li>
<li><a href="#">推移グラフ</a></li>
<li><a href="#">結果レポート</a></li>
<li><a href="#">設定</a></li>
<li><a href="#">問い合わせ</a></li>
</ul>
</template>
<script>
export default {};
</script>
<style scoped lang="scss">
.side-menu {
top: 74px;
position: fixed;
width: 200px;
height: 100vmax;
float: left;
background-color: #3373d3;
margin: 0;
padding: 15px 0 0 0;
& li a {
color: white;
font-weight: bold;
text-decoration: none;
}
& li {
padding: 25px 0 25px 35px;
list-style: none;
}
& li:hover {
background-color: #6ea3e9cb;
}
}
</style>
*カード
コンテンツとして表示するカードを実装します。タイトルとカード内部は複数箇所で使うので、別コンポーネントに切り出しました。
親コンポーネントからカードタイトルと表示内容を
props
で受け取り、その値をさらにvalue
という名前で子コンポーネントに渡しています。<src/components/molecules/ValueCard.vue>
<template>
<div class="value-content">
<card-title :value="name"></card-title>
<card-number :value="value"></card-number>
</div>
</template>
<script>
import CardTitle from "../atoms/CardTitle";
import CardNumber from "../atoms/CardNumber";
export default {
components: {
CardTitle,
CardNumber
},
props: {
name: String,
value: String
}
};
</script>
<style scoped lang="scss">
.value-content {
display: inline-block;
margin: 40px 0 40px 40px;
height: 130px;
width: 350px;
background-color: white;
border: 0 solid #aaa;
border-radius: 4px;
box-shadow: 2px 2px 2px 2px #ccc;
padding: 15px;
}
</style>
カードのタイトルを
atoms
コンポーネントにし、親からprops
で受け取った値を表示しています。<src/components/atoms/CardTitle.vue>
<template>
<div class="title-label">{{ value }}</div>
</template>
<script>
export default {
props: ["value"]
};
</script>
<style scoped lang="scss">
.title-label {
font-size: 20px;
font-weight: bold;
color: grey;
padding-bottom: 3px;
border-bottom: solid 2px #3280f57e;
}
</style>
カードの数値だけを表示する部分も
atoms
コンポーネントにしています。<src/components/atoms/CardNumber.vue>
<template>
<div class="number-label">{{ value }}</div>
</template>
<script>
export default {
props: ["value"]
};
</script>
<style scoped lang="scss">
.number-label {
text-align: center;
margin-top: 30px;
font-size: 40px;
}
</style>
*所感
CSSフレームワークは便利ですが、微調整がしにくかったり似たデザインになってしまうといったデメリットもあるので、CSS だけで柔軟な実装できるよう理解を深めていきたいと思っています。モバイルやタブレットの画面サイズの対応や、他画面のレイアウト、動的な処理の実装はこれから追加していきます。
Sign up here with your email
ConversionConversion EmoticonEmoticon