LoginSignup
2
0

More than 3 years have passed since last update.

Vueのカスタムコンポーネントで双方向データバインディングを入れてみた

Last updated at Posted at 2020-05-15

Vueのカスタムコンポーネントはすごく便利ですね。
HTMLのテンプレートとして使えて、しかも使う側がさらにHTMLを差し込むことができるのは重宝しています。

今回は、まずはカスタムコンポーネントを単純なテンプレートとして使う例を示した後、さらに汎用的にするために、カスタムコンポーネントを双方向データバインディングに対応させます。

ちなみに、ここらへんVueの方々が頑張っていただいているようで、仕様が変わる(使いやすくなる)ことがありますので、その際にはまた追従したいと思います。

最後に、自作のswagger定義ファイルエディタを紹介しています。

単純なコンポーネントの例:HTMLテンプレートとして使う

HTMLのテンプレートとして使い、使う側がさらにHTMLを差し込む例です。
期待はこんな感じです。

使う側はこんな風に書きます。

<custom-template>
    使う側が差し込みたいHTML
</custom-template>

そして、テンプレート側ではこんな感じで記載しておきます。

<div class="panel panel-default">
    <slot></slot>
</div>

最終的に、こんな感じに合成したいです。

<div class="panel panel-default">
    使う側が差し込みたいHTML
</div>

実際のテンプレート側は、以下のようなコードとなります。

Vue.component('custom-template-01', {
    template: `
        <div class="panel panel-default">
            <slot></slot>
        </div>`,
});

そうすると、使う側がこうすると、

<custom-template-01>
  <div class="panel-body">
    Hello World
  </div>
</custom-template-01>

使う側には以下のように合成されて見えるようになります。テンプレートに置き換わり、かつ <slot></slot>が挿し代わっています。

    <div class="panel panel-default">
      <div class="panel-body">
        Hello World
      </div>
    </div>

ちなみに、使う側が差し込んだHTMLは使う側の制御範囲なので、v-modelなどそのまま使えます。
例えばこんな感じ。(使う側です)

    <label>data</label> {{data}}
    <br>
    <label>custom_template_01_a</label>
    <custom-template-01>
      <div class="panel-body">
        <input type="text" v-model="data" class="form-control">
      </div>
    </custom-template-01>

以降では、使う側を親、使われる側(HTMLテンプレート側)を子と呼ぶようにします。

HTMLテンプレート例でのデータバインディング

さきほどの、HTMLテンプレート例では、子の<slot></slot>の内容をすべて親に任せています。一方で、親からのパラメータを使って子が内容を作成したいことが多々あります。

そこで、もう一つ例を挙げます。まず子の方です。

Vue.component('custom-template-02', {
    props: ['header'],
    template: `
        <div class="panel">
            <div class="panel-heading">
                <h4 class="panel-title">{{header}}</h4>
            </div>
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <slot name="footer"></slot>
            </div>
        </div>`,
});

親の方はこちらです。

    <custom-template-02 class="panel-default" header="This is Header">
      <template>
        Hello World
      </template>
      <template v-slot:footer>
        since 2020
      </template>
    </custom-template-02>

この例では、最初の例に比較して、追加の仕組みを3つ使っています。

・1つめ:差し込み先のslotに名前を付けました。

テンプレート内のname=footerの属性 が付いたslotエレメントが、v-slot:footer の属性を付けたtemplateエレメントの中身に置き換わります。footerという名前は自由に決められます。名前を付けたことで、差し込む場所を複数作ることができるようになります。

・2つめ:親から子にパラメータを渡しています。

子に、props: [ ‘header’] というプロパティが増えています。
これは、子側は親側からheaderというプロパティを受け取ることを宣言しています。

親は、header="This is Header" という感じで、カスタムコンポーネントの要素にheaderを追加していますので、それを子が受け取ることができています。
受け取った値は、

            <div class="modal-header">
                <h4 class="modal-title">{{header}}</h4>
            </div>

のような感じで、HTMLの中に含めているのがわかります。
これにより、すべてを親側に任せるだけでなく、親からパラメータを取得して子側でHTMLに反映することができます。

・3つ目:親で指定した属性が子のHTMLに渡されます。

class="panel-default" の部分ですが、headerと異なり、子側で受け取る準備(props指定)をしていません。
その場合、HTMLのテンプレートのルートのエレメントの属性として追加されます。

テンプレート上は、

        <div class="panel">

となっていますが、実際のHTMLに描画されたときには、

        <div class="panel panel-default">

となります。子側にclassがすでに指定済みですので、子側のclassに親からのclassの属性値が追加された形になっています。当然ながら、propsに指定済みの属性は除きます。

親と子の双方向データバインディング

propsを使って親のデータを子に渡しました。
また、slotを使って子がHTMLを生成する一部を親に任せることができました。
しかしながら、今まで説明してきた方法では子で処理した結果を親が受け取ることはできません。

何をいっているわからないかもしれませんが、Vueで必ず使う双方向データバインディングであるv-modelが使えていないということです。

inout_textのエディットボックスで入力した値を初期値とし、カスタムコンポーネント側でダイアログを表示して初期値を表示し、別の値を入力してもらって、その値をinout_textのエディットボックスに表示したい例です。
以下が期待する親のHTMLです。

    <input type="text" v-model="inout_text" class="form-control">

    <custom-template-03 class="panel-default" header="This is Header" v-model="inout_text">
      <template>
        Input Dialog Test
      </template>
    </custom-template-03>

少し分解すると、Vueとしてv-model=”inout_text”は以下に置き換えられます。

  v-bind:value=”input_text” v-on:input=”inout_text”

valueという名前で子に渡し、inputというイベントで親に返してもらえればよいわけです。

まず、valueの子への渡し方はすでに説明しました。
propsにvalueを追加すればよいだけです。
残るは、inputイベントです。

それには、子で以下を呼べばよいのです。

this.$emit(input, 返したい値)

それを反映したのがこちら。

Vue.component('custom-template-03', {
    props: ['value'],
    template: `
        <div class="panel">
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <button class="btn btn-default" v-on:click="do_input">do_input</button>
            </div>
        </div>`,
    methods:{
        do_input: function(){
            var ret = window.prompt('入力してください。', this.value);
            if( ret )
                this.$emit('input', ret);
        }
    }
});

親から受け取ったthis.valueを子で書き換えたら、そのまま親にイベントが伝わってほしいかもしれません。ですが、this.valueやemitで、親からの子のデータバインディングと子から親へのイベントを組み合わせて実現しているだけで、それぞれの要素は、片方向でしかないのです。

子と孫の双方向データバインディング

HTMLテンプレート例では、親が子のカスタムコンポーネントを使っていました。
実際には、親が子のカスタムコンポーネントを使って、子がさらに孫のカスタムコンポーネントを使って、というように、階層的につながっていく場合が多々あります。
そうすると、子の中でもv-modelを使いたくなります。

まず、孫から。

Vue.component('custom-template-04-a', {
    props: ['value'],
    template: `
        <div>
            <button class="btn btn-default" v-on:click="do_input">do_input</button>
        </div>
    `,
    methods:{
        do_input: function(){
            var ret = window.prompt('入力してください。', this.value);
            if( ret )
                this.$emit('input', ret);
        }
    }
});

さきほど子側で、入力ダイアログを出していた部分を抜き出したものです。
次が子側です。孫に対してv-modelを使っています。

Vue.component('custom-template-04', {
    props: ['value'],
    template: `
        <div class="panel">
            <div class="panel-body">
                <slot></slot>
            </div>
            <div class="panel-footer">
                <custom-template-04-a v-model="value_"></custom-template-04-a>
            </div>
        </div>`,
    data: function(){
      return {
          value_: this.value,
      }
    },
    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }
});

親から受け取ったthis.valueは、親から子への片方向専用であり、子の中での孫との双方向データバインディングには使うことができません。
そこで、親から受け取ったthis.valueをthis.value_にコピーして、それを孫との双方向データバインディングに使っています。

    data: function(){
      return {
          value_: this.value,
      }
    },

の部分で、孫とのv-modelに使う変数を宣言し、(コンポーネントの場合、dataは関数で返さないといけないです)

    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },

によって、親からvalueの変更通知を受け取ったら、this.value_に再コピーしています。
一方で、以下の孫のカスタムコンポーネント側でthis.value_値の更新通知が来ます。

    <custom-template-04-a v-model="value_"></custom-template-04-a>

そのイベントも、watchでthis.value_を監視することでフックしています。
トリガーされると、親に変更された新しい値を伝えています。

    watch: {
・・・
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }

まとめると、以下の形を覚えておけば、親-子-孫の間の双方向データバインディングを実現できそうです。

Vue.component('カスタムコンポーネント名', {
    props: ['value'],
    template: `
   表示したいHTML
    `,
    data: function(){
      return {
          value_: this.value,
      }
    },
    watch: {
        value: function(newValue){
            this.value_ = newValue;
        },
        value_: function(newValue){
            this.$emit('input', newValue);
        }
    }
});

以上でVueコンポーネントの実験は終わりです。

以下に、上記を確認できるページを用意しました。

Vueコンポーネント実験室
 https://poruruba.github.io/vuecomp_laboratory/labo_01/

image.png

サンプルアプリ:Swagger定義ファイルエディタ

さきほど作った双方向データバインディングの定型を使って、Swagger定義ファイルのエディタを作ってみました。

Web上から、エントリポイントを作成したり、メソッドを追加して、複数のパラメータを作ったりしできるようにしました。
(かなり端折っていますし、バグもたくさんあるとは思いますが、自己満足です。。。)

画面はこんな感じです。
お決まりの、Petstoreをサンプルとして読み込むボタンを用意したので、なんとなくイメージはできるかもしれません。

image.png

image.png

poruruba/vuecomp_laboratory
 https://github.com/poruruba/vuecomp_laboratory/

以下からアクセスできます。
 https://poruruba.github.io/vuecomp_laboratory/swagger_editor/

以上

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0