Vue.jsでゆるくAtomic Designをやる
これは Aizu Advent Calendar 2018 の 22 日目の記事です。
20 日目は @4rotsugd さんでC++ゲーム開発録 - 1週間で音ゲー制作、23 日目は @yt8492 さんのRaspberry Pi 3でAndroid Things入門です。
adventar.org
アドベントカレンダーは初めてでプログラミング初心者なのでお手柔らかにお願いします 🙇🙇
🙇
遅れてしまってすみません!!なんでもはしないけど許して下さい。
はじめに
本記事では Vue.js を使って Atomic Design でゆるく UI を作っていきます。Vue.js についての詳細やロジカルな部分にはあまり触れないのでご了承ください。ざっくりやっていきます。結構雑な理解です。
Atomic Design とは
ざっくり説明すると Brad Fros 氏によって提唱されている UI の設計手法です。UI を
- Atoms
- Molecules
- Organisms
- Templates
- Pages
の5段階で構成します。上から下に行くに従って抽象的なインターフェースから具象的なインターフェースとなります。
以下で簡単にそれぞれどのようなものか説明します。
Atoms
全ての UI を構成する基礎で、例としてはフォームのボタンや入力欄などが挙げられ、UI として機能が壊れない最小の要素です。
Molecules
Atoms の組み合わせで構成されます。比較的単純で再利用可能なように作成します。単純な機能を持つことが可能です。
こちらでは検索欄が例に挙げられています。後述の Organisms に分類したくなるかもしれません。ただ、検索欄自体は複数箇所で使用が想定でき、配置することによって意味が明確するはずです。例えば、ヘッダーに配置すればページもしくはその Web サイト全体の検索だと直感的にわかりますし、ユーザー一覧の上部に配置されていればユーザーを検索するんだなということがわかるかと思います。(プロダクトによって変えるべきだとは思います。)
Organisms
Atoms と Molecules の組み合わせ構成され、コンテンツとして完結しているような比較的複雑な UI の構成要素となります。例としては ヘッダーやフッターなどが挙げられます。
Templates
Atoms、Molecules、Organisms を組み合わせてページの雛形とするのが Templates です。実際に画像や文章などは流し込ません。
Pages
Templates に実際にコンテンツを流し込んがものが Pages です。文章や画像など実際に表示することができます。イメージとしては Templates というクラスのインスタンスを生成してできたものが Page でしょうか。(ホンマか?)
Vue.js で Atomic Design を実践してみる
ここまでで Atomic Design についてざっくり(本当にざっくりと雑に)と説明が終わりました。ここで抑えていて欲しいのは Atomic Design はデザインのための手法であってコンポーネントを作成するための手法ではないということです。ですから実際にコーディングしていく上では具象的な層から抽象的な層のデザインを決定してみたり、状態をどこに持たせるか?など考えることは多いです。
今回は、できる限り CSS は各 Atoms で適用して、状態はコンテンツとして完成し始める Organisms から持たせるという方針でやりたいと思います。ここは個人やプロダクトに寄る部分になるかなと思います。
実際のソースコードは以下に置いておきます。(名前か被るコンポーネントは My をつけてますが実際のプロダクトではやらないでくださいね!)
github.com
セットアップなど
まずはプロジェクトを作成するところから始めましょう。vue cli
を使用してプロジェクトの雛形を作成します。
$ vue create project-name
でプロジェクトが作成できます。実行するといくつか設定をどうするか聞かれますがここは個人で好きなのを選択していいと思います。私はPrettier が入る構成にするのをおすすめします。(オートフォーマットが非常に便利なため)
$ vue add storybook
でStorybookを導入して置くとコンポーネントカタログを作成でき便利です。が今回は Storybook でのコンポーネントカタログの作成方法は述べません。基本的な初期設定はvue cli
がやってくれます。
そして/src/components
以下にAtoms
、Molecules
、Organisms
、Pages
を作成しておきましょう。それぞれのディレクトリー以下でコンポーネントを定義していくことになります。
なぜTemplate
を作らないかといえばデータの流し込みは Vue の機能(もし他のフラームワークを使用するならその機能)を使用してデータを流し込むためです。逆にTemplate
を利用すると冗長な形になります。
ポイント
2 点ほど実装に入る前に Tips があります。まず 1 つ目ですが Vue ではすでに HTML 要素で使用されている名前はコンポーネントの名前として使用できないことです。
2 つ目はv-model
を独自定義したコンポーネントで使用するにはvalue
属性をvalue
プロパティにバインドし、input
イベントにて新しい値で独自のinput
イベントを発行する必要があるということです(v-model
は@input
と:value
の糖衣構文)。具体的には以下のように定義します。(一例です。他にも記述方法はあります)
<template> <input :value="value" @input="onInput"></input> </template> <script> export default { name: 'my-input' props: ["value"], methods: { // 親からさらに親へ伝えるときはeventではなくnewValueを渡すと良い onInput(event) { this.$emit('input', event.target.value) } } } </script> // 上記を提起することで以下のように使用可能 <template> <my-input @v-model="hoge"></my-input> </template> <script> import MyInput from "path/MyInput.vue" export default { name:"fuga", components: { MyInput } } </script>
それでは実際に各 1 つずつコンポーネントの実装を追っていきましょう。
Atoms
input
は上記の例にあるので button
を使ってみたいと思います。以下のような形になると思います。
<template> <button class="button" @click="onClick"><slot /></button> </template> <script> export default { name: "my-button", methods: { onClick() { this.$emit("click"); } } }; </script> // スタイルは省略
クリックイベントの発火を親に伝えて親でイベントをハンドリングするようにするだけですね。簡単!<slot></slot>
を使うことにより<my-buton>hoge</my-buton>
のような形でボタンに表示するテキストを制御できます。
また、なぜthis.$emit("click")
しているかといえば親でイベントを制御しないと URL からどの動作をするか判断したり、イベントの数だけボタンを定義することになるためです。また、ボタンの色やサイズなどこの中で決定してもいいですし、共通のスタイルだけ当てて色やサイズは親コンポーネントで決定するとより柔軟に扱えると思います。
Molecules
リンクをリストにしたものを扱ってみましょう。
<template> <ul> <li v-for="(link, key) in linkList" :key="key"> <link-list-item :to="link">{{ key }}</link-list-item> </li> </ul> </template> <script> import LinkListItem from "@/components/Atoms/LinkListItem.vue"; export default { name: "link-list", components: { LinkListItem }, props: { linkList: { type: Object, required: true } } }; </script>
Atoms にrouter-link
をラップしたLinkList
コンポーネントがあることとします。単純に親から遷移先の名前と URL のペアの Array を受け取ってそれをv-for
を使用し、リストにしています。
Organisms
Molecules でみたLinkList
を使用してみましょう。
<template> <nav> <my-title>Menu</my-title> <link-list :link-list="linkList" /> </nav> </template> <script> import MyTitle from "@/components/Atoms/MyTitle.vue"; import LinkList from "@/components/Molecules/LinkList.vue"; export default { name: "side-nav", components: { MyTitle, LinkList }, data() { return { linkList: { home: "/", page1: "page1", page2: "page2", page3: "page3" } }; } }; </script>
こんな形でサイドメニューを定義してみました。MyTitle
コンポーネントは見出しとして使用する想定です。ここでようやく状態を持つことを考えます。このコンポーネントでは遷移先に関する情報をもたせています。API からのレスポンスでリンクを生成したりする際に、より上位のコンポーネントで取得しこのコンポーネントに渡してもいいですが、これはここだけで完結可能と考えることができるのでもしリンクを API から取得するのであればこのコンポーネントでの定義も問題ないです。場合によって使い分けましょう。
Pages
今までのものを組み合わせ、API 通信などを行ったりして各ページを作成するだけですので割愛させていただきます。ポイントとしては共通部分はくくりだしてベースとなっているファイル(例: App.vue
)に置いてみたり、必要なレイアウトが複雑であればレイアウトを Atoms として切り出してみたりすると良いかもしれません。
まとめ
簡単にですが Atomic Design についての説明と Vue.js による実装をしてきましたが、いかがでしたでしょうか?コンポーネントの分類が難しかったり、私自身の理解が甘かったりしますが一通りの概念を把握してもらえれば幸いです。実際のソースではこの記事内で紹介していないコンポーネントも実装してありますのでクソコードだと思っていただいても OK ですし参考になればとても喜びます。
余談
冒頭に書いてあるプログラミング初心者は大嘘です(多分)
おわり