Vue.js のコンポーネントを使って親子のデータの送受信をする

フロント開発がもっと楽しくなる!Vue.js 学習メモ。コンポーネントを使ってサイトの部品を作っていきます。

Vue.js のコンポーネント

コンポーネントは機能を細分化・カプセル化することで、Vue.js では Web Components の思想に沿ってコンテンツを作れるのが特徴みたいです。Web Components は現状 Polyfill を使って実装しますが、それと同じような事をしてるのですね。Web Components 自体まだ仕様が固まっているわけじゃないみたいですが将来的に Web Components 的なものがスタンダードになった時移行するのもスムーズになりそう。

Vue.js のコンポーネントには、JavaScript はもとより HTML や CSS も付属できます。

コンポーネントの使いどころ

例えば私のサイトのようなブログでは、一つのページにヘッダー、フッター、メニュー、記事、というようなまとまりがあります。さらに記事の中にはタグなどネストされた部品もあります。これをひとつひとつ独立させる事が出来ます。

細かくコンポーネント化させることで機能やデータ、HTML/CSS 要素をカプセル化できてメンテナンス性が上がったり、必要な場面で単体で使いまわす事も出来るようになります。この図を作るのにも使ってるイラレのシンボルにも似てますね~!

コンポーネントの名前の付け方

コンポーネントは自分で名前をつけたカスタムタグで配置する事ができますが、HTML の制約でケバブケース(小文字でハイフン区切り)で記述する必要があります。また Web components の規則だと1つ以上のハイフンを含む必要がありますが、Vue のマニュアルには文字列テンプレートの場合は特に制約は無いよという事です。文字列テンプレートは以下の事を指すみたいです。

  • <script type="text/x-template">
  • JavaScript のインラインテンプレート文字列(template: '~' で書いたもの)
  • .vue コンポーネント
<my-component />

このような自己終了の書き方も出来る。

コンポーネントの書き方

コンポーネントは Vue のインスタンスと同じようにデータやメソッドと、独自のテンプレートを持つことが出来ますが、少し違うのはコンポーネントのデータはオブジェクトではなく関数を返さないといけないという事でこれはインスタンス毎に別々のデータを持たせるためみたいです。 Vue.component で名前付きで作成するとグローバルに登録されるのでどこからでも使用する事が出来ます。

Vue.component('タグ名', { template: '<p>example</p>', data: function() {}});

下のよう Vue.extend を使って作成し components プロパティに名前を付けて登録することで子コンポーネントとして使用することも可能というか、こっちの方が使う場面が多そうです。

var child = Vue.extend({ template: '<p>example</p>' })
new Vue({
components: {
'child-component': child
}
});

親子間でデータを受け取るコンポーネントを作る

早速ですが今後絶対的に必要になりそうなスコープを跨いだ親子間のデータの参照をしてみます。

<div id="app1">
    <div class="my-component-box">
        <h3>ここは親のスコープ</h3>
        <p>{{ childtext }}</p>
        <my-component v-bind:parenttext="parenttext"
v-on:send-text="getChildText"></my-component>
    </div>
</div>
var myComponent = Vue.extend({
    props: ['parenttext'],
    template: '<div class="my-component-box"><h3>ここは子のスコープ</h3>\
                  <p>{{ parenttext }}</p></div>',
    data: function() {
        return {
            childtext: 'これは子のデータだよ'
        }
    },
    created: function() {
        this.$emit('send-text', this.childtext);
    }
});
new Vue({
    el: '#app1',
    data: {
        parenttext:  'これは親のデータだよ',
        childtext: '',
    },
    components: {
        'my-component': myComponent
    },
    methods: {
        getChildText: function(text) {
            this.childtext = text;
        }
    }
});

親から子

子が親のデータを参照する時は子は props プロパティを使用して親は明示的にその値を渡すことで使えるようになります。(読み込みのみ)

子から親

親が子のプロパティを参照する方は、マニュアルの古いバージョンからの移行手順に $emit を使いましょう~と書いてあったのでそれを参考にして、子コンポーネントを配置する時にタグに v-on で send-text というカスタムイベントを登録しておいて、子が created のタイミングで自分に登録されているイベントを $emit で発火して、そうすると親側で登録したイベントハンドラの getChildText が実行されるという感じですね。

どちらも読み込みのみなので自分のデータではない値を変えたい場合はイベントを通す必要がありそうです。

実はこの $emit の使い方、親に登録して $parent を使って実行したりとか何度もいろんな書き方しまくってハマっていたのですがようやく理解しました…。

インタラクティブなコンポーネントを作る

もう少し実用的っぽい感じの例でフレンズ用のリストをコンポーネントを使って作ってみます。
「イイネ」を押すとカウンターが増えて算出プロパティでカウンターの数値順にソートされるようにします。

テンプレートの書き方も色々あるみたいでとりあえず普通に template の値に書く方法と、inline-template で直接書く方法を使いました。

<div id="app2">
    <friends-list inline-template>
        <div>
            <h3>Friends List</h3>
            <transition-group name="flip-list" tag="ul">
                <friends-profile
                    v-for="(friend, idx) in sortedFriends"
                    v-bind:val="friend"
                    v-bind:key="friend.id"
                    v-on:count-trigger='countUp(idx)'></friends-profile>
            </transition-group>
        </div>
    </friends-list>
</div>

マニュアルからコピペしただけですが組み込みコンポーネントの <transition-group> も使ってみました。組み込みのトランジションは基本 CSS でアニメーションをしているのですが、終わったタイミングで非表示を入れてくれるのか display:none; を気にせず簡単に使えてとってもたのしい!トランジションを使う場合は v-bind:key は必須みたいです。

/* プロフィール詳細用のコンポーネント */
var friendsProfile = Vue.extend({
    props: ['val'],
    template: '<li>name:<span :style="{color:val.color}">{{ val.name }}</span> [{{ val.count }}]\
               <a href="#" @click.prevent="$emit(\'count-trigger\')">イイネ</a></li>',
});
/* プロフィールリスト用のコンポーネント */
var friendsList = Vue.extend({
    data: function() {
        return {
            friends: [
                { id: '1', name: 'サーバル', color: '#e69313', 'count': 0 },
                { id: '2', name: 'フェネック', color: '#a7a264', 'count': 0 },
                { id: '3', name: 'アライグマ', color: '#bbbbbb', 'count': 0 },
            ]
        }
    },
    computed: {
        sortedFriends: function () {
            return this.friends.sort(function (a, b) {
                if (a.count < b.count) return 1;
                if (a.count > b.count) return -1;
                return 0;
            });
        }
    },
    components: {
        'friends-profile': friendsProfile
    },
    methods: {
        countUp: function(idx) {
            this.friends[idx].count++;
        }
    }
});
/* ルートインスタンス */
var app2 = new Vue({
    el: '#app2',
    components: {
        'friends-list': friendsList
    }
});

一応期待通りに動いてくれてました!これでカウンターを Ajax 使って 保存&取得したりすると割りとそれなりな感じになりますね。

コンポーネントに v-model を使う

上ではカスタムイベントを作成していますが、コンポーネントに v-model で値を紐付けておくと通常のフォームのイベントがそのまま使用できるようです。上の例を少し書き換えてやってみます。

<div id="app3">
    <friends-list inline-template>
        <div>
            <h3>Friends List</h3>
            <transition-group name="flip-list" tag="ul">
                <friends-profile
                    v-for="(friend, idx) in sortedFriends"
                    :val="friend"
                    :key="friend.id"
                    v-model="friend.count"></friends-profile>
            </transition-group>
        </div>
    </friends-list>
</div>

friends-profile の v-model に friend.count を結びつけてみました。

/* プロフィール詳細用のコンポーネント */
var friendsProfile = Vue.extend({
    props: ['val'],
    template: '<li>name:<span :style="{color:val.color}">{{ val.name }}</span> [{{ val.count }}]\
               <a href="#" @click.prevent="$emit(\'input\', val.count + 1)">イイネ</a></li>',
});
/* プロフィールリスト用のコンポーネント */
var friendsList = Vue.extend({
    data: function() {
        return {
            friends: [
                { id: '1', name: 'サーバル', color: '#e69313', 'count': 0 },
                { id: '2', name: 'フェネック', color: '#a7a264', 'count': 0 },
                { id: '3', name: 'アライグマ', color: '#bbbbbb', 'count': 0 },
            ]
        }
    },
    computed: {
        sortedFriends: function () {
            return this.friends.sort(function (a, b) {
                if (a.count < b.count) return 1;
                if (a.count > b.count) return -1;
                return 0;
            });
        }
    },
    components: {
        'friends-profile': friendsProfile
    },
});
/* ルートインスタンス */
new Vue({
    el: '#app3',
    components: {
        'friends-list': friendsList
    }
});

friends-profile コンポーネントの中から input イベントを発火させると friend.count が入力された事になります。

$emit('input', val.count + 1)

親から props で受け取っているカウント数 val.count は常に新しい数値で更新されていくので、この数値に+1するとどんどんカウントが増えていく感じですね。リアルタイムに反映させたい場合はこっちの方がシンプルに書けそうです。ボタンを押した時に反映するとか送信を遅延させたい場合は、親データとは分離した内部データで持っておいて送信したいタイミングになったら input やカスタムイベントでその値を送信するのが Good みたいです。

まだまだこれでいいのかな?みたいな不安な所もありますが、どういう書き方が一番いいのか記事やサンプルを色々読んで身につけていきたいです(•ө•)ノ

しかし、だんだん長くなってきたテンプレートをスクリプト内に書くのは結構大変ですね…。次回はちょっと気が早いですが、もっと快適に Vue を書くための開発環境を作りたいと思います。

Edited on 2017.03.19 Created on 2017.02.16 JavaScript Vue.js Webデザイン