mt_coff's log

メモとか雑に

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以下にAtomsMoleculesOrganismsPagesを作成しておきましょう。それぞれのディレクトリー以下でコンポーネントを定義していくことになります。
なぜ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 ですし参考になればとても喜びます。

余談

冒頭に書いてあるプログラミング初心者は大嘘です(多分)

おわり

参考