Vue.js入門 ―最速で作るシンプルなWebアプリケーション

第3回Vue.jsでコンポーネント開発

はじめに

第3回では、Vue.jsのコンポーネントの基本的な作成の仕方と使用方法を紹介します。

Vue.jsは、UIをコンポーネント化する仕組みを持っています。HTML+CSS+JavaSciptで構築されたUIの再利用性が高まり、カプセル化されて開発で意識すべき範囲を限定できるようになります。今回の記事が、プロジェクトにVue.jsを導入する際のコンポーネント設計のイメージをつかむ助けになれば幸いです。

Vue.jsのコンポーネント指向

Vue.jsのコンポーネント指向について

Vue.jsのコンポーネントは、大まかにWeb ComponentCustom Elementsの仕様に沿って設計されています。コンポーネントは、定義したタグ名で親となるコンポーネントのHTML上に記述できます。

大規模なアプリケーションを作成する際は、コンポーネントをツリー状に構成してわかりやすく設計することが可能です。

基本的なcomponent

まず、基本的なVue.jsのコンポーネントの書き方を示します。

Vue.component('fruits-list-title', {
  template: '<h1>フルーツ一覧</h1>'
})

この例では、<h1>フルーツ一覧</h1>のHTML要素を含んだコンポーネントを、fruits-list-titleという名前で登録しています。登録したコンポーネントを別のコンポーネントから使用するには、以下のように親となるコンポーネントの中にHTMLタグを書きます。

<div id="fruits-list">
  <fruits-list-title></fruits-list-title>
</div>

このように構成した場合、実際にレンダリングされるHTML要素は、以下のようになります。

<div id="fruits-list">
  <h1>フルーツ一覧</h1>
</div>

上記の例ではグローバルのVueにコンポーネントを、Vue.component(tag, constructor)の形式で追加していましたが、ある特定のVueコンポーネントの中でのみ使えるように子コンポーネントを登録することもできます。Vue.extend()を使用してコンポーネントを作成した上で、親となるコンポーネントのcomponentsというoptionの中に以下のように登録してください。

var fruitsListChild = Vue.extend({
  template: '<h1>フルーツ一覧</h1>'
})

var fruitsListParent = Vue.extend({
  template: '<div>親コンポーネント<fruits-list-child></fruits-list-child></div>',
  components: {
    'fruits-list-child': fruitsListChild
  }
})

new Vue({
    el: "#fruits-list",
  components: {
    'fruits-list-parent': fruitsListParent
  }
})
<div id="fruits-list">
  <fruits-list-parent></fruits-list-parent>
</div>

jsfiddleで実行するとわかりますが、fruits-list-childコンポーネントはfruits-list-parentコンポーネントのスコープ内に定義されています。そのため、以下のHTMLファイルのようにfruits-list-childコンポーネントをfruits-list-parentの外側で指定しても、実行されません。

<div id="fruits-list">
  <fruits-list-child></fruits-list-child>
  <fruits-list-parent></fruits-list-parent>
</div>

コンポーネントのdataの扱い

各インスタンスごとに異なるdataオブジェクトを定義したいとき、dataオブジェクトは1つのコンポーネントの全てのインスタンスで共有されてしまいます。そのためVueコンポーネントの中のdataはオブジェクトを返す関数にします。

data: function(){
  return {
    fruits: [/* */]
  }
}

コンポーネント間の通信

親コンポーネントと子コンポーネントのデータのやりとりを解説します。

図1 コンポーネント間の通信図
図1 コンポーネント間の通信図
Vue.js公式ドキュメントから引用。

親コンポーネントが子コンポーネントへデータを渡す際には、propsを利用します。

例を見ていきましょう。以下のように、フルーツの名前をリストするコンポーネントを作成します。ここでは配列fruitsItemsに入ったフルーツの名前fruitsItem.nameをリストするテンプレートを含んだコンポーネントを、fruits-listという名前でVueインスタンスに登録しています。

Vue.component('fruits-list', {
  props: ['fruitsItem'],
  template: '<li>{{fruitsItem.name}}</li>'
});

配列fruitsは親コンポーネントのdataで定義します。

new Vue({
  el: '#fruits-component',
  data: {
    fruitsItems: [
      {name: '梨'},
      {name: 'イチゴ'}
    ]
  }
});

上記のように子コンポーネントのpropsオプションに変数名fruitsItemsを追加したうえで、HTML上の子コンポーネントのfruits-listタグの属性にfruits-itemsを記述します。すると、親コンポーネントから子コンポーネントに値を渡せます。

propsにキャメルケースでfruitsItemsと書いた場合、テンプレート側にはケバブケースでfruits-itemsと書きます。

<div id="fruits-component">
  <ol>
    <fruits-list v-for="fruit in fruitsItems" fruits-item="fruit"></fruits-list>
  </ol>
</div>

ここで、親のdataの変更を子に伝えるには、以下のようにv-bindディレクティブを使用すると良いでしょう。

<fruits-list v-for="fruit in fruitsItems" v-bind:fruits-item="fruit"></fruits-list>

このように記述すると、親コンポーネントでfruitsの値が更新されるたびに、子コンポーネントのpropsに書いたfruitsItemsが更新されます。なお、v-bindは省略できます。

<fruits-list v-for="fruit in fruitsItems" :fruits-items="fruits"></fruits-list>

レンダリングされるHTML要素は以下のようになります。

<div id="fruits-component">
  <ol>
    <li>梨</li>
    <li>イチゴ</li>
  </ol>
</div>

ここまでのサンプルコードは、こちらのjsfiddleで確認できます。

子コンポーネントから親コンポーネントへの通信では、カスタムイベントを使用します。Vueインスタンスには、以下のようなイベントのインターフェイスが実装されています。

  • イベントのlisten: $on(eventName)
  • イベントのtrigger: $emit(eventName)

 $dispatchや$broadcastを紹介する記事を読んだことのある方もいるかもしれませんが、Vue.js 2.0では非推奨となりました。

こちらも例を見ていきましょう。以下のようにcounter-buttonコンポーネントが定義されているとします。ボタンを押すとこのコンポーネントのaddToCartメソッドが呼ばれ、その中でincrementというカスタムイベントが発行されます。親コンポーネント側では v-on:increment(increment)incrementイベントをlistenしているため、ボタンを押した時に親コンポーネントのincrementメソッドが呼ばれます。

var counterButton = Vue.component( {
  template: '<span>{{counter}}個<button v-on:click="addToCart">追加</button></span>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    addToCart: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
});

new Vue({
  el: '#fruits-counter',
  components:
    'counter-button': counterButton
  },
  data: {
    total: 0,
    fruits: [
      {name: '梨'},
      {name: 'イチゴ'}
    ]
  },
  methods: {
    increment: function () {
      this.total += 1
    }
  }
});
<div id="fruits-counter">
  <div v-for="fruit in fruits">
    {{fruit.name}}: <counter-button v-on:increment="increment()"></counter-button>
  </div>
  <p>合計: {{total}}</p>
</div>

ここまでのサンプルコードは、こちらのjsfiddleで実行できます。

大規模なアプリケーションを作成していると、親と子の関係以外にもコンポーネントがさまざまな関係を持つことがあります。複雑なケースについては、ストアというオブジェクトに状態を持たせてそこで管理する方法(ストアパターン)が有効です。詳しくはこちらのドキュメントを参照してください。

コンポーネントの作成

ログインフォームコンポーネントの作成

ここまでの知識を踏まえてログインフォームを作成しましょう。まずtemplateはログインIDとパスワードのinputフォームを用意し、それぞれv-modelでコンポーネントのデータとバインドしておきます。ボタンにはv-on:click(login)と記述し、クリックイベントが発生した際にloginメソッドが呼ばれるようにしておきます。

<div id="login-template">
  <input type="text" placeholder="ログインID" v-model="userid">
  <input type="password" placeholder="パスワード" v-model="password">
  <button v-on:click="login">ログイン</button>
</div>

コンポーネントのdataにはuser_idpasswordを返す関数を定義し、methodにはloginメソッドを定義しています。なお、ここでは解説のために、認証の仕組みはauth.loginという仮の関数を置いています。

var auth = {
  login: function(id, pass){
    window.alert("login id:" + id + "\n" + "password:" + pass);
  }
}

var loginTemplate = `
  <div>
    <input type="text" placeholder="ログインID" v-model="userid">
    <input type="password" placeholder="パスワード" v-model="password">
    <button v-on:click="login">ログイン</button>
  </div>
`
Vue.component('user-login', {
  template: loginTemplate,
  data: function(){
    return {
      userid: '',
      password: ''
    }
  },
  methods: {
    login: function(){
      auth.login(this.userid, this.password);
    }
  }
});

以上で簡単なログイン用のコンポーネントが作成できました。このコンポーネントを、親となるコンポーネントの中で以下のように呼びだすと、ログインのUIコンポーネントを再利用することができます。

new Vue({
  el: "#login-example"
});
<div id="login-example">
  <user-login></user-login>
</div>

ここまでのサンプルコードは、このURLから確認できます。

ヘッダーコンポーネントの作成

より実践的な例として、Webアプリケーションのヘッダーを、再利用可能なVue.jsのコンポーネントとして作成してみましょう。ヘッダーはアプリケーションの各ページに共通して置かれることが多いため、コンポーネント化しておくと便利です。なお、ヘッダーの内容は親のコンポーネントによって変化するものとします。

まずは、あるpageというコンポーネントの中に、ヘッダーとページのコンテンツをそれぞれ子コンポーネントとして構成した例を示します。

<page>
  <page-header></page-header>
  <!-- ページのコンテンツ -->
</page>

ここで、親のコンポーネントごとに子のコンポーネントの内容を書きかえるためにcontent distributionという仕組みを使用します。Vue.jsのcontent distributionのAPIは、Webコンポーネントの仕様のドラフトに基づいて設計されたものです。

たとえば、ヘッダーのコンポーネントpage-headerコンポーネントを定義するとします。

var headerTemplate = `
        <div style="background: white;">
        <slot name="header"></slot>
   </div>
`
Vue.component('page-header', {
        template: headerTemplate
});

上記のようにヘッダーのコンポーネントの中にslotというタグを埋め込みます。こうすることで親コンポーネントを使用する際に、以下のように子コンポーネントに対して埋め込みを行えます。

<div>
   <page-header>
     <h1 slot="header">冬の果物</h1>
   </page-header>
  <ul>
    <li>りんご</li>
    <li>イチゴ</li>
  </ul>
</div>

上記のように、親のコンポーネントを使用する際に、slot属性に対応する子コンポーネントのslotタグのname属性を指定することで、子コンポーネントのコンテンツをカスタマイズすることができます。レンダリングされるHTMLは以下のようになります。

   <div style="background: white;">
     <h1>冬の果物</h1>
   </div>
    <li>りんご</li>
    <li>イチゴ</li>
  </ul>
</div>

ここまでのサンプルコードは、このURLから確認できます。

まとめ

いかがでしたか。ここまでVue.jsのコンポーネントの基礎について駆け足で紹介しましたが、Vue.jsのコンポーネントについてより詳しく知りたい方は、公式のドキュメントを参照してください。

次回は、Vue.jsを使用したシングルページアプリケーションの作成について紹介しますので、どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧