LoginSignup
25
23

More than 3 years have passed since last update.

Vue.jsのカスタムコンポーネントを拡張する

Last updated at Posted at 2020-01-10

Vue.jsのカスタムコンポーネントを拡張する

既存のコンポーネントの機能を生かしつつ、機能追加する方法のまとめ。
VuetifyのVTextFieldを拡張して、数値をカンマ区切りで表示する入力フィールドを作ります。

仕様

  • 入力(focus)時はtype="number"としてカンマなしの数値を入力する
  • 表示(非focus)時はtype="text"として3桁のカンマ区切りで表示する
  • valueの型はNumber|nullとする
  • v-modelでの双方向バインディングが出来るようにする

ラッパーコンポーネントの作成

まずプロパティ、イベント、スロットを透過的にVTextFieldに渡すコンポーネントを作成します。

プロパティ

プロパティをVTextFieldに受け渡すために属性の継承を無効化して、v-bindを指定します。

デフォルトの挙動はテンプレートのルート要素に属性が継承されますが、$propsに受け渡されるわけではなく、$attrsに直接渡されてしまうため、期待した動作をしません。
※この辺りの詳細は参考の記事に分かりやすくまとめられています

また、VTextFieldをルート以外の要素にしたい場合はデフォルトの継承では出来ないのでv-bindで渡す必要があります。

<template>
  <v-text-field
    v-bind="$attrs"
  >
  </v-text-field>
</template>

<script>
export default {
  inheritAttrs: false,
}
</script>

参考

イベント

v-onにイベントリスナーを受け渡します。
後のv-modelの実装を分かりやすくするため:value props listeners()を実装していますが、子がカスタムコンポーネントなので、inputイベントの内容を変更する必要が無ければv-on="$listeners"を追加するだけでも動作します。

<template>
  <v-text-field
    v-bind="$attrs"
    :value="value"
    v-on="listeners"
  >
  </v-text-field>
</template>

<script>
export default {
  inheritAttrs: false,
  props: ['value'],
  computed: {
    listeners() {
      const vm = this;
      return {
        ...vm.$listenres,
        // 子がカスタムイベントなのでeventに直接値が入っている
        input: event => vm.$emit('input', event)
      };
    },
  }
}
</script>

参考

スロット

$slotsからスロット名を取り出して、VTextFieldに渡します。

<template>
  <v-text-field
  >
    <template v-for="(value, name) in $slots" v-slot:[name]>
      <slot :name="name"/>
    </template>
  </v-text-field>
</template>

参考

これで作成したコンポーネントがVTextFieldの機能をすべて利用できるようになりました。

実装

続いて追加機能を実装します。

入力時はtype="number"、表示時はtype="text"にする。

dataに入力/表示の状態を保持するeditingを追加して、@focusin @focusoutで制御します。

<template>
  <v-text-field

    :type="type"
    @focusin="edit"
    @focusout="display"
  >

  </v-text-field>
</template>

<script>
export default {

  data() {
    return {
      editing: false
    };
  },
  computed: {

    type() {
      if (this.editing) {
        return "number";
      } else {
        return "text";
      }
    }
  },
  methods: {
    display() {
      this.editing = false;
    },
    edit() {
      this.editing = true;
    }
  }
};
</script>

valueの型をNumberに。入力時はカンマ無し、表示時はカンマ区切り。

valueをNumberにして、このコンポーネントが受け取る場合はNumber、VTextFieldに渡す場合はstringにします。
受け渡し用に算出プロパティのinnerValueを追加して、getterでNumber -> stringとカンマ区切りの変換、setterでstring -> Number の変換を行います。
算出プロパティを使うことでその中で使用している変数(value、editing)が変更された場合に算出プロパティに反映されます。

<template>
  <v-text-field
    v-bind="$attrs"
    :value="innerValue" // VTextFieldには算出プロパティを渡す
    v-on="listeners"

  >

  </v-text-field>
</template>

<script>
export default {
  inheritAttrs: false,
  props: {
    value: {
      type: Number,
      default: null
    }
  },
  data() {
    return {
      editing: false
    };
  },
  computed: {
    listeners() {
      const vm = this;
      return {
        ...vm.$listenres,
        input: event => vm.innerValue = event // 直接emitせずにinnerValueを通すように変更
      };
    },
    innerValue: {
      get() {
        if (this.editing) {
          return this.value != null ? this.value.toString() : "";
        } else {
          return this.value != null ? this.value.toLocaleString() : "";
        }
      },
      set(newValue) {
        const value = newValue.length > 0 ? Number(newValue) : null;
        this.$emit("input", value);
      }
    },
  },

};
</script>

これで、v-modelに値を渡せば双方向バインディングでリアルタイムに書き換わるようになります。

呼び出し元コンポーネント

<template>
  <div id="app">
    <my-number-field v-model="val" label="Number Field">
      <template v-slot:append></template>
    </my-number-field>
  </div>
</template>

<script>
import MyNumberField from "./components/MyNumberField.vue";

export default {
  name: "App",
  components: {
    MyNumberField
  },
  data() {
    return {
      val: 1000
    };
  },
}
</script>

コード

全体コードはこちら

25
23
1

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
25
23