Vue の基本的な使い方 (2) Composition API

Vue 3 の Composition API やカスタムディレクティブ、プラグイン(Element Plus、Vue I18n)の基本的な使い方、単純なプラグインの作成方法などについて。

関連ページ

作成日:2022年10月16日

Composition API の基本

以下は Composition API の基本的な使い方についての解説(覚書)です。扱っているサンプルはとても単純なもので、Vue は CDN 経由で読み込んでいます。

また、以下では主に https://v3.ja.vuejs.org/ のドキュメントを参考にしています。

現在の Vue の公式ドキュメントは Vue3 に対応していて、左上の「API 選択」ボタンで内容によっては Composition API と Options API を選択できるようになっています。

Composition API は Vue 3 で導入された大規模なコンポーネントを記述するのに適した API です。

従来の Options API では data や methods、computed など定義(オプション)の種類ごとにコードを記述しますが、Composition API ではコンポーネントの定義を setup メソッドを使ってコードを論理的なまとまりで記述することができるようになっています。

Composition API に関するよくある質問 | Composition API FAQ

Vue のドキュメントでは Options API と Composition API で異なる内容が含まる場合、左上の「API 選択」で内容を切り替えられるようになっています(例:リアクティビティーの基礎)。

以下は Options API で記述したボタンをクリックするとカウントを増加させるコンポーネントの例です。

HTML
<div id="app">
  <button v-on:click="increment">Count Up</button>
  <p>{{ count }}</p>
</div>

Options APIでは、オブジェクトプロパティとして data や methods などの役割(オプション)ごとに記述します。

JavaScript
Vue.createApp({
  // data オプション(リアクティブな変数を定義)
  data() {
    return {
      count: 0
    }
  },
  // methods オプション(イベントリスナー)
  methods: {
    increment() {
      this.count++
    }
  }
}).mount('#app');

以下は上記を Composition API を使って書き換えたものです。

Composition API ではコンポーネントに必要なもの(変数や関数)を setup メソッドで定義し、定義したオブジェクト(変数や関数)を return することでコンポーネントのテンプレート内でオブジェクトのプロパティにアクセスすることができます。

data

Options API ではリアクティブな変数は data オプションに定義しますが、Composition API では setup メソッド内で refreactive というメソッドを使用して定義し、最後にまとめて return します。

methods

Options API ではイベントリスナーは methods オプションに定義しますが、Composition API では setup メソッド内で関数として定義し、最後にまとめて return します。

Vue.createApp({
  //setup メソッド
  setup() {
    //リアクティブな変数(データオブジェクト)を宣言(data オプションに相当)
    const count = Vue.ref(0);

    //イベントリスナー( methods オプションに相当)
    const increment = () => {
      //ref には value プロパティで値にアクセス
      count.value ++;
    };

    //上記で定義したオブジェクトを戻り値にして返す(プロパティの短縮構文)
    return {
      count,  //count: count と同じ
      increment  //increment: increment と同じ
    }
  }
}).mount('#app');

以下も Options API を使ったコンポーネントの例です。

HTML
<div id="app">
  <counter-div v-bind:initial-count="0"></counter-div>
</div>

この例では、data オプションにプロパティ(props)の値を初期値として使うリアクティブな変数を定義し、methods オプションにイベントリスナーを定義しています。

JavaScript(Options API)
const app = Vue.createApp({});

app.component('counter-div', {
  // props オプション
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // template オプション
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  // data オプション
  data() {
    return {
      //プロパティの値を初期値として使うリアクティブな変数を定義
      count: this.initialCount
    }
  },
  // methods オプション
  methods: {
    countUp() {
      this.count++;
    },
    countDown() {
      this.count--;
    }
  }
});

app.mount('#app');

以下は上記を Composition API を使って書き換えた例です(props と template オプションは同じ)。

Composition API ではコンポーネントに必要なものを setup メソッドで定義しますが、プロパティ(props)やテンプレート、イベントなどは setup メソッドの外で定義します。

setup メソッドでは 2 つの引数(props と context)を受け取れるので、引数経由で props などにアクセスすることができます。

そして、setup メソッドで定義したものをまとめて戻り値にして返すことで、それらをコンポーネントの他のオプションで使用することができます。

以下の例の場合、setup メソッドで定義して返した count、countUp、countDown を template オプションで使用(参照)しています。

また、以下の場合、data オプションに相当するリアクティブな変数(count)は ref でリアクティブにしているので、その値には value プロパティ経由でアクセスします。

Composition API サンプル
const app = Vue.createApp({});

app.component('counter-div', {
  // props オプション(setup の外側で宣言)
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // template オプション(setup の外側で宣言)
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  // setup メソッド内でコンポーネントに必要なものを定義して返す
  setup(props, context) {
    // データオブジェクトを宣言( data オプションに相当)
    const count = Vue.ref(props.initialCount);
    // イベントリスナー(関数)を定義( methods オプションに相当)
    const countUp = () => {
      // value プロパティ経由で値を操作
      count.value ++;
    };
    // イベントリスナー(関数)を定義( methods オプションに相当)
    const countDown = () => {
      count.value --;
    }
    //上記で定義したものをまとめて戻り値にして返す(テンプレートに公開する)
    return {
      count,  //データオブジェクト( data オプションに相当)
      countUp, // イベントリスナー( methods オプションに相当)
      countDown // イベントリスナー( methods オプションに相当)
    }
  }
});

app.mount('#app');

setup

setup メソッド(コンポーネントオプション)は、コンポーネントが作成される前に props が解決されると実行され、 Composition API のエントリポイントとして機能します。

setup メソッドは他のコンポーネントオプションとは異なり、コンポーネントのインスタンスを参照しないため、setup の中で this にアクセスできません。

また、setup は data プロパティ、computed プロパティ、methods が解決される前に呼び出されるため、setup の中では利用できません。

setup メソッドは次のオプションのいずれかが評価される前に実行されます。

  • Components
  • Props
  • Data
  • Methods
  • Computed Properties
  • Lifecycle methods

セットアップ

setup は 2 つのオプションの引数(props と context)を受け取り、これらの引数を介して this を使って通常 アクセスするプロパティにアクセスすることができます。

setup(props, context) {
  //・・・コンポーネントに必要な変数や関数の定義・・・

  return {
    //定義したものを明示的に返す
    //ここで返されるものはコンポーネントの他のオプションで使用可能
  }
}

props

setup 関数の第 1 引数は props(プロパティ)です。props は明示的に宣言されたプロパティのみが含まれます。標準コンポーネントと同様、setup 関数内の props はリアクティブで、新しい props が渡されたら更新されます。

但し、props はリアクティブなので、ES6 の分割代入を使うことができません(分割代入を使うと、props のリアクティブを削除してしまいます)。props を分割代入する必要がある場合は、setup 関数内で toRefs(または toRef)を使います。

context

setup 関数に渡される第 2 引数は context(コンテキスト)です。

context は以下のようなプロパティを持つ JavaScript オブジェクトです。

  • attrs :属性の情報(インスタンスプロパティの $attrs に相当)
  • emit :イベントの情報(インスタンスプロパティの $emit に相当)
  • slots :スロットの情報(インスタンスプロパティの $slots に相当)
  • expose :明示的に公開するプロパティを制御する関数

context はリアクティブではないので、分割代入を安全に使用できます。

setup(props, { attrs, slots, emit, expose }) {
  ...
}

attrs と slots はステートフルなオブジェクトで、コンポーネント自身が更新されたとき、常に更新されます。そのため、分割代入の使用を避け、attrs.xxx や slots.xxx のようにプロパティを常に参照する必要があります。

また、props とは異なり、attrs と slots のプロパティは リアクティブではありません。もし、attrs か slots の変更による副作用を適用したい場合は、onBeforeUpdate ライフサイクルフックの中で行います。

コンポーネントプロパティへのアクセス

setup が実行されるとき、以下のプロパティにのみアクセスできるようになります。

  • props
  • attrs
  • slots
  • emit

以下のコンポーネントオプションには アクセスできません。

  • data
  • computed
  • methods
  • refs (template refs)

戻り値

定義したオブジェクト(変数や関数)を返して、他のコンポーネントのオプションから利用できるようにします(テンプレートに公開します)。

setup がオブジェクトを返す場合、コンポーネントのテンプレート内でオブジェクトのプロパティにアクセスすることができ、 setup に渡された props のプロパティも同じようにアクセスできます。

返される関数は methods と同様の振る舞いをします。

(Composition API サンプルと同じコード)
const app = Vue.createApp({});
app.component('counter-div', {
  // props オプション(setup の外側で宣言)
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // template オプション(setup の外側で宣言)
  // setup で返されたオブジェクトのプロパティ(count、countUp、countDown)にアクセス
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  // setup メソッド内でコンポーネントに必要なものを定義して返す
  setup(props, context) {
    // 引数の props を使ってプロパティにアクセス(ref メソッドでデータをリアクティブに)
    const count = Vue.ref(props.initialCount);
    // 関数を定義( methods オプションに相当)
    const countUp = () => {
      count.value ++;
    };
    // 関数を定義( methods オプションに相当)
    const countDown = () => {
      count.value --;
    }
    //定義したものを返すことで、コンポーネントの他のオプションで使用可能になります
    return {
      count,
      countUp,
      countDown
    }
  }
});

app.mount('#app');

28〜32行目は以下のように記述したのと同じことです(プロパティの短縮構文)。

return {
  count: count,
  countUp: countUp,
  countDown: countDown
}
emit

以下は Options API で $emit を使用する例です。

子コンポーネントでは、emits オプションに発行するイベント clickCount を宣言し、イベントリスナーの onclick で this.$emit('clickCount') により clickCount イベントを発生させています。

JavaScript(Options API)
const app = Vue.createApp({
  data() {
    return {
      count: 0
    };
  },
  methods: {
    onclickcount() {
      //clickCount 発生時にカウントを1増加
      this.count ++;
    },
  }
});

app.component('counter-button', {
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    count up
  </button>`,
  methods: {
    onclick() {
      //クリック時に clickCount イベントを発生
      this.$emit('clickCount');
    }
  }
});

app.mount('#app');

親コンポーネントでは、カスタムイベントを v-on:click-count で購読し、イベントリスナ onclickcount() で clickCount t 発生時にカウントを1増加しています。

HTML
<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="onclickcount"></counter-button>
</div>

以下は上記を Composition API で書き換えた例です(呼び出しの HTML は同じです)。

子コンポーネントでは、setup の第2引数 context のプロパティ emit を使って、イベントリスナーの onclick で context.emit('clickCount') により clickCount イベントを発生させています。

親コンポーネントのデータ count は ref メソッドで生成したオブジェクトなので、値は value プロパティを使います。

JavaScript(Composition API)
const app = Vue.createApp({
  setup() {
    //データ(ref でリアクティブに)
    const count = Vue.ref(0);
    // メソッド(clickCount 発生時にカウントを1増加。※ref の値は value プロパティを操作)
    const onclickcount = () => count.value++;
    //変数とメソッドを返す(テンプレートに公開)
    return {
      count,
      onclickcount,
    }
  }
});

app.component('counter-button', {
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    count up
  </button>`,

  // setup の第2引数 context から emit にアクセス
  setup(props, context) {
    const onclick = () => {
      //クリック時に clickCount イベントを発生
      context.emit('clickCount')
    }
    //定義したメソッドを返す(テンプレートに公開)
    return {
      onclick
    }
  }
});
app.mount('#app');

また、Options API 同様、以下のようにテンプレートで $emit を使って clickCount イベントを発行することもできます(5行目)。

app.component('counter-button', {
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="$emit('clickCount')">
    count up
  </button>`
});
app.mount('#app');

以下は Options API で $emit の第二引数を使ってリスナーに値を渡す例です。

この例では amount というプロパティ(クリック時に増減する値)を追加して、その値を $emit の第二引数に指定して親コンポーネントのコールバック関数に渡しています。

JavaScript(Options API)
const app = Vue.createApp({
  data() {
    return {
      count: 0
    };
  },
  methods: {
    //$emit の第二引数を、引数に受け取る(引数名は任意の文字列)
    onclickcount(value) {
      this.count += value;
    },
  }
});

app.component('counter-button', {
  //ボタンをクリックした際に増減する値を指定するプロパティ
  props: {
    amount: {
      type: Number, //数値型
      required: true  //必須
    }
  },
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    {{ amount }}
  </button>`,
  methods: {
    onclick() {
      //第二引数に親コンポーネントのコールバック関数に渡す値(データ)を指定
      this.$emit('clickCount', this.amount);
    }
  }
});

app.mount('#app');

プロパティ amount は数値として扱うので、v-bind: を使用します。

HTML
<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="onclickcount" v-bind:amount="100"></counter-button>
</div>

以下は上記を Composition API で書き換えた例です(呼び出しの HTML は同じです)。

emit の第二引数に指定した値は、親コンポーネントのコールバック関数の引数に渡されます。また、setup 内では props は、第1引数の props のプロパティ(props.amount)としてアクセスします。

JavaScript(Composition API)
const app = Vue.createApp({
  setup() {
    //データ(ref でリアクティブに)
    const count = Vue.ref(0);
    // メソッド(渡された値を引数に取る)
    const onclickcount = (value) => count.value += value;
    //変数とメソッドを返す
    return {
      count,
      onclickcount,
    }
  }
});

app.component('counter-button', {
  //ボタンをクリックした際に増減する値を指定するプロパティ
  props: {
    amount: {
      type: Number, //数値型
      required: true  //必須
    }
  },
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    {{ amount }}
  </button>`,

  //この例では、第2引数で分割代入を使って emit を取得
  setup(props, { emit }) {
    const onclick = () => {
      //第二引数に親コンポーネントのコールバック関数に渡す値(データ)を指定
      emit('clickCount', props.amount)
    }
    //定義したメソッドを返す
    return {
      onclick
    }
  }
});
app.mount('#app');

親コンポーネントでカスタムイベントを購読する際に、イベントハンドラを直接記述する場合は、Options API 同様、$event で clickCount イベントの値(emit の第二引数)にアクセスすることができます。

HTML
<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="count += $event" v-bind:amount="10"></counter-button>
</div>

上記の場合、親コンポーネントではイベントリスナは不要なので以下のようになります(子コンポーネントは同じなので省略)。

const app = Vue.createApp({
  setup() {
    //データ(ref でリアクティブに)
    const count = Vue.ref(0);
    //データを返す
    return {
      count,
    }
  }
});
expose

expose は公開するプロパティを明示的に制限するために使用することができる関数です。また、setup で Render 関数を返すような場合に、必要なプロパティを公開するために使用することができます。

Vue 3.2 より前では、Options API でメソッドやデータなどで宣言されたものはすべて、テンプレートがアクセスできるように公開されていました。Composition API についても同様で、setup メソッドから返されるものはすべて、親からテンプレート参照でアクセスできます。

例えば、以下の子コンポーネント my-counter の全てのメソッドには、親コンポーネントからテンプレート参照を使ってアクセスすることができます。

JavaScript
const app = Vue.createApp({});

app.component('my-counter', {
  template: `
  <p>Counter: {{ count }}</p>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
  <button @click="reset">Reset</button>
  `,
  setup() {
    //変数
    const count = Vue.ref(0);
    // メソッド
    const increment = () => count.value++;
    const decrement = () => count.value--;
    const reset = () => count.value = 0;
    //変数とメソッドを返す(テンプレートに公開)
    return {
      count,
      increment,
      decrement,
      reset,
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <my-counter></my-counter>
</div>
出力
<div id="app" data-v-app="">
  <p>Counter: 0</p>
  <button>Increment</button>
  <button>Decrement</button>
  <button>Reset</button>
</div>

以下は、親コンポーネントでテンプレート参照を使って子コンポーネントのメソッドにアクセスして、親コンポーネントでも子コンポーネントのメソッドを実装したボタンを追加する例です。

JavaScript
const app = Vue.createApp({
  setup() {
    //子コンポーネント側のコンポーネント情報を受け取る ref を宣言
    const counter = Vue.ref(null);  //または Vue.ref()
    // 子コンポーネント側の increment メソッドを発火させるメソッド
    const increaseCount = () => {
      // 公開されているメソッドを呼び出す
      counter.value.increment();
    };
    // decrement メソッドを発火させるメソッド
    const decreaseCount = () => {
      counter.value.decrement();
    };
    // reset メソッドを発火させるメソッド
    const resetCount = () => {
      counter.value.reset();
    };
    // テンプレートに公開
    return {
      counter,
      increaseCount,
      decreaseCount,
      resetCount
    };
  },
});

//子コンポーネントのコードは前述と同じ
app.component('my-counter', {
  template: `
  <p>Counter: {{ count }}</p>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
  <button @click="reset">Reset</button>
  `,
  setup () {
    const count = Vue.ref(0);
    const increment = () => count.value++;
    const decrement = () => count.value--;
    const reset = () => count.value = 0;
    return {
      count,
      increment,
      decrement,
      reset,
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <my-counter ref="counter"></my-counter>
  <div> <!-- 親のボタン  -->
    <button @click="increaseCount">Increment from parent</button>
    <button @click="decreaseCount">Decrement from parent</button>
    <button @click="resetCount">Reset from parent</button>
  </div>
</div>

この場合、親で追加したボタンからもカウントを操作することができます。

出力
<div id="app" data-v-app="">
  <p>Counter: 0</p>
  <button>Increment</button>
  <button>Decrement</button>
  <button>Reset</button>
  <div><!-- 親のボタン  -->
    <button>Increment from parent</button>
    <button>Decrement from parent</button>
    <button>Reset from parent</button>
  </div>
</div>

expose を使うと、親コンポーネントからテンプレート参照でコンポーネントインスタンスにアクセスする際に、公開するプロパティを明示的に制限することができます。

例えば、以下のようにすると、親からは reset にしかアクセスできなくなります。そのため、親で追加した「Increment from parent」のボタンをクリックすると、「Uncaught TypeError: counter.value.increment is not a function」のようなエラーになります。

app.component('my-counter', {
  template: `
  <p>Counter: {{ count }}</p>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
  <button @click="reset">Reset</button>
  `,
  //第2引数の context で expose にアクセス
  setup (props, context) {
    const count = Vue.ref(0);
    const increment = () => count.value++;
    const decrement = () => count.value--;
    const reset = () => count.value = 0;

    //公開するプロパティを reset のみに制限する
    context.expose({ reset });

    return {
      count,
      increment,
      decrement,
      reset,
    }
  }
});

以下は increment も親に公開する例です。

また、以下では setup の第2引数に分割代入で expose を取得しています。

app.component('my-counter', {
  template: `
  <p>Counter: {{ count }}</p>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
  <button @click="reset">Reset</button>
  `,
  //第2引数に分割代入を使って expose にアクセス
  setup (props, { expose }) {
    const count = Vue.ref(0);
    const increment = () => count.value++;
    const decrement = () => count.value--;
    const reset = () => count.value = 0;

    //公開するプロパティを reset と increment に制限する
    expose({ reset, increment });

    return {
      count,
      increment,
      decrement,
      reset,
    }
  }
});
app.mount('#app');

expose の引数を空にすれば、全てを親に公開しないようにできます。

setup(props, { expose }) {
  // 全てを親に公開しない(インスタンスを隠蔽)
  expose();
  ・・・
}
Render 関数での使用

setup は同じスコープで宣言されたリアクティブなステートを直接利用することができる Render 関数を返すこともできます(render オプション)。

以下はテンプレートを template オプションで定義しています。

JavaScript
const app = Vue.createApp({});
app.component('count-button', {
  //template オプションを利用
  template: `
    <button type="button" v-on:click="increment">
      Count: {{ count }}
    </button>`
  ,

  setup() {
    // データオブジェクトを宣言
    const count = Vue.ref(0);
    // イベントリスナー(関数)を定義
    const increment = () => ++count.value;
    // テンプレートに公開
    return {
      count,
      increment
    };
  }
});
app.mount('#app');
HTML
<div id="app">
  <count-button></count-button>
</div>

以下は上記を template オプションを使わずに Render 関数を返すように書き換えた例です。

JavaScript
const app = Vue.createApp({});
app.component('count-button', {

  setup() {
    const count = Vue.ref(0);
    const increment = () => ++count.value;

    // Render 関数を返す
    return () => Vue.h(
      'button',
      {
        type: "button",
        onClick: increment
      },
      'Count: ' + count.value
    );
  }
});

app.mount('#app');

h() 関数を直接返すとエラーになります。「h() 関数を返す関数」を返します。

以下のように別途 Render 関数を定義して記述することもできますが、通常は上記のように記述します。

const app = Vue.createApp({});
app.component('count-button', {

  setup() {
    const count = Vue.ref(0);
    const increment = () => ++count.value;

    // Render 関数
    const myRender = function() {
      return Vue.h(
        'button', //HTML タグ名
        {
          type: "button", // type 属性
          onClick: increment // v-on:click
        },
        'Count: ' + count.value  // 子ノード
      );
    }
    // Render 関数を返す
    return myRender ;
  }
});
app.mount('#app');

Render 関数では h() 関数を使ってテンプレートになる VNode を返しています。

h() 関数の第2引数には、テンプレートで使う属性、プロパティ、イベントに対応するオブジェクトを指定します(v-on:click の場合は、プロパティ名は onClick になります)。テンプレートの機能をプレーンな JavaScript で置き換える

いずれの場合も、出力は以下のようになります。

<div id="app" data-v-app="">
  <button type="button">Count: 0</button>
</div>

Render 関数を返す場合は、他のプロパティを返すことができなくなります。プロパティを公開して、外部からアクセスする必要がある場合は、expose を呼び出して、外部コンポーネントのインスタンスで利用可能なプロパティを定義したオブジェクトを渡します。

Render 関数での使用

以下は前述の例の count-button コンポーネントの increment メソッドを expose で公開して、親コンポーネントでテンプレート参照を使ってアクセスするように書き換えたものです。

HTML
<div id="app">
  <!-- ref の childRef を指定 -->
  <count-button ref="childRef" v-on:click="handleClick"></count-button>
</div>
const app = Vue.createApp({
  setup() {
    //子コンポーネント側のコンポーネント情報を受け取る ref を宣言
    const childRef = Vue.ref(null);  //または Vue.ref()
    // 子コンポーネント側のメソッドを発火させるメソッド
    const handleClick = () => {
      // 公開されているメソッドを呼び出す
      childRef.value.increment();
    };
    // テンプレートに公開
    return {
      childRef,
      handleClick,
    };
  },
});

app.component('count-button', {

  setup(props, { expose })  {
    const count = Vue.ref(0);
    const increment = () => ++count.value;

    // increment メソッドを公開
    expose({
      increment
    });

    return () => Vue.h(
      'button',
      {
        type: "button",
      },
      'Count: ' + count.value
    );
  }
});

app.mount('#app');

テンプレート機能(v-if や v-for など)を同等の render 関数で実装する例は以下に掲載されています。

イベントやキー修飾子は、withModifiers ヘルパーを使用することもできます。

slots

以下は Options API で $slots を使用する例です。

HTML
<div id="app">
  <base-layout>
    <template v-slot:header>
      <h1>Vue.js Sample</h1>
    </template>

    <template v-slot:default>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
      <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    </template>

    <template v-slot:footer>
      <p>Copyright ©xxxx. All rights reserved.</p>
    </template>
  </base-layout>
</div>

render 関数でコンポーネントを記述する際に、this.$slots を使ってそれぞれの名前付きスロットのコンテンツにアクセスしています。

h() 関数の引数の子要素(children)の指定では、this.$slots.header().length が true であれば (0でなければ)、スロットのコンテンツ this.$slots.header() とし、そうでなければデータオブジェクトのフォールバックコンテンツを指定しています。

JavaScript
const { createApp, h } = Vue
const app = createApp({})

app.component('base-layout', {
  data() {
    return {
      //スロットのフォールバックコンテンツ
      headerContent : h('h1', 'Default Header Content'),
      mainContent : h('p', 'Default Main Content'),
      footerContent : h('p', 'Default Footer Content')
    };
  },
  //h() で VNode を生成して返す
  render() {
    return h('div', {class:'container' },  [
      h('header',
        //this.$slots.header() でスロットのコンテンツにアクセス
        this.$slots.header().length ? this.$slots.header() : this.headerContent
      ),
      h('main',
        this.$slots.default().length ? this.$slots.default() : this.mainContent
      ),
      h('footer',
        this.$slots.footer().length ? this.$slots.footer() : this.footerContent
      )
    ])
  }
});
app.mount('#app');

以下は上記を Composition API で書き換えた例です。スロットの情報 slots は setup() の第2引数のプロパティ context.slotsとして受け取れますが、以下では分割代入で直接変数 slots に受け取っています。

JavaScript
const { createApp, h } = Vue
const app = createApp({})

app.component('base-layout', {
  // 分割代入で第2引数に slots を受け取る
  setup(props, { slots }) {
    //スロットのフォールバックコンテンツの定義
    const headerContent = h('h1', 'Default Header Content');
    const mainContent = h('p', 'Default Main Content');
    const footerContent = h('p', 'Default Footer Content');

    // Render 関数を返す
    return () => h('div', {class:'container' },  [
      h('header',
        slots.header().length ? slots.header() : headerContent
      ),
      h('main',
        slots.default().length ? slots.default() : mainContent
      ),
      h('footer',
        slots.footer().length ? slots.footer() : footerContent
      )
    ])
  }
});
app.mount('#app');

ref メソッド

ref メソッドを使うと、あらゆる変数をリアクティブにすることができ、ref メソッドを使ってリアクティブな(値の変更を検知できる)変数を定義することができます。

ref メソッドは引数を受け取って、それを value プロパティを持つミュータブルな ref オブジェクトでラップして返します。つまり、ref メソッドは値へのリアクティブな参照(refrence)を作成します。

const { ref } = Vue;

const counter = ref(0);  // counter は value プロパティを持つオブジェクト

ref で定義したリアクティブな変数の値にアクセスするには .value を使用します。

const counter = Vue.ref(0);

console.log(counter) // { value: 0 } オブジェクト(実際には以下のように出力されます)
//RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}

//value プロパティを使って変数の値にアクセス
console.log(counter.value) // 0

///value プロパティを使って変数の値を操作(増加)
counter.value++
console.log(counter.value) // 1

以下では、プロパティ(props)の値を ref でリアクティブ(ref オブジェクト)にしています。

setup 関数内の props はリアクティブですが、setup 関数内で const count = props.initialCount とすると、コンポーネントがマウントされた後には更新されない値となってしまうので、 ref でリアクティブにします(setup は、コンポーネントが作成される前に props が解決されると実行されます)。

(Composition API サンプルと同じコード)
const app = Vue.createApp({});
app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // テンプレートでは count はアンラップされるので count で値にアクセス
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  setup(props, context) {
    // 引数の props を使ってプロパティにアクセスし、その値を ref メソッドでリアクティブに
    const count = Vue.ref(props.initialCount);
    const countUp = () => {
      // value プロパティを使って変数の値を操作
      count.value ++;
    };
    const countDown = () => {
      count.value --;
    }
    return {
      count,
      countUp,
      countDown
    }
  }
});
app.mount('#app');

ref のアンラップ

ref メソッドでリアクティブ化された変数(Ref 変数)は、 setup() によって返されるオブジェクトのプロパティとして返されていてテンプレート内でアクセスされる場合、自動的に内部の値にアンラップ(ref でラップされた値を取り出す)されます。

このため、上記の場合、テンプレートでは count.value ではなく、count で値にアクセスします。

ref のアンラップ

ref の値としてオブジェクトが代入された場合

ref の値としてオブジェクトが代入された場合、内部では reactive メソッドによりそのオブジェクトは深いリアクティブになります。

reactive メソッド

reactive メソッドを使って、オブジェクトをリアクティブにすることができます。

reactive メソッドは、プリミティブ以外の値を受け取り、リアクティブなオブジェクトに変換します(オブジェクトのリアクティブなコピーを返します)。

リアクティブの変換は、全てのネストされたプロパティに対して影響を及ぼします。深い変換を避け、ルートレベルのリアクティビティーのみを保持するためには、代わりに shallowReactive() 使用します。

リアクティブの基礎

//通常のオブジェクトを受け取り、リアクティブオブジェクトに変換(リアクティブなコピーをす)
const obj = Vue.reactive({
  count: 1,
})

console.log(obj)  // Proxy {count: 1}
obj.count++
console.log(obj.count)  // 2

reactive メソッドは、引数としてオブジェクトを受け取り、そのメンバーをまとめてリアクティブに変換します(オブジェクトに複数のプロパティを指定できます)。

以下は前述の例の ref を reactive を使って書き換えたものです。

ref の場合は、value プロパティで値にアクセスしますが、reactive の場合は、定義したオブジェクトのプロパティで値にアクセスします。

const app = Vue.createApp({});

app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // 定義したオブジェクト(state)のプロパティ(count)でアクセス
  template: `<div>Count: {{ state.count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  setup(props, context) {
    //reactive メソッドにオブジェクトを渡してリアクティブ化
    const state = Vue.reactive({
      count: props.initialCount,
    });
    const countUp = () => {
      //オブジェクト(state)のプロパティ(count)でアクセス
      state.count ++;
    };
    const countDown = () => {
      state.count --;
    }
    return {
      state,
      countUp,
      countDown
    }
  }
});
分割代入などによるリアクティブの消失

reactive メソッドで生成したオブジェクトに分割代入スプレッド構文を適用して得た変数はリアクティブではなくなります。

const obj = Vue.reactive({
  count: 0,
});

// 分割代入により、count はリアクティブではなくなります
const { count } = obj;

例えば、前述の例の setup で以下のように分割代入を使ってしまうと、変数 count はリアクティブではなくなり、初期値は表示されますが、ボタンをクリックしても変更が反映されません(この例の場合、プロパティが1つなので、分割代入を使う意味はありませんが)。

setup(props, context) {
  //reactive メソッドにオブジェクトを渡してリアクティブ化
  const state = Vue.reactive({
    count: props.initialCount,
  });

  // count はリアクティブではない(分割代入で state.count を変数 count に代入)
  let { count } = state;

  //また、以下のようにプロパティを直接参照して代入してもリアクティブではなくなります
  //let count = state.count;  //上記の分割代入と同じこと

  const countUp = () => {
    //分割代入された変数 count でアクセス
    count ++;
  };
  const countDown = () => {
    count --;
  }
  return {
    state,
    countUp,
    countDown
  }
}

以下はスプレッド構文を使った例で、同様に変数 count はリアクティブではなくなります。

app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  //スプレッド構文で展開された count でアクセス(count はリアクティブではない)
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  setup(props, context) {
    //reactive メソッドにオブジェクトを渡してリアクティブ化
    const state = Vue.reactive({
      count: props.initialCount,
    });
    const countUp = () => {
      state.count ++;
    };
    const countDown = () => {
      state.count --;
    }
    return {
      ...state,   //スプレッド構文でプロパティを返す
      countUp,
      countDown
    }
  }
});

reactive メソッドで生成したオブジェクトに分割代入やスプレッド構文を使う場合は toRefs メソッドを使用します。

toRefs メソッド

toRefs メソッドは reactive で生成したオブジェクトの各プロパティを(元のオブジェクトの対応するプロパティを指す)ref に変換します。これらの ref は元となるオブジェクトへのリアクティブな接続を保持します。

const obj = Vue.reactive({
  count: 0,
});

// obj のプロパティ count は ref に変換され、リアクティビティが保たれます
const { count } = Vue.toRefs(obj);

前述の分割代入を使った例は、toRefs メソッドを使って以下のように書き換えれば、それぞれのプロパティが ref になっているので、リアクティビティが保たれ機能します。

変換されたプロパティは、ref なので .value で値にアクセスします。

app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  setup(props, context) {
    //reactive メソッドにオブジェクトを渡してリアクティブ化
    const state = Vue.reactive({
      count: props.initialCount,
    });

    // toRefs でオブジェクトの各プロパティを ref に変換し、分割代入で変数 count に代入
    let { count } = Vue.toRefs(state);

    const countUp = () => {
      // count は ref なので .value で値にアクセス
      count.value ++;
    };
    const countDown = () => {
      count.value --;
    }
    return {
      count,  //count を返す
      countUp,
      countDown
    }
  }
});

前述のスプレッド構文を使った例も、toRefs メソッドを使ってリアクティビティを保つことができます。

app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  //スプレッド構文で展開された count でアクセス
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  setup(props, context) {
    //reactive メソッドにオブジェクトを渡してリアクティブ化
    const state = Vue.reactive({
      count: props.initialCount,
    });

    const countUp = () => {
      state.count ++;
    };
    const countDown = () => {
      state.count --;
    }
    return {
      //toRefs でオブジェクトの各プロパティを ref に変換してスプレッド構文を適用
      ...Vue.toRefs(state),
      countUp,
      countDown
    }
  }
});

toRef メソッド

toRef メソッドを使うと、リアクティブなオブジェクトの特定のプロパティを ref として取り出すことができます。

const obj = Vue.reactive({
  count: 0,
});

// obj のプロパティ count を ref に変換
const count = Vue.toRef(obj, 'count');

例えば、以下のように、リアクティブなオブジェクトのプロパティを直接参照して変数に代入するとリアクティブではなくなります。

const state = Vue.reactive({
  count: props.initialCount,
});

// count はリアクティブではなくなります
let count = state.count; 

toRef メソッドを使えば、リアクティブなオブジェクトのプロパティを ref として取り出して、リアクティブを保ったまま変数に代入できます。

setup(props, context) {
  //reactive メソッドにオブジェクトを渡してリアクティブ化
  const state = Vue.reactive({
    count: props.initialCount,
  });

  //プロパティ count を ref として取り出す
  let count = Vue.toRef(state, 'count');

  const countUp = () => {
    // count は ref なので .value で値にアクセス
    count.value ++;
  };
  const countDown = () => {
    count.value --;
  }
  return {
    count,  //count を返す
    countUp,
    countDown
  }
}

toRefs と toRef

前述の例の場合、以下はほぼ同じことになります。

const state = Vue.reactive({
  count: props.initialCount,
});

//プロパティ count を ref として取り出す
let count = Vue.toRef(state, 'count');

//toRefs でオブジェクトの各プロパティを ref に変換し、分割代入で変数 count に代入
let { count } = Vue.toRefs(state);

但し、元のオブジェクトに対象のプロパティが存在しない場合は、結果が異なります。

toRef メソッドでは、値が空(undefined)の ref が生成されますが、toRefs メソッドでは undefined になります。

const obj = Vue.reactive({
  count: 0,
});

// obj の存在しないプロパティ foo を ref に変換(空の ref が生成される)
const foo = Vue.toRef(obj, 'foo');

console.log(foo);
//ObjectRefImpl {_object: Proxy, _key: 'foo', _defaultValue: undefined, __v_isRef: true}
const obj = Vue.reactive({
  count: 0,
});

// obj の存在しないプロパティ foo を ref に変換(undefined になる)
let { foo } = Vue.toRefs(obj);

console.log(foo);  //undefined

このため、オブジェクトが対象のプロパティを持たない可能性がある場合は、toRef を使います。

computed メソッド

Options API では、算出プロパティを定義するために computed オプションを使用しますが、Composition API では computed メソッドを使用します。

computed メソッドは、getter 関数を受け取り、getter からの戻り値に対してイミュータブルでリアクティブな ref オブジェクトを返します。computed メソッドの第一引数に指定した関数が getter として動作します。

以下は computed メソッドを使用して、データオブジェクト(count)の値を2倍にする例です。

setup メソッド内で computed メソッドを使用して算出プロパティを定義します。以下では computed メソッドの引数に count の値を2倍にして返す関数を指定しています。

そして、定義した算出プロパティを setup メソッドから return します。

テンプレート内では、setup メソッドから return した算出プロパティを利用できます。

JavaScript
const app = Vue.createApp({});
app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // テンプレートでは算出プロパティにアクセスできます
  template: `<div>Count: {{ count }} <br> Count の2倍 : {{ doubleCount }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  setup(props, context) {
    //プロパティの値を ref メソッドでリアクティブに
    const count = Vue.ref(props.initialCount);

    const countUp = () => {
      count.value ++;
    };
    const countDown = () => {
      count.value --;
    }
    //computed メソッドを使用して算出プロパティを定義
    const doubleCount = Vue.computed(() => {
      return count.value * 2;
    });

    //算出プロパティの値を変更しようとするとエラーになる(setter を用意すればOK)
    //doubleCount.value = 10; //Write operation failed: computed value is readonly

    return {
      count,
      countUp,
      countDown,
      doubleCount //算出プロパティ
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <counter-div :initial-count="0"></counter-div>
</div>

getter と setter

Options API の算出プロパティはデフォルトでは getter 関数のみですが、必要に応じて setter 関数を設定することもできます(算出 Setter 関数)。

Composition API の算出プロパティでも、setter 関数がサポートされています(Writable Computed)。

setter が必要な場合は、get と set をプロパティに持つオブジェクトを computed() の第一引数に指定します。

以下は前述の例の算出プロパティに setter を指定した例です。computed の値は ref 同様 value プロパティ経由でアクセスします。

setup(props, context) {
  //プロパティの値を ref メソッドでリアクティブに
  const count = Vue.ref(props.initialCount);
  const countUp = () => {
    count.value ++;
  };
  const countDown = () => {
    count.value --;
  }
  //computed メソッドを使用して算出プロパティを定義
  const doubleCount = Vue.computed({
    //getter を定義
    get: () => count.value * 2,
    //setter を定義
    set: val => { count.value = val / 2; },
  });

  //算出プロパティの値(value プロパティ経由でアクセス)を変更することができる
  doubleCount.value = 10;
  console.log(count.value); //5

  return {
    count,
    countUp,
    countDown,
    doubleCount //算出プロパティ
  }
}

以下は、Options API のコンポーネント内で算出プロパティを使って v-model を実装する例です。

コンポーネント内で v-model を実装する方法の1つは、以下のように算出プロパティ(computed)の機能を使ってゲッターとセッターを定義します。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
});

app.component('custom-input', {
  // modelValue プロパティ
  props: ['modelValue'],
  // emits オプション
  emits: ['update:modelValue'],
  // テンプレートで算出プロパティを v-model としてバインド
  template: `<input v-model="myValue">`,
  // 算出プロパティを定義(modelValue プロパティを操作)
  computed: {
    myValue: {
      // getter
      get() {
        return this.modelValue
      },
      // setter
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
});

app.mount('#app');
HTML
<div id="app">
  <custom-input v-model="myText"></custom-input>
  <p>{{ myText }}</p>
</div>

以下は上記を setup メソッド で Composition API の算出プロパティを使って書き換えた例です。

const app = Vue.createApp({
  setup() {
    const myText = Vue.ref('');
    return { myText }
  }
});

app.component('custom-input', {
  // modelValue プロパティ
  props: ['modelValue'],
  // emits オプション
  emits: ['update:modelValue'],
  // テンプレートで算出プロパティを v-model としてバインド
  template: `<input v-model="myValue">`,

  //setup メソッド
  setup(props, context) {
    // 算出プロパティに getter と setter を定義
    const myValue = Vue.computed({
      get() {
        return props.modelValue
      },
      set(value) {
        if(props.modelValue !== value){
          context.emit('update:modelValue', value)
        }
      }
    })

    return {
      myValue
    }
  }
});

app.mount('#app');
算出プロパティのデバッグ

computed メソッドの第2引数には、デバッグのためのオブジェクトを渡すことができます(3.2+)。

デバッグのためのオブジェクトは、以下のオプション(コールバック)を指定できます。

  • onTrack :リアクティブプロパティが依存関係として参照される時に呼び出される
  • onTrigger :リアクティブプロパティが変更(更新)される時に呼び出される

両方のコールバックは依存情報を持つデバッグイベント(e)を引数に受け取り、その引数を介してデバッグに関する情報を取得できます。

const app = Vue.createApp({});
app.component('counter-div', {
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  template: `<div>Count: {{ count }} <br> Count の2倍 : {{ doubleCount }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,

  setup(props, context) {
    const count = Vue.ref(props.initialCount);

    const countUp = () => {
      count.value ++;
    };
    const countDown = () => {
      count.value --;
    }
    //computed メソッドの第2引数にデバッグのためのオブジェクトを指定
    const doubleCount = Vue.computed(() => {
      return count.value * 2;
    },
    {
      onTrack(e) {
        // count.value を依存関係として参照する場合にトリガーする
        console.log(`${e.type} : ${e.key}`);
        console.log(e);
      },
      onTrigger(e) {
        // count.value を変更される場合にトリガーする
        console.log(`${e.type} : ${e.key}`);
        console.log(e);
      }
    });

    return {
      count,
      countUp,
      countDown,
      doubleCount
    }
  }
});
app.mount('#app');
<div id="app">
  <counter-div :initial-count="0"></counter-div>
</div>
コンソールへの出力例
//onTrack(e) :レンダリングの際に出力される
get : value  //`${e.type} : ${e.key}`
{effect: ReactiveEffect, target: RefImpl, type: 'get', key: 'value'}

//onTrigger(e) :ボタンをクリックする際に出力される
set : value  //`${e.type} : ${e.key}`
{effect: ReactiveEffect, target: RefImpl, type: 'set', key: 'value', newValue: 1}

watchEffect メソッド

例えば、以下の場合、コンソールには最初に 0 と出力されるだけで、setTimeout() により1秒後に値を増加しても何も出力されません。

//リアクティブな値を定義
const count = Vue.ref(0);

console.log(count.value);  //即座に 0 が出力されるだけ

setTimeout(() => {
  //1秒後に count を1増加(何も起きない)
  count.value++
}, 1000);

watchEffect メソッドは、渡されるコールバック関数に含まれるリアクティブ値を検出してコールバック関数を即時実行し、リアクティブ値が変化するとコールバック関数を再実行します。

以下の場合、watchEffect メソッドのコールバック関数にリアクティブな値(count.value)が含まれているので、setTimeout() により1秒後に値が増加すると、コンソールに 1 と出力されます。

//リアクティブな値を定義
const count = Vue.ref(0);

Vue.watchEffect(() => {
  //コールバック関数に含まれるリアクティブ値を検出
  console.log(count.value); //即座に 0 が出力され、1秒後に 1 が出力される
});

setTimeout(() => {
  //リアクティブ値が変化すると watchEffect のコールバック関数が再実行される
  count.value++
}, 1000);

watchEffect ガイド

以下はボタンをクリックすると initial-count 属性で渡されたカウントの値を増加するコンポーネントの例です。

HTML
<div id="app">
  <counter-div :initial-count="0"></counter-div>
</div>
JavaScript
const app = Vue.createApp({});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
    </div>
  </div>
  `,

  setup(props, context) {
    //プロパティの値を ref でリアクティブに
    const count = Vue.ref(props.initialCount);
    const countUp = () => {
      count.value ++;
    };
    return {
      count,
      countUp
    }
  }
});
app.mount('#app');

上記の例の場合、initialCount の値は initial-count 属性で渡されるコンポーネントの初期化時に反映されるでけで、後からの変更を検出できません。

以下はルートコンポーネントにカウントを乱数で初期化するボタンを追加した例ですが、初期化するボタンをクリックして init を実行しても、initialCount の値は更新されません。

HTML
<div id="app">
  <counter-div :initial-count="initialCount"></counter-div>
  <button type="button" v-on:click="init">乱数で初期化</button>
</div>
JavaScript
const app = Vue.createApp({
  setup() {
    //リアクティブな変数(カウントの初期値)を定義(data オプションに相当)
    const initialCount = Vue.ref(0);
    //イベントリスナー( methods オプションに相当)
    const init = () => {
      //initialCount を乱数でリセット(ref の値には value プロパティ経由でアクセス)
      initialCount.value = Math.floor(Math.random() * 10);
      //console.log(initialCount.value)  //コンソールには出力される
    };
    return {
      initialCount,
      init
    }
  }
});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
    </div>
  </div>
  `,

  setup(props, context) {
    //プロパティの値を ref でリアクティブに
    const count = Vue.ref(props.initialCount);
    const countUp = () => {
      count.value ++;
    };
    return {
      count,
      countUp
    }
  }
});
app.mount('#app');

watchEffect を使って変更を検出

以下は前述の例を、子コンポーネントで watchEffect を使って initialCount の変更を検出するように書き換えたものです。この場合、ルートコンポーネントの初期化するボタンをクリックするとカウントの値を初期化できます(HTML は同じなので省略)。

JavaScript
const app = Vue.createApp({
  setup() {
    //リアクティブな変数(カウントの初期値)を定義(data オプションに相当)
    const initialCount = Vue.ref(0);
    //イベントリスナー( methods オプションに相当)
    const init = () => {
      //initialCount を乱数でリセット(ref の値には value プロパティ経由でアクセス)
      initialCount.value = Math.floor(Math.random() * 10);
      //console.log(initialCount.value)
    };
    return {
      initialCount,
      init
    }
  }
});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
    </div>
  </div>
  `,

  setup(props, context) {
    //プロパティの値を ref でリアクティブに
    const count = Vue.ref(props.initialCount);

    //カウントの初期値(リアクティブ値)を監視して変更があればコールバック関数を実行
    Vue.watchEffect(() => {
      //props.initialCount が変更されたら count の値を更新
      count.value = props.initialCount;
    })
    const countUp = () => {
      count.value ++;
    };
    return {
      count,
      countUp
    }
  }
});
app.mount('#app');

監視の停止

watchEffect をコンポーネントの setup() 関数または ライフサイクルフック の中で呼び出すと、ウォッチャはコンポーネントのライフサイクルにリンクされ、コンポーネントのアンマウント時に自動的に監視が停止します。

また、明示的にウォッチャによる監視を停止するための stop ハンドル(関数)が返されます。

const stop = watchEffect(() => {
  /* ... */
})

// 監視を停止
stop()
watchEffect のオプション

watchEffect メソッドの第2引数にオプションを指定することができます。

watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'  //コールバックの実行タイミングのオプションを指定
  }
)

コールバックの実行タイミングを制御するには、flush オプションに以下を指定することができます。

flush オプションに指定する値
概要
pre レンダリング(描画)前に実行(デフォルト)。テンプレートの実行前にコールバックが他の値を更新することができます。
post レンダリング後に実行(更新後の文書ツリーにアクセスする場合や $refs 経由で子コンポーネントにアクセスする場合に利用)
sync 値が変更されたら即座に実行(作用をいつも同期的に発火することを強制するため非効率的)。

Vue 3.2 以降では、flush オプションの post と sync に対応するエイリアスのメソッド、watchPostEffect と watchSyncEffect が用意されています。

デバッグ用の onTrack および onTrigger オプションが必要ない場合は、それらのメソッドを利用することでコードの意図をより明確にすることもできます。

作用フラッシュのタイミング

watch メソッド

watch メソッドは Options API の $watch メソッド(及び watch オプション)と同じものです。

watch メソッドを使うと、特定のデータソースを監視して変化したときにコールバック関数を実行できます。デフォルトでは監視しているデータソースが変更された場合に限り、コールバック関数が実行されます。

watchEffect と比較すると、watch では以下のことが可能です。

  • 作用の効率的な実行
  • ウォッチャの再実行条件を、より具体的に指定できる
  • 監視している状態の、以前の値と現在の値の両方にアクセスできる

単一のデータソースの監視

watch メソッドの第1引数に指定する監視対象のデータソースは以下のいずれかになります。

  • 値を返す gettter 関数
  • ref
  • リアクティブなオブジェクト
  • または上記の配列
const state = Vue.reactive({ count: 0 });
//値を返す gettter 関数を渡す
Vue.watch(
  () => state.count,  // gettter 関数
  (newValue, oldValue) => {
    /* ...コールバック... */
  }
)

const count = Vue.ref(0);
//直接 ref を渡す
Vue.watch(
  count, // ref
  (newValue, oldValue) => {
  /* ...コールバック... */
})

以下のようなリアクティブのオブジェクトのプロパティを監視できません。

const obj = reactive({ count: 0 })

// これは、watch() に数値(オブジェクトのプロパティ)を渡しているので動作しません。
Vue.watch(
  obj.count,   //オブジェクトのプロパティを監視できない
  (count) => {
    console.log(`count is: ${count}`)
  }
)

代わりに、getter 関数を使います。

Vue.watch(
  () => obj.count,  // gettter 関数
  (count) => {
    console.log(`count is: ${count}`)
  }
)

以下は、コンソールに即座に「count is: 0」と出力し、1秒後に「count is: 1」と出力する例です。

//リアクティブなオブジェクトを定義
const obj = Vue.reactive({ count: 0 })

console.log(`count is: ${obj.count}`)  //count is: 0

Vue.watch(
  () => obj.count,  // gettter 関数でオブジェクトのプロパティを監視
  (count) => {
    //第2引数を出力
    console.log(`count is: ${count}`)  //1秒後に count is: 1
  }
)

setTimeout(() => {
  obj.count++
}, 1000);

以下でも同じ結果になります。

const obj = Vue.reactive({ count: 0 })
console.log(`count is: ${obj.count}`)  //count is: 0

Vue.watch(
  obj,  // リアクティブなオブジェクトを監視
  (obj) => {
    //第2引数のプロパティを出力
    console.log(`count is: ${obj.count}`)  //1秒後に count is: 1
  }
)

setTimeout(() => {
  obj.count++
}, 1000);

第2引数はソースが変更した時に呼ばれるコールバックで 3 つの引数を受け取ります。

  1. 新しい値
  2. 古い値
  3. 副作用のクリーンアップコールバックを登録するための関数

クリーンアップコールバックは、次に作用が再実行される直前に呼び出され、保留中の非同期リクエストのような無効化された副作用をクリーンアップするために使用することができます。

複数のデータソースの監視

配列を使って複数のソースを同時に監視することもできます。

const firstName = Vue.ref('');
const lastName = Vue.ref('');

Vue.watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues);
})

firstName.value = 'John'; // logs: ["John", ""] ["", ""]

setTimeout(() => {
  lastName.value = 'Smith'; // logs: ["John", "Smith"] ["John", ""]
}, 100);

ただし、同じ関数内で監視しているソースの両方を同時に変更している場合には、ウォッチャは一度だけ実行されます。

watch | watch | ウォッチャー

以下は前述の watchEffect メソッドの例を watch メソッドで書き換えたものです。

const app = Vue.createApp({
  setup() {
    const initialCount = Vue.ref(0);
    const init = () => {
      initialCount.value = Math.floor(Math.random() * 10);
    };
    return {
      initialCount,
      init
    }
  }
});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
    </div>
  </div>
  `,

  setup(props, context) {
    const count = Vue.ref(props.initialCount);

    /* //watchEffect メソッドの場合
    Vue.watchEffect(() => {
      //props.initialCount が変更されたら count の値を更新
      count.value = props.initialCount;
    })
     */

    //上記を watch メソッドで書き換え
    Vue.watch(
      () => props.initialCount, //props.initialCount を監視
      (newValue, prevValue) => {
        //props.initialCount が変更されたら count の値を newValue で更新
        count.value = newValue;
      }
    )

    const countUp = () => {
      count.value ++;
    };

    return {
      count,
      countUp
    }
  }
});
app.mount('#app');

ネストしたオブジェクトや配列の監視

ネストしたオブジェクトや配列のプロパティの変更をチェックするには、deep オプションを true にする必要があります。

const state = Vue.reactive({
  id: 1,
  attributes: {
    name: ''
  }
});

Vue.watch(
  () => state,
  (newState, prevState) => {
    //実行されない
    console.log('not deep', newState.id, prevState.id);
    console.log('not deep', newState.attributes.name, prevState.attributes.name);
  }
)

Vue.watch(
  () => state,
  (newState, prevState) => {
    console.log('deep', newState.id, prevState.id);
    console.log('deep', newState.attributes.name, prevState.attributes.name);
  },
  { deep: true } //第3引数にオプション deep: true を指定
)

state.id = 7; // ログの出力: "deep" "7" "7"
state.attributes.name = 'Alex' // ログの出力: "deep" "Alex" "Alex"

但し、リアクティブなオブジェクトや配列を監視すると、そのオブジェクトの状態の現在値と前回値の両方について参照が常に返されます。

以下はオブジェクトのプロパティを指定して(プロパティを返す関数を渡して)監視する例です。

Vue.watch(
  () => state.id,
  (newState, prevState) => {
    console.log('newState: ' + newState, 'prevState: ' + prevState);
  }
)

Vue.watch(
  () => state.attributes.name,
  (newState, prevState) => {
    console.log('newState: ' + newState, 'prevState: ' + prevState);
  }
)

state.id = 7;  //ログの出力: newState: 7 prevState: 1
state.attributes.name = 'Alex';  //ログの出力: newState: Alex prevState:

配列を使って複数のソースを同時に監視する例です。

Vue.watch(
  () => [state.id, state.attributes.name],
  (newState, prevState) => {
    console.log(newState, prevState);
  }
)

state.id = 7;
state.attributes.name = 'Alex';
//ログの出力: [7, 'Alex'] [1, '']

watch メソッドのオプション

watch メソッドの第3引数に以下のようなオプションを指定できます。

オプション 説明
deep ネストされたオブジェクトを監視するかどうか(デフォルトは false)
immediate 起動時に即座に実行するかどうか(デフォルトは false)
flush 処理(コールバック)の実行タイミングを制御(以下を指定)
  • pre:レンダリング(描画)前に実行(デフォルト)。テンプレートの実行前にコールバックが他の値を更新することができます。
  • post:レンダリング後に実行(更新後の文書ツリーにアクセスする場合や $refs 経由で子コンポーネントにアクセスする場合に利用)
  • sync:値が変更されたら即座に実行
Vue.watch(
  () => state,
  (newState, prevState) => {
    /* ...コールバック... */
  },
  //第3引数にオプションを指定
  {
    deep: true,
    immediate: true,
    flush: 'post'
  }
)

ライフサイクルフック

Composition API でも各ライフサイクルに該当する関数(ライフサイクルフック)が用意されていて、setup メソッドで使用することができます。

Composition API のライフサイクルフック名は、Options API でのフックの前に on を付けます。

Options API Composition API (setup)
beforeCreate setup() を使用(※)
created setup() を使用(※)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

※ Composition API では、setup メソッドに定義した処理が beforeCreate と created に該当するため、これらに該当するフックは用意されておらず、setup を利用します。

言い換えると、setup メソッドは beforeCreate と created のライフサイクルで実行されます。

以下は Composition API のライフサイクルフックのそれぞれのタイミングでログを出力する例です。

HTML
<div id="app">
  <button v-on:click="onclick">Click</button>
  <p>{{ count }}</p>
</div>
const app = Vue.createApp({
  setup() {
    console.log('setup');

    Vue.onBeforeMount(() => {
      console.log('onBeforeMount hook');
    });
    Vue.onMounted(() => {
      console.log('onMounted hook');
    });
    Vue.onBeforeUpdate(() => {
      console.log('onBeforeUpdate hook');
    });
    Vue.onUpdated(() => {
      console.log('onUpdated hook: count = ' + count.value);
    });
    Vue.onBeforeUnmount(() => {
      console.log('onBeforeUnmount hook');
    });
    Vue.onUnmounted(() => {
      console.log('onUnmounted hook');
    });

    const count = Vue.ref(0);
    const onclick = () => {
      count.value ++;
    };
    return {
      count,
      onclick
    }
  }
});
app.mount('#app');

setTimeout(() => {
  //5秒後にアンマウント
  app.unmount();
},5000);

上記の場合、最初に以下がほぼ同時にコンソールに出力されます。

setup
onBeforeMount hook
onMounted hook

そして5秒後に app.unmount() により以下が出力されます。

onBeforeUnmount hook
onUnmounted hook

5秒以内(アンマウントする前)にボタンをクリックすると上記の前に以下が出力されます(以下はボタンを2回クリックした例です)。

onBeforeUpdate hook
onUpdated hook: count = 1
onBeforeUpdate hook
onUpdated hook: count = 2

テンプレート参照(ref 属性)

ref 属性を使ってコンポーネントや HTML 要素を参照することができます。

Options API では、ref 属性を付与した要素を this.$refs で参照することができます。以下の例ではテンプレートの div 要素の ref 属性に foo という名前を指定し、その要素を $refs.foo で参照しています。

Options API
const app = Vue.createApp({});

app.component('foo-div', {
  //ref 属性に foo を指定
  template: `<div ref="foo">This is Foo.</div>`,
  mounted() {
    //this.$refs.foo で参照
    console.log(this.$refs.foo); //<div>This is Foo.</div> が出力される
    console.log(this.$refs.foo.innerHTML); //This is Foo.
    console.log(this.$refs.foo.localName); //div (タグ名)
  }
});

app.mount('#app');
HTML
<div id="app">
  <foo-div></foo-div>
</div>

Composition API では、$refs の代わりに ref メソッドを使って要素の参照を保持する ref($refs に該当)を宣言します(名前はテンプレートの ref 属性の値に一致させる必要があります)。

テンプレート内の要素やコンポーネントインスタンスの参照を取得するには、参照する要素に ref 属性で名前を指定します(参照先に ref 属性を指定します)。

そして setup() 内で ref() メソッドを使って ref 属性に指定した名前と同じ名前の要素の参照を保持する ref オブジェクトを定義します(以下9行目)。そして定義した ref オブジェクトを return してテンプレートに公開します。

これにより、ref 属性で指定した名前と、ref() メソッドで定義した ref オブジェクトが紐付き、変数名.value で参照できます(ref オブジェクトの値には value プロパティでアクセスします)。

Composition API
const app = Vue.createApp({});

app.component('foo-div', {
  //ref 属性 foo を設定
  template: `<div ref="foo">This is Foo.</div>`,

  setup() {
    //ref() を使って ref 属性に指定した名前と同じ名前の変数を定義($refs.foo に該当)
    const foo = Vue.ref(null);  //または Vue.ref()
    //onMounted ライフサイクルでアクセス
    Vue.onMounted(() => {
      // .value プロパティでアクセス(foo は ref オブジェクト)
      console.log(foo.value);  //<div>This is Foo.</div> が出力される
      console.log(foo.value.innerHTML);  //This is Foo.
      console.log(foo.value.localName);  //div
    });
    //ref メソッドで定義した変数(ref オブジェクト)を返す
    return {
      foo
    }
  }
});

app.mount('#app');

テンプレート参照に値が代入される(バインドされた ref にアクセスできる)のは初回レンダリング後になります(onMounted でアクセスできるようになります)。

また、ref メソッドは value プロパティを持つ ref オブジェクトを返すので、.value でアクセスします。

テンプレート参照

以下は親のルートコンポーネントでテンプレートに ref 属性を指定して、子コンポーネントのインスタンスやテンプレートの要素を参照する例です。

HTML
<div id="app">
  <!-- それぞれに ref 属性を指定 -->
  <p ref="hello">hello</p>
  <bar-div ref="bar" class="child"></bar-div>
</div>
JavaScript
const app = Vue.createApp({
  setup() {
    //要素の参照を保持する ref を宣言(名前はテンプレートの ref 属性の値に一致させる)
    const hello = Vue.ref();  //hello は ref オブジェクト
    const bar = Vue.ref();  //bar は ref オブジェクト
    //onMounted ライフサイクルフックで参照先の情報を出力
    Vue.onMounted(() => {
      //ref オブジェクトの value プロパティにアクセス
      console.log(hello.value);  //<p>hello</p>
      console.log(hello.value.innerHTML);  //hello
      console.log(bar.value);  //Proxy {…} 子コンポーネントのインスタンス
      console.log(bar.value.message);  //This is Bar.
      console.log(bar.value.onclick);  //() => { alert('Bar'); } メソッド
      console.log(bar.value.$attrs);  //Proxy {class: 'child', __vInternal: 1}
      console.log(bar.value.$attrs.class);  //child
    });
    // ref をテンプレートに公開
    return {
      hello,
      bar,
    };
  },
});

//子コンポーネント
app.component('bar-div', {
  template: `<div class="bar" v-on:click="onclick">{{ message }}</div>`,
  setup() {
    //データ
    const message = 'This is Bar.';
    //メソッド
    const onclick = () => {
      alert('Bar');
    }
    //上記データとメソッドを公開
    return {
      message,
      onclick
    }
  }
});
app.mount('#app');

子コンポーネントのメソッドを親コンポーネント内で呼び出す

setup 関数に渡される第2引数 context の expose プロパティを使ってコンポーネントのプロパティを明示的に公開することができます。

setup メソッドから返されるものはすべて、親から直接アクセスできますが、expose プロパティで指定することで、公開するプロパティを制限することができます。

expose で公開されたプロパティは、親コンポーネントでテンプレート参照を使って利用できます。

HTML
<div id="app">
  <!-- 子コンポーネントの ref 属性に childRef を付与 -->
  <child-div ref="childRef" message="Foo!"></child-div>
  <button type="button" v-on:click="handleClick">childFunc を呼び出す</button>
</div>

子コンポーネント側のコンポーネント情報を受け取る ref(この例では childRef)を ref メソッドで初期化して宣言します。

テンプレートの ref 属性に宣言した変数と同じ名前を指定します。

expose で公開されたプロパティ は childRef.value.xxxx のような形で利用することができます。

JavaScript
//親コンポーネント
const app = Vue.createApp({
  setup() {
    //ref 属性に指定した値と同じ名前の変数を宣言して ref メソッドで初期化
    const childRef = Vue.ref(null);  //または Vue.ref()
    // 子コンポーネントのメソッドを発火させるメソッドを定義
    const handleClick = () => {
      // 子コンポーネントで公開されているメソッドを呼び出す
      childRef.value.childFunc();
      //以下のメソッドは公開されていないので使用するとエラーになる
      //childRef.value.childOnlyFunc();
    };
    // テンプレートに公開
    return {
      childRef,
      handleClick,
    };
  },
});

//子コンポーネント
app.component('child-div', {
  // props オプション
  props: {
    message : {
      type: String,
      default: 'World!'
    }
  },

  template: `<div>Hello {{ message }}</div>`,

  setup(props, context) {
    // 公開するメソッドの定義
    const childFunc = () => {
      console.log("childFunc: props の値は " + props.message);
    };
    // 非公開のメソッドの定義
    const childOnlyFunc = () => {
      console.log("非公開のメソッド ");
    };

    // context.expose で childFunc メソッドを外部コンポーネントに公開
    context.expose({
      childFunc
    });

    return {
      childFunc,
      childOnlyFunc
    }
  }
});

子コンポーネントの setup 関数は第2引数を分割代入を使って以下のように記述することもできます。

setup(props, { expose }) {
  const childFunc = () => {
    console.log("childFunc: props の値は " + props.message);
  };
  const childOnlyFunc = () => {
    console.log("非公開のメソッド ");
  };
  // expose で childFunc メソッドを外部コンポーネントに公開
  expose({
    childFunc
  });
  return {
    childFunc,
    childOnlyFunc
  }
}

以下のように出力され、ボタンをクリックすると「childFunc: props の値は Foo!」と出力されます。

<div id="app" data-v-app="">
  <div>Hello Foo!</div>
  <button type="button">childFunc を呼び出す</button>
</div>

provide / inject メソッド

Provide / Inject の仕組みも、Composition API の setup メソッドでは、provide / inject メソッドを使って実装することができます。

以下は Provide / Inject の仕組み(Options API)を使った例で、parent-div で provide オプションを使って title というリアクティブな値を提供し、child-div で inject オプションを使って title を取り込む例です。

Root
└─ parent-div
   └─ foo-div
      └─ child-div

この例の場合、parent-div で提供する値はテキストボックスに入力された値で変更できるように、computed メソッドでリアクティブにしています。

JavaScript(Options API の例)
const app = Vue.createApp({});

app.component('parent-div', {
  template: `
    <div id="parent">
      <input v-model="title">
      <foo-div />
    </div>`,
  data() {
    return {
      title: 'Vue3'
    }
  },
  //値を提供(provide オプション)
  provide() {
    return {
      //computed メソッド でリアクティブに
      title: Vue.computed(() => this.title)
    }
  }
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  template: `
    <div id="child">
      child: {{ title.value }}
    </div>`,
  //値を注入(inject オプション)
  inject: ['title'],
});

app.mount('#app'); 
HTML
<div id="app">
  <parent-div />
</div>

Vue 3.2.36 で上記を実行すると「[Vue warn]: injected property "title" is a ref and will be auto-unwrapped and no longer needs `.value` in the next minor release. To opt-in to the new behavior now, set `app.config.unwrapInjectedRef = true` (this config is temporary and will not be needed in the future.) 」のような警告が出ます(次のマイナーリリースでは ref が自動的にアンラップされるので .value は不要になるとのこと)。

以下は、上記を Composition API の provide / inject メソッドを使って書き換えた例です。

script setup を使用しない場合、provide() と inject() は setup() 内で同期的に呼び出す必要があります。

(Composition API の例)
const app = Vue.createApp({});

app.component('parent-div', {
  template: `
    <div id="parent">
      <input v-model="title">
      <foo-div />
    </div>`,
  setup() {
    //提供する値をリアクティブに
    const title = Vue.ref('Vue3');
    //provide メソッドで値を登録
    Vue.provide('title', title);
    return {
      title
    };
  },
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  template: `
    <div id="child">
      child: {{ title }}
    </div>`,
  setup() {
    //inject メソッドで登録された値を取得
    const title = Vue.inject('title');
    return {
      title
    };
  }
});

app.mount('#app');

provide メソッドは 2 つの引数(名前、値)によってプロパティを定義できます。

  • 名前:文字列または Symbol。子孫のコンポーネントが注入する際に使用する名前(キー)。インジェクションキーと呼ばれます。
  • :提供される値。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます
// 静的な値を提供
Vue.provide('foo', 'bar')

// リアクティブな値を提供
const count = Vue.ref(0)
Vue.provide('count', count)

inject メソッドは 2 つの引数(注入されるプロパティ名、デフォルト値)をとります。第2引数のデフォルト値は省略可能です。

上記の例(30行目)では、inject メソッドの第2引数を省略していますが、もし該当する provide がない場合のデフォルト値を指定することもできます。

const title = Vue.inject('title', '既定値');

リアクティブ

提供された値と注入された値をリアクティブにするには、値を提供する際に ref または reactive を使います。

以下は、前述の例の provide で提供する値をオブジェクトにして、reactive メソッドを使ってリアクティブにするように書き換えたものです。

const app = Vue.createApp({});

app.component('parent-div', {
  template: `
    <div id="parent">
      <input v-model="obj.title">
      <foo-div />
    </div>`,
  setup() {
    //reactive メソッドを使ってリアクティブに
    const obj = Vue.reactive({
      title: 'Object Title'
    });
    //オブジェクトで提供
    Vue.provide('obj', obj);
    return {
      obj
    };
  },
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  template: `
    <div id="child">
      child: {{ obj.title }}
    </div>`,
  setup() {
    const obj = Vue.inject('obj');
    return {
      obj
    };
  }
});

app.mount('#app');

リアクティブプロパティの変更

リアクティブな provide / inject の値を使う場合、リアクティブなプロパティに対しての変更は可能な限り提供する側(provide 側)で行うこと推奨されます。

もし、inject 側で値を操作したい場合は、provide 側で更新メソッドを定義して、inject 側で更新メソッドも inject します。

const app = Vue.createApp({});

app.component('parent-div', {
  template: `
    <div id="parent">
      <input v-model="title">
      <foo-div />
    </div>`,
    setup() {
    const title = Vue.ref('Vue3');
    //更新メソッドを提供
    const updateTitle = () => {
      title.value = 'New Title'
    };
    Vue.provide('title', title);
    //provide メソッドで更新メソッドを登録
    Vue.provide('updateTitle', updateTitle);
    return {
      title
    };
  },
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  template: `
    <div id="child">
      <p>child: {{ title }}</p>
      <button  v-on:click="updateTitle">タイトル更新</button>
    </div>`,
  setup() {
    const title = Vue.inject('title');
    //更新メソッドを注入
    const updateTitle = Vue.inject('updateTitle');
    return {
      title,
      updateTitle
    };
  }
});

app.mount('#app');

readonly 渡したデータが変更されないように

provide で渡したデータが、注入されたコンポーネント内で変更されないようにしたい場合は、提供するプロパティに readonly メソッドを適用して値を読み取り専用にすることができます。

以下は 子コンポーネントで provide された値を変更するクリックイベントを設定していますが、provide でデータを渡す際に、readonly で値を読み取り専用にしているので、クリックしても値は変更されません。

クリックすると、[Vue warn] Set operation on key "value" failed: target is readonly.のような警告がコンソールに出力されます。

本来、inject 側で provide された値を変更すべきではありません

const app = Vue.createApp({});

app.component('parent-div', {
  template: `
    <div id="parent">
      <foo-div />
    </div>`,
    setup() {
    const title = Vue.ref('Vue3');
    //readonly メソッドで値を読み取り専用に
    Vue.provide('title', Vue.readonly(title));
    return {
      title
    };
  },
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  template: `
    <div id="child">
      <p>child: {{ title }}</p>
      <button  v-on:click="title='child で変更'">タイトル変更</button>
    </div>`,
  setup() {
    const title = Vue.inject('title');
    return {
      title
    };
  }
});

app.mount('#app');

Provide / Inject

Composables コンポーザブル

Options API では computed や data などを記述する場所が決まっているため、コンポーネントからロジックを分離することが困難でしたが、Composition API では、computed メソッドや ref メソッドを使うことで標準的な JavaScript として記述できるため、ロジックを切り出すことが容易になりました。

以下は Composition API で記述したカウンターの例です(すでに何度か同じコードを使用しています)。

HTML
<div id="app">
  <counter-div v-bind:initial-count="0"></counter-div>
</div>
JavaScript
const app = Vue.createApp({});
app.component('counter-div', {
  // props オプション
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // template オプション
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  // setup メソッド内でコンポーネントに必要なものを定義
  setup(props, context) {
    // データオブジェクトの宣言
    const count = Vue.ref(props.initialCount);
    // イベントリスナー(メソッド)の定義
    const countUp = () => {
      count.value ++;
    };
    // イベントリスナー(メソッド)の定義
    const countDown = () => {
      count.value --;
    }
    // 定義したものをまとめて戻り値にして返す
    return {
      count,
      countUp,
      countDown
    }
  }
});
app.mount('#app');

以下は上記のコンポーネントからカウンターののロジックを関数 useCounter として切り出した例です。

コンポーザブル関数

ロジックを分離し(カプセル化し)、再利用する目的で切り出した関数を Composable Function (コンポーザブル関数、またはコンポジション関数)と呼びます(Composables)。

//切り出したカウンターのためのロジック(コンポーザブル関数)
const useCounter = (initialCount) => {
  //カウンターで使用する変数(データオブジェクト)
  const count = Vue.ref(initialCount);
  //カウンターで使用するメソッド
  const countUp = () => {
    count.value ++;
  };
  //カウンターで使用するメソッド
  const countDown = () => {
    count.value --;
  }
  //カウンターで使用するオブジェクト(変数とメソッド)を返す
  return {
    count,
    countUp,
    countDown
  }
}

//コンポーザブル関数を利用するコンポーネント
const app = Vue.createApp({});
app.component('counter-div', {
  // props オプション (同じ)
  props: {
    initialCount : {
      type: Number,
      default: 1
    }
  },
  // template オプション (同じ)
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  // setup メソッド
  setup(props, context) {
    //コンポーザブル関数(useCounter)を呼び出し、その結果を変数に代入(分割代入)
    const {count, countUp, countDown } = useCounter(props.initialCount);
    //この例では単にコンポーザブル関数の結果を setup の戻り値に
    return {
      count,
      countUp,
      countDown
    }
  }
});
app.mount('#app');

この例の場合、コンポーザブル関数(useCounter)からの戻り値を setup メソッドの戻り値にしているだけなので、上記の setup メソッド(40〜49行目)はスプレッド構文を使って以下のように記述しても同じです。

setup(props, context) {
  return {
    ...useCounter(props.initialCount)
  }
}

コンポーザブル関数は一般的に以下のようなルールで定義します。

  • 戻り値は、setup メソッドで利用する変数や関数からなるオブジェクト
  • 関数名は通常(慣例的に) use から始める(useXxxx)

コンポーザブル

カスタムディレクティブ

Vue 本体で提供されているディレクティブに加えて、独自のカスタムディレクティブ (custom directives) を登録することができます。

Vue ではコードの再利用の基本はコンポーネントとコンポーザブルですが、単純な要素に対する低レベルの DOM アクセスを含むロジックの再利用はカスタムディレクティブを利用することができます。

コンポーネント内では単純な DOM 要素(文書ツリー)の操作のコードを記述するのは避けるべきで、単純な DOM 要素へのアクセスが必要な場合はカスタムディレクティブを使用します。

言い換えると、カスタムディレクティブは、DOM を直接操作することで目的の機能を実現できる場合にのみ使用します。

以下はページを読み込むと、v-focus というカスタムディレクティブを指定した要素にフォーカスが当たります。

HTML
<div id="app">
  <input v-focus />
</div>
JavaScript
const app = Vue.createApp({});
// v-focus というグローバルカスタムディレクティブを登録
app.directive('focus', {
  // バインドされた要素が DOM にマウントされるタイミングで
  mounted(el) {
    // その要素にフォーカスを当てる
    el.focus();
  }
});
app.mount('#app');

directive メソッド

カスタムディレクティブは directive メソッドを使って定義することができます。

directive メソッド
directive(name, definition)
  • name:ディレクティブの名前(接頭辞の v- は付けない)
  • definition:ディレクティブの定義オブジェクト(実行タイミングと関数からなるオブジェクト)

ディレクティブの定義オブジェクトは、「どのタイミングでどのような処理をするか」を関数(フック関数)で指定し、複数指定することができます。

利用できるタイミングは以下になり、それぞれのタイミングで実行する関数をフック関数と呼びます。

フック関数 概要(タイミング)
created バインドされた要素の属性やイベントリスナが適用される前。v-on イベントリスナの前に呼ばれなければならないイベントリスナをつける必要がある場合に利用できます。
beforeMount ディレクティブが最初に要素にバインドされたとき、親コンポーネントがマウントされる前
mounted バインドされた要素の親コンポーネントとそのすべての子がマウントされた時。ディレクティブの挙動の初期化などに利用できます。
beforeUpdate バインドされた要素を含むコンポーネントの VNode が更新される前(コンポーネント配下の要素が更新される前)
updated バインドされた要素を含むコンポーネントの VNode とその子コンポーネントの VNode が更新された後(コンポーネント配下の要素が更新された後)
beforeUnmount バインドされた要素の親コンポーネントがアンマウントされる前
unmounted ディレクティブが要素からバインドを解除された時、また親コンポーネントがアンマウントされた時に 1 度だけ呼ばれます

フック関数に渡される引数

フック関数が受け取る引数は以下になります(いずれの引数も省略可能)。

引数 概要
el ディレクティブが適用される(バインドされる)要素。
binding このオブジェクトは、以下のプロパティを持ちます。
  • instance: ディレクティブが使われているコンポーネントのインスタンス。
  • value: ディレクティブの値(式)。例えば v-my-directive="1 + 1"の場合、value は 2 。
  • oldValue: 以前の(変更前の)値。beforeUpdate および updated でのみ利用可能。値が変更されているかを判別できます。
  • arg: 引数がある場合はそれを含むオブジェクト。例えば v-my-directive:foo の場合、 arg は "foo" 。
  • modifiers: 修飾子がある場合はそれを含むオブジェクト。例えば v-my-directive.foo.bar の場合、 modifiers オブジェクトは { foo: true, bar: true }。
  • dir: ディレクティブが登録されたとき、パラメータとして渡されるオブジェクト(directive メソッドの第2引数の定義オブジェクト)。
vnode el で受け取った DOM 要素の仮想ノード(VNode)
prevNode 変更前の仮想ノード(VNode)。beforeUpdate および updated フックでのみ利用可能

※ 引数の el 以外のすべての引数は読み取り専用であり、フック関数の中で変更してはいけません。

ディレクティブに渡す値

ディレクティブに渡す値(属性値)は任意の型の値を渡すことができ data プロパティなどとバインドすることができます。そしてフック関数の中で binding.value でアクセスできます。

以下で定義したディレクティブ v-text-color は、その要素の文字色(el.style.color)を、ディレクティブに渡した値(binding.value)に設定します。

JavaScript
const app = Vue.createApp({
  data() {
    return {
       myColor: 'green'
    }
  }
}).directive('text-color', {
  mounted(el, binding) {
    // binding.value はディレクティブに渡した値
    el.style.color = binding.value
  }
});
app.mount('#app');
const app = Vue.createApp({
  setup(){
    const myColor = Vue.ref('green');
    return {
      myColor
    };
  }
}).directive('text-color', {
  mounted(el, binding) {
    el.style.color = binding.value
  }
});
app.mount('#app');

テンプレートの1つ目の div 要素のディレクティブ v-text-color には data プロパティの myColor が指定されているのでその値の緑色に、2つ目の div は文字列 'red' が渡されているので赤になります。

HTML
<div id="app">
  <div v-text-color="myColor">Hello, it's green.</div><!-- 文字は緑色 -->
  <div v-text-color="'red'">Hello, it's red.</div><!-- 文字は赤色 -->
</div>
出力
<div id="app" data-v-app="">
  <div style="color: green;">Hello, it's green.</div>
  <div style="color: red;">Hello, it's red.</div>
</div>

オブジェクトリテラル

ディレクティブに複数の値が必要な場合、JavaScript のオブジェクトリテラルを渡すこともできます。属性値には任意の型の値(あらゆる妥当な JavaScript 式)を渡すことができます。

以下は属性値(ディレクティブの値)にオブジェクトを指定する例です。

HTML
<div id="app">
  <div v-demo="{ color: 'white', bg: 'red', text: 'hello!' }"></div>
</div>
JavaScript
const app = Vue.createApp({})
.directive('demo', {
  mounted(el, binding) {
    el.style.color = binding.value.color;
    el.style.backgroundColor = binding.value.bg;
    el.textContent = binding.value.text;
  }
});
app.mount('#app');

以下も属性値にオブジェクトを指定する例です。この例の場合、属性値が指定されていない場合の既定値を設定しています。

JavaScript
const app = Vue.createApp({})
.directive('my-style', {
  mounted(el, binding) {
    //属性値が指定されている場合
    if(binding.value) {
      //オブジェクトの color プロパティが指定されていればその値を、そうでなければ green
      el.style.color = binding.value.color? binding.value.color : 'green';
      el.style.fontSize = binding.value.fontSize ? binding.value.fontSize : '20px';
    }else{
      //属性値が指定されていない場合
      el.style.color = 'green';
      el.style.fontSize = '20px';
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <div v-my-style="{color:'red', fontSize:'40px'}">Hello</div><!-- 赤で40px-->
  <div v-my-style="{fontSize:'50px'}">Hello</div><!-- 緑で50px-->
  <div v-my-style="{color:'blue'}">Hello</div><!-- 青で20px-->
  <div v-my-style>Hello</div><!-- 緑で20px-->
</div>

ローカルディレクティブ

コンポーネントの directives オプションを使うと、特定のインスタンス配下でのみ有効なディレクティブを定義することができます。

JavaScript
const app = Vue.createApp({});
app.component('focused-input', {
  template: `<input v-focus />`,
  // directives オプション(ローカルディレクティブの定義)
  directives: {
    //ディレクティブの名前
    focus: {
      //ディレクティブの定義オブジェクト(
      mounted(el) {
        el.focus()
      }
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <focused-input />
</div>

ディレクティブに引数を渡す

ディレクティブ名の後にコロン(:)区切りで、ディレクティブの引数を指定することができます。

<elem v-my-directive:引数="ディレクティブの値">

フック関数の中では引数を binding.arg で参照することができ、引数の値は文字列として取得できます。

以下で定義したディレクティブ v-color は、引数に「background-color」が指定されていれば背景色を、引数が指定されていなければ文字色を、ディレクティブに渡した値の色にします。

この例では引数に静的な値(文字列)を指定していますが、動的な値を指定することもできます。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myColor: 'green',
    }
  }
}).directive('color', {
  mounted(el, binding) {
    // binding.arg はディレクティブに渡した引数
    const prop = binding.arg || 'color';
    el.style[prop]=binding.value;
  }
});
app.mount('#app');

1つ目の div 要素の文字色は引数に background-color が指定されているので背景色が、2つ目の div は引数が指定されていないので文字色が myColor の値(緑)になります。

HTML
<div id="app">
  <div v-color:background-color="myColor">Background is green.</div><!--背景色が緑-->
  <div v-color="myColor">Text is green.</div><!-- 文字色が緑 -->
</div>

以下は switch 文を使って引数に指定された値に応じて要素に色を設定する例です。

引数の値として background、border、text 以外が指定された場合は、文字色を設定するようにしています。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myColor: 'green',
    }
  }
}).directive('color', {
  mounted(el, binding) {
    //引数の値に応じて色を付ける
    switch(binding.arg) {
      case 'background':
        el.style.backgroundColor = binding.value;
        break;
      case 'border':
        el.style.borderStyle = 'solid';
        el.style.borderWidth = '1px';
        el.style.borderColor = binding.value;
        break;
      case 'text':
        el.style.color = binding.value;
        break;
      default:
        //引数が省略されたり、値が無効な場合
        el.style.color = binding.value;
    }
  }
});
app.mount('#app');

HTML
<div id="app">
  <div v-color:border="myColor">Hello!</div><!--緑のボーダー-->
  <div v-color:background="'yellow'">Hello!</div><!--黄色の背景色 -->
</div>

動的なディレクティブ引数

v-bind や v-on 同様、[ ] で囲むことで JavaScript 式をディレクティブの引数に使うことができます。

例えば、v-mydirective:[argument]="value" において、argument はコンポーネントインスタンスの data プロパティに基づいて更新されます。

以下は v-pin ディレクティブの動的な引数 [direction] の値により、要素をピン留め(固定表示)する基準を変更できるようにした例です。

動的な引数 [direction] には、要素をピン留めする基準の値として、data プロパティの direction に right や bottom、left、top を指定します(デフォルトは top)。

v-pin ディレクティブの値には、基準からの位置を data プロパティの pinPadding に数値で指定します。

HTML
<div id="app">
  <p v-pin:[direction]="pinPadding">
    {{ pinPadding }}px from the {{ direction || 'top' }}
  </p>
</div>

この例の場合、data プロパティの direction に right を指定しているので、右端から pinPadding で指定した 200px の位置に固定(fixed)表示されます。

data プロパティの direction に bottom などを指定することで固定する基準を変更することができます。

ディレクティブに引数が指定されていないか、data プロパティの direction の値が空文字の場合は、top が適用されます(data プロパティに direction が定義されていない場合は警告が出力されます)。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      direction: 'right', //ディレクティブの引数
      pinPadding: 200 //ディレクティブの値
    }
  }
}).directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    // binding.arg はディレクティブに渡した引数
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
});
app.mount('#app');

動的なディレクティブ引数

属性値の変化を検出

以下は前述の v-pin ディレクティブの値(pinPadding )を input 要素のスライダーの値で、引数の値(direction)をセレクトボックスの値で変更するようにした例ですが、実際には期待通りに動作しません。

HTML
<div id="app">
  <input type="range" min="0" max="500" v-model="pinPadding">
  <select v-model="direction">
    <option value="top">top</option>
    <option value="right">right</option>
    <option value="bottom">bottom</option>
    <option value="left">left</option>
  </select>
  <p v-pin:[direction]="pinPadding">
    {{ pinPadding }}px from the {{ direction || 'top' }}
  </p>
</div>
JavaScript(前述の例と同じ)
const app = Vue.createApp({
  data() {
    return {
      direction: 'right',
      pinPadding: 200
    }
  }
}).directive('pin', {
  //マウントされた時に一度だけ呼び出される
  mounted(el, binding) {
    el.style.position = 'fixed'
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
});
app.mount('#app');

上記のままでは、スライダーやセレクトボックスで変更した値は v-model によってテンプレート構文(Mustache 構文)の {{ xxxx }} の部分には反映されますが、ディレクティブには反映されません(初期状態のまま)。

これは、フック関数 mounted は、バインドされた要素のコンポーネントがマウントされた時に一度だけ呼び出され、更新時には呼び出されないためです。

update フック関数を追加

コンポーネントの更新を検知するには、update フック関数を追加で定義する必要があります。

17〜20行目は、top や right などの CSS での位置の指定を一旦クリアするための記述です(これらの CSS プロパティは複数指定できてしまうため)。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      direction: 'right',
      pinPadding: 200
    }
  }
}).directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed';
    const s = binding.arg || 'top';
    el.style[s] = binding.value + 'px';
  },
  //update フック関数を定義(更新を検知)
  updated(el, binding) {
    //位置の指定を初期化
    el.style.top = null;
    el.style.right = null;
    el.style.bottom = null;
    el.style.left = null;
    const s = binding.arg;
    el.style[s] = binding.value + 'px';
  }
});
app.mount('#app');
mounted/updated をまとめて定義

以下はカスタムディレクティブ v-text-color の値をセレクトボックスで変更できるようにした例です。

HTML
<div id="app">
  <select v-model="myColor">
    <option value="green">Green</option>
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="orange">Orange</option>
  </select>
  <div v-text-color="myColor">Hello!</div>
</div>

マウントされた時(mounted)と更新された時(updated)のタイミングで同じ処理を行っています。

const app = Vue.createApp({
  data() {
    return {
       myColor: 'green'
    }
  }
}).directive('text-color', {
  //mounted フック関数(マウントされた際に1回だけ実行)
  mounted(el, binding) {
    el.style.color = binding.value;
  },
  //updated フック関数(更新時に毎回実行)
  updated(el, binding) {
    el.style.color = binding.value;
  }
});
app.mount('#app');

マウントされた時と更新された時に同じ処理を行い、他のフックは必要ない場合は、以下のようにディレクティブにコールバックを渡すことで簡潔に記述することができます。

directive メソッドの第2引数にオブジェクトを渡す代わりに、関数リテラルを渡すことで mounted と updated をまとめて定義することができます(関数による省略記法)。

const app = Vue.createApp({
  data() {
    return {
       myColor: 'green'
    }
  }
  //関数による省略記法(第2引数に関数リテラルを渡す)
}).directive('text-color', (el, binding) => {
  //mounted と updated で実行される処理
  el.style.color = binding.value;
});
app.mount('#app');
値が変化したかを判定

以下は前述の例にテキストボックスを追加し、表示する文字(message)を変更できるようにした例です。

HTML
<div id="app">
  <select v-model="myColor">
    <option value="green">Green</option>
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="orange">Orange</option>
  </select>
  <input type="text" v-model="message">
  <div v-text-color="myColor">{{ message }}</div>
</div>

updated フック関数はディレクティブの値(binding.value)が変化したかどうかに関わらず、親コンポーネントに変化があった場合に毎回呼び出されます。

この例で追加したテキストボックスは、v-text-color ディレクティブを利用していませんが、テキストボックスに文字を入力したり変更すると updated フック関数が毎回呼び出され、コンソールに binding.value が出力されるのが確認できます。

JavaScript
const app = Vue.createApp({
  data() {
    return {
       myColor: 'green',
       message: 'Hello!'
    }
  }
  //関数による省略記法(mounted/updated をまとめて定義)
}).directive('text-color', (el, binding) => {
  el.style.color = binding.value;
  console.log(binding.value); //呼び出されるたびにコンソールへ出力
});
app.mount('#app');

binding.oldValue で確認

ディレクティブの値に変化がないのに、フック関数が繰り返し呼び出されるのは無駄なので、ディレクティブの値(binding.value)が変化していないかを binding.oldValue(変更前の値)と比較して、変化している場合にのみ処理を実行するようにします。

const app = Vue.createApp({
  data() {
    return {
       myColor: 'green',
       message: 'Hello!'
    }
  }
}).directive('text-color', (el, binding) => {
  //ディレクティブの値に変化がなければ終了
  if(binding.value === binding.oldValue) { return; }
  el.style.color = binding.value;
  console.log(binding.value);
});
app.mount('#app');

修飾子付きのディレクティブ

ディレクティブ名の後にドット(.)区切りで、ディレクティブの修飾子を指定することができます。

フック関数の中では修飾子を binding.modifiers で参照することができ、「binding.modifiers.名前」で真偽値(その修飾子が指定されていれば true、指定されていなければ false)が返ってきます。

以下は指定した値の文字色を適用する v-text-color というディレクティブに、italic、bold、underline という修飾子を追加する例です。

JavaScript
const app = Vue.createApp({
  data() {
    return {
       myColor: 'green'
    }
  }
}).directive('text-color', {
  mounted(el, binding) {
    el.style.color = binding.value;
    //修飾子 italic が指定されていれば
    if(binding.modifiers.italic) {
      el.style.fontStyle = 'italic';
    }
    //修飾子 bold が指定されていれば
    if(binding.modifiers.bold) {
      el.style.fontWeight = 'bold';
    }
    //修飾子 underline が指定されていれば
    if(binding.modifiers.underline) {
      el.style.textDecoration = 'underline';
    }
  }
});
app.mount('#app');

修飾子は複数同時に指定することができます。

HTML
<div id="app">
  <div v-text-color.italic="myColor">Hello!</div> <!-- 緑色で斜体 -->
  <div v-text-color.bold="myColor">Hello!</div> <!-- 緑色で太字 -->
  <div v-text-color.italic.bold="myColor">Hello!</div> <!-- 緑色で斜体で太字 -->
  <div v-text-color.underline.italic="myColor">Hello!</div> <!-- 緑色で下線付き斜体 -->
</div>

ディレクティブでイベント処理

ディレクティブでイベントリスナーを設定することもできます。

フック関数の中で、ディレクティブがバインド(適用)される要素を表す引数 el に addEventListener を使ってイベントリスナーを登録することができます。

以下の v-mo-color ディレクティブはマウスオーバーすると背景色を付け、マウスアウトすると背景色を元に戻します。初期化のタイミングで呼び出される mounted フック関数でディレクティブが適用される要素にイベントリスナーを登録しています。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myColor: 'lightgreen',
    }
  }
}).directive('mo-color', {
  mounted(el, binding) {
    //ディレクティブが適用される要素にイベントリスナーを登録
    el.addEventListener('mouseenter', (e) => {
      el.style.backgroundColor = binding.value;
      //または e.currentTarget.style.backgroundColor = binding.value;
    });
    el.addEventListener('mouseleave', (e) => {
      el.style.backgroundColor = null;
      //または e.currentTarget.style.backgroundColor = null;
    });
  }
});
app.mount('#app');
HTML
<div id="app">
  <div v-mo-color="myColor">Hello!</div>
</div>

以下は、ディレクティブを指定した要素を固定表示し、現在のスクロール量を表示する例です。親要素にある程度の高さがないとスクロールしないので、親要素に高さを指定しています。

HTML
<div style="margin-top:50px; height: 2000px;">
  <div id="app">
    <div v-log-scroll>window.scrollY</div>
  </div>
</div>
JavaScript
const app = Vue.createApp({})
.directive('log-scroll', {
  mounted(el) {
    window.addEventListener('scroll', () => {
      el.style.position = 'fixed';
      el.textContent = 'window.scrollY : ' + window.scrollY;
      console.log(window.scrollY);
    })
  }
});
app.mount('#app');

引数や属性値を利用

以下は指定された引数により異なるイベントを登録し、属性値(ディレクティブの値)により実行する処理を変える例です。

ディレクティブ名の後にコロン(:)区切りで指定された引数の値(文字列)は binding.arg で取得でき、属性値に指定されているメソッドは binding.value()で実行できます。

JavaScript
const app = Vue.createApp({
  methods: {
    //属性値に指定するメソッド
    alertHello() {
      alert('hello');
    },
    logHello() {
      console.log('hello');
    }
  }
}).directive('hello', {
  mounted(el, binding) {
    //引数に mouseenter が指定されていれば mouseenter イベントを登録
    if (binding.arg === 'mouseenter') {
      el.addEventListener("mouseenter", () => {
        //属性値に指定されているメソッドを実行
        binding.value();
      });
    //引数に dblclick が指定されていれば dblclick イベントを登録
    }else if (binding.arg === 'dblclick') {
      el.addEventListener("dblclick", () => {
        binding.value();
      });
    //引数が指定されていない(上記のいずれでもない)場合は click イベントを登録
    }else{
      el.addEventListener("click", () => {
        binding.value();
      });
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <div v-hello:mouseenter="logHello">Mouse Enter</div><!--マウスオーバーでログ出力-->
  <div v-hello:dblclick="alertHello">Double Click</div><!--ダブルクリックでアラート-->
  <div v-hello="alertHello">Click</div><!--クリックでアラート-->
</div>

修飾子を利用

以下は前述の例に、修飾子のオプションを追加したものです。ディレクティブに修飾子 bye が指定されていれば、アラートやログで出力する文字を変えています。

JavaScript
const app = Vue.createApp({
  methods: {
    alertHello(bye) {
      //引数が true であれば
      if(bye) {
        alert('hello and goodbye');
      }else{
        alert('hello');
      }
    },
    logHello(bye) {
      if(bye) {
        console.log('hello and goodbye');
      }else{
        console.log('hello');
      }
    }
  }
}).directive('hello', {
  mounted(el, binding) {
    if (binding.arg === 'mouseenter') {
      el.addEventListener("mouseenter", () => {
        //修飾子 bye が指定されていれば引数に true を指定
        binding.modifiers.bye ? binding.value(true) : binding.value();
      });
    }else if (binding.arg === 'dblclick') {
      el.addEventListener("dblclick", () => {
        binding.modifiers.bye ? binding.value(true) : binding.value();
      });
    }else{
      el.addEventListener("click", () => {
        binding.modifiers.bye ? binding.value(true) : binding.value();
      });
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <!--ダブルクリックで「hello」とアラート表示-->
  <div v-hello:dblclick="alertHello">Double Click</div>
  <!--クリックで「hello and goodbye」とコンソールに出力-->
  <div v-hello.bye="logHello">Click</div>
</div>

ミックスイン

ミックスイン (mixin) はコンポーネントオプションを再利用するための仕組み(機能)です。

mixin で定義した共通なオプション(data、methods、computed、life cycle)を component 内で使用できるようになります。

但し、以下のような 欠点があるため、Vue3 からはミックスインの使用は非推奨とされていて、 Composition API(Composables)が推奨されています。

  • プロパティのソースが不明確
  • 名前空間の競合
  • 暗黙的なクロスミックスイン通信

以下はコンポーネントの data オプション(データオブジェクト)の内容と「hello from mixin!」という文字列をコンソールに出力するミックスインです。

ミックスインは「オプション名: 値」形式で記述されたオブジェクトリテラルで、任意のコンポーネントオプションを含むことができます。

data、methods、computed、life cycle などコンポーネントで利用できる全てのオプションを利用でき、メソッドやライフサイクルフックでコンポーネントのインスタンスにアクセスすることができます。

以下の例では、created ライフサイクルフックでコンポーネントのデータオブジェクト($data インスタンスプロパティ)を参照しています。

コンポーネントにミックスインを組み込むには mixins オプションを利用します。mixins オプションでは、複数のミックスインを組み込めるので配列として指定します。

JavaScript
//ミックスインオブジェクトを定義
const myMixin = {
  created() {
    this.hello(); //このミックスインのメソッド hello() を実行
    console.log(this.$data); //コンポーネントの data オプションの内容を出力
  },
  methods: {
    hello() {
      console.log('hello from mixin!');
    }
  }
}

//ミックスインを使用するコンポーネントを定義
const app = Vue.createApp({})
.component('my-compo', {
  data() {
    return {
      current: new Date().toLocaleString()
    };
  },
  template: `<div>{{ current }}</div>`,
  //mixins オプションでミックスインを組み込む
  mixins: [myMixin]
});
app.mount('#app');
HTML
<div id="app">
  <my-compo></my-compo>
</div>

以下がコンソールに出力されます。

  hello from mixin!
  Proxy {current: '2022/9/22 19:52:34'}

ブラウザには以下が出力されます。

<div id="app" data-v-app="">
<div>2022/9/22 19:52:34</div>
</div>

オプションのマージ

ミックスインとコンポーネントで同じオプションが定義されている(重複したオプションを含む)場合、以下のようなルールでマージされます。

data オプション

各ミックスインはそれぞれの data オプションを持つことができます。それぞれのオプション(関数)が呼び出され、返されたオブジェクトがマージされます。コンフリクトした場合(同名のプロパティがある場合)には、コンポーネント自身の data のプロパティが優先されます。

//ミックスイン
const myMixin = {
  //data オプション(オブジェクトを返す関数)
  data() {
    return {
      message: 'hello',
      foo: 'Foo'
    }
  }
}

//ミックスインを使用するコンポーネント
const app = Vue.createApp({
  mixins: [myMixin],
  //data オプション(オブジェクトを返す関数)
  data() {
    return {
      //data オプション(オブジェクトを返す関数)
      message: 'goodbye',
      bar: 'Bar'
    }
  },
  created() {
    //オブジェクトがマージされ、同名のプロパティはコンポーネントのプロパティが優先される
    console.log(this.$data) // => { message: "goodbye", foo: "Foo", bar: "Bar" }
  }
})
app.mount('#app');

オブジェクトの値を期待するオプション

methods や computed、components、directives などのオブジェクトの値を期待するオプションは、data オプション同様、同じオブジェクトにマージされ、これらのオブジェクトでキーのコンフリクトがある場合は、コンポーネントオプションが優先されます。

//ミックスイン
const myMixin = {
  methods: {
    foo() {
      console.log('foo')
    },
    conflicting() {
      console.log('from mixin')
    }
  }
}

//ミックスインを使用するコンポーネント
const app = Vue.createApp({
  mixins: [myMixin],
  methods: {
    bar() {
      console.log('bar')
    },
    //同名のメソッド
    conflicting() {
      console.log('from component')
    }
  }
})
//コンポーネントのインスタンスを変数 vm に代入
const vm = app.mount('#app');

vm.foo() // => "foo"
vm.bar() // => "bar"
//同名のメソッド(コンポーネントオプションが優先される)
vm.conflicting() // => "from component"

ライフサイクルフック

同じ(名前の)ライフサイクルフックはそれら全てが呼び出されるよう配列にマージされます。ミックスインのフックはコンポーネント自身のフックの前に呼び出されます。

//ミックスイン
const myMixin = {
  created() {
    console.log('mixin hook called')
  }
}

//ミックスインを使用するコンポーネント
const app = Vue.createApp({
  mixins: [myMixin],
  //同名のライフサイクルフック
  created() {
    console.log('component hook called')
  }
})
app.mount('#app');

//以下が出力される
//mixin hook called
//component hook called

カスタムオプション

カスタムオプションは単純に上書きされます。カスタムオプションはインスタンスプロパティの $options でアクセスできます。

//ミックスイン
const myMixin = {
  //カスタムオプション
  myOption: 'Mixin'
}

//ミックスインを使用するコンポーネント
const app = Vue.createApp({
  mixins: [myMixin],
  //同名のカスタムオプション
  myOption: 'Component'
})
const vm = app.mount('#app');

console.log(vm.$options.myOption);
//Component

カスタムロジックを使用してカスタムオプションをマージ

カスタムオプションはデフォルトでは単純に上書きされますが、カスタムロジックを使用してカスタムオプションを独自のルールでマージすることもできます。

カスタムオプションが重複した場合のルールは optionMergeStrategies を使います。使い方は、「app.config.optionMergeStrategies.カスタムオプション名」にルールを表す関数を定義します。

app.config.optionMergeStrategies.customOption = (toVal, fromVal) => {
  // マージされた値を返す
}

ルールを表す関数は、親インスタンスと子インスタンスで定義されたオプションの値をそれぞれ第 1 引数と第 2 引数として受け取り、メージの結果を戻り値として返します。

  • toVal:マージ先の値
  • fromVal:マージ元の値

以下はルールを表す関数の引数に何が入っているかを確認する例です。

//ミックスイン
const myMixin = {
  //カスタムオプション
  myOption: 'Mixin'
};

//ミックスインを使用するコンポーネント
const app = Vue.createApp({})
.component('my-compo', {
  //重複したカスタムオプション
  myOption: 'Component',
  template: `<div>{{ $options.myOption }} </div>`,
  mixins: [myMixin],
  created() {
    console.log('created : ' + this.$options.myOption); // => created : Component
  }
});

//マージルールを定義(マウントする前)
app.config.optionMergeStrategies.myOption = (toVal, fromVal) => {
  console.log('toVal(マージ先の値): ' + toVal);
  console.log('fromVal(マージ元の値): ' + fromVal);
  return fromVal || toVal;
}

app.mount('#app');

上記を実行すると、以下のように最初にミックスインから、次にコンポーネントから出力された toVal と fromVal がコンソールに表示されます(関数がそれぞれで実行される)。

24行目の return では fromVal が存在する場合には常にそれが返されるため、最終的には this.$options.myOption に Component がセットされます。

toVal(マージ先の値): undefined
fromVal(マージ元の値): Mixin
toVal(マージ先の値): Mixin
fromVal(マージ元の値): Component
created : Component  //created() フック

常に子インスタンスの値を返すようにするには、マージルールを以下のようにします。

app.config.optionMergeStrategies.myOption = (toVal, fromVal) => {
  //toVal が存在する場合には常に toVal を返す
  return toVal || fromVal
}

以下は、カスタムオプションとして配列を用意し、競合した場合は配列を連結する例です。

この例の場合、引数 toVal と fromVal が undefined の場合の既定値として空の配列を指定しています。

JavaScript
//ミックスイン
const myMixin = {
  //カスタムオプション
  myOption: ['foo', 'bar']
};

//ミックスインを使用するコンポーネント
const app = Vue.createApp({})
.component('my-compo', {
  //重複したカスタムオプション
  myOption: ['baz'],
  template: `<div>{{ $options.myOption }} </div>`,
  mixins: [myMixin],
  created() {
    console.log(this.$options.myOption); // =>(3) ['baz', 'foo', 'bar']
  }
});

//マージルールを定義(引数 toVal と fromVal が undefined の場合の既定値を指定)
app.config.optionMergeStrategies.myOption = (toVal=[], fromVal=[]) => {
  return fromVal.concat(toVal)
}

app.mount('#app');
HTML
<div id="app">
  <my-compo></my-compo>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div>["baz","foo","bar"]</div>
</div>

もし、引数 toVal と fromVal に既定値として空の配列を指定しない場合は、以下のような出力になります。

<div id="app" data-v-app="">
  <div>["baz","foo","bar", null]</div>
</div>

グローバルミックスイン

mixin メソッドを使って無条件に全てのコンポーネントに適用されるグローバルミックスインを定義することもできます。

コンポーネント側では、mixins オプションを指定する必要はありません。

const app = Vue.createApp({
  myOption: 'hello!'
});

// myOption オプションが存在すれば、その値をコンソールに出力するグローバルミックスイン
app.mixin({
  created() {
    const myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption); //hello!
    }
  }
});

app.mount('#app');

グローバルにミックスインを適用すると、その後にアプリ内で作成される全ての Vue コンポーネントインスタンスに影響するので注意が必要です。

const app = Vue.createApp({
  myOption: 'hello!'
})

// `myOption` カスタムオプションにハンドラを注入する
app.mixin({
  created() {
    const myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
});

// myOption を子コンポーネントにも追加
app.component('my-compo', {
  myOption: 'hello from child component!',
  template: `<div>{{ this.$options.myOption }}</div>`
})

app.mount('#app');

//それぞれのコンポーネントでログが出力される
//hello!
//hello from child component!

以下は、コンポーネントの data オブジェクトに title プロパティが存在すれば、<title> 要素の値(document.title)に設定する例です。

const app = Vue.createApp({})
.mixin({
  created() {
    //data オブジェクトに title プロパティが存在すれば
    if(this.$data.title) {
      //data の title プロパティを document.title(title 要素の値)に設定
      document.title = this.$data.title;
      // または document.querySelector('title').textContent = this.$data.title;
    }
  }
});

app.component('my-compo', {
  template: `<div>My Compo</div>`,
  data() {
    return {
      //title プロパティが存在するので、この値が title 要素に反映される
      title: 'Global Mixin!'
    }
  }
})

app.mount('#app');

<head> 内の title 要素は以下のように出力されます。

<title>Global Mixin!</title>

プラグイン

プラグインを利用すると目的に特化した機能を Vue に追加することができます。

Vue で利用できるプラグインは以下のページで探すことができます(プラグイン以外にもフレームワークやライブラリなど、Vue 関連のリストが多数掲載されています)。

github.com/vuejs/awesome-vue | awesome-vue.js.org

プラグインをインストールして利用するには、使用するプラグインを読み込んで use() メソッドを使って第1引数にプラグイン、第2引数にプラグインのオプション(プラグインごとに異なる)を指定します。

例えば、myPlugin というプラグインをインストールするには以下のようになります。

CDN で読み込んでいる場合(別途プラグインを読み込みます)
const app = Vue.createApp({
  /* ... */
})

app.use(myPlugin, {
  /* 省略可能なオプション */
});

import を使用している場合は、以下のようになります。

import { createApp } from 'vue'
import MyPlugin from './plugins/MyPlugin'

const app = createApp({
  /* ... */
})

app.use(myPlugin, {
  /* 省略可能なオプション */
});

プラグイン

以下はサードパーティで提供されているプラグイン(ライブラリ)を利用する例です。

Element Plus

Element Plus は Vue 3 ベースのコンポーネントライブラリで、様々な UI コンポーネントが含まれています。以下は Element Plus のページへのリンクです。

以下で使用している Element Plus のバージョンは 2.2.18 です。

以下は Component のボタンのページの例です。Component にはフォームメニューカルーセルなどたくさんの UI コンポーネントがあります。

Installation

パッケージマネージャ(NPM, Yarn, pnpm)を使ってインストールする方法が推奨されていますが、CDN 経由で読み込むこともできます。

パッケージマネージャを利用する場合
# NPM
$ npm install element-plus --save

# Yarn
$ yarn add element-plus

# pnpm
$ pnpm install element-plus

以下は unpkg を利用して CDN で読み込む例です。JavaScript は body の閉じタグの前で読み込むこともできます。

<head>
  <!-- Import style -->
  <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
  <!-- Import Vue 3 -->
  <script src="//unpkg.com/vue@3"></script>
  <!-- Import component library -->
  <script src="//unpkg.com/element-plus"></script>
</head>

※ CDN を使用してインポートする場合は、Element Plus がアップグレードされたときに互換性のないアップデートの影響を受けないように、リンクアドレスでバージョンをロックすることが推奨されています。

バージョンをロックするには以下のように @ の後にバージョンを指定します。

<head>
  <!-- バージョン 2.2.18 -->
  <link rel="stylesheet" href="//unpkg.com/element-plus@2.2.18/dist/index.css" />
  <!-- Import Vue 3 -->
  <script src="//unpkg.com/vue@3"></script>
  <!-- バージョン 2.2.18 -->
  <script src="//unpkg.com/element-plus@2.2.18"></script>
</head>

Installation

ボタンの実装例

以下は CDN で Element Plus を読み込み、ボタン(Button コンポーネント)を実装する例です。

Element Plus のインストールは use() メソッドの第1引数にグローバル変数 ElementPlus を指定します。ボタンは <el-button> 要素で呼び出せます。

<el-button> 要素に指定できる属性は Button Attributes で確認できます。以下は type 属性に primary を指定しているので青系の色のボタンが表示されます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- import CSS -->
  <link rel="stylesheet" href="//unpkg.com/element-plus@2.2.18/dist/index.css" />
  <title>Vue Sample</title>
</head>
<body>

<div id="app">
  <!-- ボタン -->
  <el-button type="primary">{{ message }}</el-button>
</div>

<!-- import Vue JavaScript -->
<script src="//unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- import Element Plus JavaScript -->
<script src="//unpkg.com/element-plus@2.2.18"></script>
<script>
const app = Vue.createApp({
  setup() {
    const message = "Hello Element Plus";
    return {
      message
    }
  }
});
// Element Plus をインストール(グローバル変数 ElementPlus を指定)
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

パッケージマネージャを利用している場合の Element Plus のインストールは以下のようになります。

Quick Start

main.js
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';

const app = createApp(App);
app.use(ElementPlus);
app.mount('#app');

Element Plus の全てを読み込む Full Import ではなく、必要なものだけを利用することもできるようです。詳細は On-demand Import を参照。

Global configuration

use() メソッドを使って Element Plus を登録する際に、form コンポーネントの size と popup コンポーネントの zIndex を設定することができます。zIndex のデフォルト値は 2000 です。

以下のように use() メソッドの第2引数のオプションにグローバル設定オブジェクト(global config object)を渡します。

const app = createApp(App);

app.use(ElementPlus, {
  size: 'small', //form コンポーネントのサイズを small に
  zIndex: 3000 //popup コンポーネントの zIndex を 3000 に
});

Global Configuration

Internationalization

Element Plus のコンポーネントはデフォルトで英語を使用していますが、他の言語を使用したい場合は use() メソッドの第2引数のオプション(グローバル設定オブジェクト)に指定することができます。

※DatePicker や TimePicker などのコンポーネントで日本語のロケールを使用する場合に設定します。ボタンやカルーセルなどのコンポーネントを利用する場合は不要です。

import { createApp } from 'vue';
import ElementPlus from 'element-plus';
//日本語リソースをインポート
import Ja from 'element-plus/dist/locale/ja.mjs'
import 'element-plus/dist/index.css';
import App from './App.vue';

const app = createApp(App);

app.use(ElementPlus, {
  locale: Ja, //ロケールを日本語に
});
app.mount('#app');

CDN の場合

CDN で利用している場合は、以下のようにします。

<script src="//unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="//unpkg.com/element-plus@2.2.18"></script>
<!-- 日本語リソースをインポート -->
<script src="//unpkg.com/element-plus@2.2.18/dist/locale/ja"></script>
<script>
const app = Vue.createApp({});

app.use(ElementPlus, {
  //ElementPlusLocale の後に Ja(言語コード)を続ける
  locale: ElementPlusLocaleJa,
})
app.mount("#app");
</script>

Internationalization

Vue I18n

Vue I18n は Vue アプリを国際化する(複数言語に対応させる)ためのライブラリで、I18n とは「Internationalization」の意味です(I と n の間に18文字あることから)。

ドキュメントは以下で確認できます。

以下で使用している Vue I18n のバージョンは 9.2.2 です。

以下は CDN を利用した日本語と英語のページに対応する基本的なサンプルです(Getting started)。

HTML(Vue と Vue I18n の読み込み)
<!-- Vue の読み込み -->
<script src="//unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Vue I18n(バージョン 9 の最新版)の読み込み -->
<script src="https://unpkg.com/vue-i18n@9"></script>
  1. en や ja などの言語名(ロケール)を最上位プロパティとする階層構造のオブジェクト(階層の深さは自由)で、「キー名:値」の形式で翻訳情報を定義し、変数名は messages としておきます。

  2. CDN 経由で利用する場合は、グローバルの VueI18n を使って createI18n() メソッドにオプションを指定して i18n のインスタンスを生成します。

  3. Vue のインスタンスを生成します。

  4. 生成した i18n インスタンスを Vue の use() メソッドに渡して Vue I18n をインストールして有効化します。

JavaScript( Legacy API: compatible with vue-i18n v8.x)
// 1. 翻訳情報(translated locale messages)を用意
const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}

// 2. VueI18n.createI18n() にオプションを指定して i18n インスタンスを生成
const i18n = VueI18n.createI18n({
  locale: 'ja', // アプリで使用するロケールを指定
  fallbackLocale: 'en', // locale で指定した言語の情報(翻訳)がない場合のフォールバックを指定
  messages, // 翻訳情報を指定(messages: messages の省略形)
  // その他のオプション
});

// 3. Vue のインスタンスを生成
const app = Vue.createApp({
  // 必要なオプションを設定
});

// 4. 生成した i18n インスタンスを use() に渡して Vue I18n をインストール(有効化)
app.use(i18n);

// 5. アプリをマウント
app.mount("#app");

Vue I18n をインストールして有効化すると、コンポーネントで $t() メソッドを使うことができます。

HTML
<div id="app">
  <p>{{ $t("message.hello") }}</p>
  <!-- $t() メソッドで現在のロケール(locale)の翻訳を取得 -->
</div>

この例では locale の値が ja なので、$t("message.hello") は message.hello の値として「こんにちは、世界」を取得します(locale の値を en にすると「hello world」を取得します)。

出力
<div id="app" data-v-app="">
  <p>こんにちは、世界</p>
</div>

大まかな仕組みとしては、messages という翻訳情報のオブジェクトにそれぞれの言語ごとのメッセージを定義しておき、テンプレートに {{ $t("message.hello") }} と記述すると、locale で指定した言語の定義したメッセージを表示できます。

フォールバック fallbackLocale

locale で指定した優先言語に翻訳がない場合のフォールバックとして、fallbackLocale オプションに使用する言語を指定することができます。

暗黙的なフォールバック

ロケールに地域などが指定されている場合(en-US など)、暗黙的なフォールバックが自動的に有効になります。

例えば、de-DE-bavarian の場合、以下のようにフォールバックされます。

  1. de-DE-bavarian
  2. de-DE
  3. de

自動フォールバックを抑制するには、末尾に ! を追加します (例: de-DE!)。

Fallbacking

翻訳情報の分離

翻訳情報の messages は外部ファイルとして読み込むこともできるので、他のコードと分離することもできます。以下は翻訳情報を messages.js というファイルに保存して import で読み込む例です。

messages.js
export const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}
<script type="module">
  //翻訳情報 messages をインポート
  import { messages } from './messages.js';

  const i18n = VueI18n.createI18n({
    locale: 'ja',
    fallbackLocale: 'en',
    messages  // messages: messages の省略形
  });

  const app = Vue.createApp({});
  app.use(i18n);
  app.mount("#app");
</script>
Installation

CDN を利用する場合、以下のような方法で Vue I18n を読み込むことができます。

<!-- バージョン 9 の最新版を読み込む場合 -->
<script src="https://unpkg.com/vue-i18n@9"></script>

<!-- 特定のバージョン(以下は 9.2.2)を読み込む場合 -->
<script src="https://unpkg.com/vue-i18n@9.2.2/dist/vue-i18n.global.js"></script>

<!-- バージョン 9.2.2 の prod.js(本番環境用)を読み込む場合 -->
<script src="https://unpkg.com/vue-i18n@9.2.2/dist/vue-i18n.global.prod.js"></script>

パッケージマネージャを利用する場合
# NPM
$ npm install vue-i18n@9

# Yarn
$ yarn add vue-i18n@9

Installation

Composition API モード

Vue の Composition API を使用する場合は、VueI18n.createI18n() に指定するオプションで legacy オプションを false に設定する必要があります。

// 2. VueI18n.createI18n() にオプションを指定して i18n インスタンスを生成
const i18n = VueI18n.createI18n({
  legacy: false, // Composition API の場合は legacy を false にしなければならない
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
});

legacy: false を設定することで、Vue I18n が API モードを Legacy API モードから Composition API モードに切り替えるので、コンポーネントの setup で useI18n() メソッドを使用することができます。

useI18n() メソッドは Composer インスタンスを返します。

Composer インスタンスは、VueI18n インスタンスと同様に、t() メソッドなどの translation API と、locale や fallbackLocale などのプロパティを提供します。

useI18n() メソッドは setup() メソッドの先頭で呼び出す必要があります。

// 3. Vue のインスタンスを生成
const app = Vue.createApp({
  // setup() で useI18n() を呼び出して Composer インスタンスの t メソッドを取得して返す
  setup() {
    const { t } = VueI18n.useI18n();  //先頭で useI18n() メソッドを呼び出す
    return { t };
  }
});
JavaScript(Composition API)
const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}

const i18n = VueI18n.createI18n({
  legacy: false, // legacy を false に
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
});

const app = Vue.createApp({
  setup() {
    // useI18n() を呼び出して t メソッドを取得して返す
    const { t } = VueI18n.useI18n();
    return { t };
  }
});

app.use(i18n);
app.mount("#app");

setup() でレンダーコンテキストとして t を返すことにより、コンポーネントのテンプレートで t() メソッドを使用できます。

HTML
<div id="app">
  <p>{{ t("message.hello") }}</p>
</div>
出力
<div id="app" data-v-app="">
  <p>こんにちは、世界</p>
</div>

以下は setup() の中で Composer インスタンスのプロパティを使用する例です。locale などの値は value プロパティでアクセスしていますが、テンプレートでは自動的にアンラップされるので .value は不要です。

必要であれば、t メソッドを使って翻訳情報にアクセスすることができます。

const app = Vue.createApp({
  setup() {
    //Composer インスタンスのプロパティを取得
    const { t, locale, fallbackLocale, messages } = VueI18n.useI18n();

    // Composer インスタンス
    console.log(VueI18n.useI18n()); // 出力は以下
    // {id: 1, locale: ComputedRefImpl, fallbackLocale: ComputedRefImpl, …}
    // locale の value プロパティ
    console.log(locale.value);  // ja
    console.log(fallbackLocale.value);  // en
    console.log(messages.value.en.message.hello);  // hello world
    // t メソッドで翻訳情報にアクセス
    console.log(t('message.hello'));  //こんにちは、世界(locale が ja の場合)

    return { t };
  }
});
ロケールの切り替え

今までの例ではロケールをアプリ側で固定していますが、ブラウザの言語設定に応じて動的に切り替えたり、ボタンやセレクトボックスで切り替えることもできます。

ブラウザの言語設定に応じて切り替え

以下はブラウザの言語設定に応じて、ロケールを動的に切り替える例です。

VueI18n.createI18n() の locale の指定を以下のようにブラウザの言語設定を取得するようにします。

navigator.languages はユーザーの言語を表す DOMString の配列を返し、配列の中では最も優先される言語が最初に来るように並べられているので、先頭の要素を取得するようにしています。

また、ブラウザによっては navigator.language が存在しないので、navigator.userLanguage や navigator.browserLanguage を参照します。

const i18n = VueI18n.createI18n({
  legacy: false, // Composition API の場合は legacy を false に
  //ブラウザの言語設定を参照して取得
  locale: (navigator.languages && navigator.languages[0]) ||
    navigator.language ||
    navigator.userLanguage ||
    navigator.browserLanguage,
  fallbackLocale: 'en',
  messages,
});
JavaScript(Composition API)
const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}

const i18n = VueI18n.createI18n({
  legacy: false, // Composition API
  //ユーザー(ブラウザ)の言語を取得
  locale: (navigator.languages && navigator.languages[0]) ||
    navigator.language ||
    navigator.userLanguage ||
    navigator.browserLanguage,
  fallbackLocale: 'en',
  messages,
});

const app = Vue.createApp({
  // setup() で useI18n() を呼び出して t メソッドを取得して返す
  setup() {
    const { t } = VueI18n.useI18n();
    return { t };
  }
});

app.use(i18n);
app.mount("#app");

セレクトボックスで動的に切り替え

以下はセレクトボックスで言語を選択して動的に切り替える例です。

vue-i18n-next/examples/composition/started.html

HTML
<div id="app">
  <form>
    <label for="locale-select">{{ t('message.language') }}</label>
    <select id="locale-select" v-model="locale">
      <option value="en">en</option>
      <option value="ja">ja</option>
    </select>
  </form>
  <p>{{ t("message.hello") }}</p>
</div>
JavaScript(Composition API)
const { createApp } = Vue;
const { createI18n, useI18n } = VueI18n;

const i18n = createI18n({
  legacy: false, // Composition API
  locale: 'ja',
  messages: {
    en: {
      message: {
        language: 'Language',  //セレクトボックスのラベル
        hello: 'hello world!'
      }
    },
    ja: {
      message: {
        language: '言語',  //セレクトボックスのラベル
        hello: 'こんにちは、世界!'
      }
    }
  }
});

const app = createApp({
  setup() {
    // useI18n() から t と locale を取得(先頭で useI18n() メソッドを呼び出す)
    const { t, locale } = useI18n();
    // t と locale を返して、テンプレートで使用できるように
    return { t, locale };
  }
});
app.use(i18n);
app.mount('#app');
翻訳情報の構文

Vue I18n は以下のような構文(Message Format Syntax)を使用して、UI に表示されるメッセージをローカライズできます。

Named interpolation(名前付き補間)

翻訳文字列に {xxxx} の形式で変数を埋め込むことができます。{ } をプレースホルダーと呼びます。

以下は message.hello キーの値に name という変数を埋め込んでいます。変数は複数埋め込むこともできます。Named interpolation

JavaScript
const messages = {
  en: {
    message: {
      hello: 'Hello {name}'  //名前付き補間
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、{name}'  //名前付き補間
    }
  }
}

const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages
});
const app = Vue.createApp({
  setup() {
    const { t } = VueI18n.useI18n();
    return { t };
  }
});
app.use(i18n);
app.mount("#app");

変数に値を指定するには、t メソッド(Legacy API の場合は $t メソッド)の呼び出しで以下のように指定します。

HTML
<div id="app">
  <p>{{ t('message.hello', {name: '太郎さん'}) }}</p>
</div>
出力
<div id="app" data-v-app="">
  <p>こんにちは、太郎さん</p>
</div>

上記の場合、ロケールを en に切り替えると出力は「Hello 太郎さん」となってしまいます。

以下のようにすれば、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは、太郎さん」と出力されます。

const messages = {
  en: {
    message: {
      hello: 'Hello {name}',
      taro: 'Taro-san'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、{name}',
      taro: '太郎さん'
    }
  }
}
<div id="app">
  <p>{{ t('message.hello', {name: t('message.taro')}) }}</p>
</div>

以下のように computed メソッドを使って t メソッドの値を返すことで同じことができます。

JavaScript(Composition API)
const messages = {
  en: {
    message: {
      hello: 'Hello {name}',
      taro: 'Taro-san'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、{name}',
      taro: '太郎さん'
    }
  }
}

const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages
});

const app = Vue.createApp({
  setup() {
    const { t } = VueI18n.useI18n();
    // computed メソッドを使って t('message.taro') を返す
    const taro = Vue.computed(() =>  t('message.taro'));
    return { t, taro };
  }
});
app.use(i18n);
app.mount("#app");
HTML
<div id="app">
  <p>{{ t('message.hello', { name: taro }) }}</p>
</div>

List interpolation(リスト補間)

プレースホルダーには、{0} のように配列のインデックスを指定することもできます。List interpolation

JavaScript(Composition API)
const messages = {
  en: {
    message: {
      hello: 'Hello {0}'  //リスト補間
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、{1}'  //リスト補間
    }
  }
}
const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages
});
const app = Vue.createApp({
  setup() {
    const { t } = VueI18n.useI18n();
    return { t };
  }
});
app.use(i18n);
app.mount("#app");

テンプレートで以下のように記述すると、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは、太郎さん」と出力されます

HTML
<div id="app">
  <p>{{ t('message.hello', ['Taro-san', '太郎さん']) }}</p>
</div>

Literal interpolation(リテラル補間)

例えば、以下の文字は特殊文字なので、翻訳文字列ではそのまま使うことができません(Special Characters)。

{ , } , @ , $ , |

プレースホルダーの中で文字列をシングルクォートで囲むことで、その文字(または文字列)を特殊文字ではなく単なる文字として認識させることができます。Literal interpolation

以下の場合、@ はメッセージ参照の特殊文字ではなく、単なる @ という文字として認識されます。

const messages = {
  en: {
    address: "{account}{'@'}{domain}" // @ をリテラル補間
}
HTML
<div id="app">
  <p>email:{{ t('message.address', { account:'foo', domain:'example.com' }) }}</p>
</div>
出力
<div id="app" data-v-app="">
  <p>email:foo@example.com</p>
</div>

Linked messages(翻訳文字列の参照)

@: の後にキー名を指定して、別の翻訳文字列を参照して利用することができます。Linked messages

JavaScript(Composition API)
const messages = {
  en: {
    message: {
      hello: 'Hello',
      taro: 'Taro-san',
      greeting: '@:message.hello @:message.taro' //翻訳文字列の参照
    }
  },
  ja: {
    message: {
      hello: 'こんにちは',
      taro: '太郎さん',
      greeting: '@:message.hello @:message.taro' //翻訳文字列の参照
    }
  }
}
const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages
});
const app = Vue.createApp({
  setup() {
    const { t } = VueI18n.useI18n();
    return { t };
  }
});
app.use(i18n);
app.mount("#app");

テンプレートで以下のように記述すると、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは 太郎さん」と出力されます

HTML
<div id="app">
  <p>{{ t('message.greeting') }}</p>
</div>
日時のフォーマット

Vue I18n では日時のフォーマットを定義してロケールにあわせてローカライズすることができます。

以下のような名前付きの日時形式 (short、long など) を定義することができます。Datetime Formatting

const datetimeFormats = {
  'en': {
    short: {
      year: 'numeric', month: 'short', day: 'numeric'
    },
    long: {
      year: 'numeric', month: 'short', day: 'numeric',
      weekday: 'short', hour: 'numeric', minute: 'numeric'
    }
  },
  'ja': {
    short: {
      year: 'numeric', month: 'short', day: 'numeric'
    },
    long: {
      year: 'numeric', month: 'short', day: 'numeric',
      weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true
    }
  }
}

関連(参考):MDN / Intl.DateTimeFormat

フォーマットは以下のような形式で定義します。フォーマット名はフォーマットを適用するためのキーとなります。任意の名前を指定できますが、通常は short や long を使います。

const 変数名 = {
  言語名: {
    フォーマット名: {
      オプション名: 値
      ・・・
    },
    ・・・
  },
  ・・・
}

フォーマットの有効化

フォーマットを定義したら、createI18n() の datetimeFormats オプションに渡します。

const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
  datetimeFormats  //datetimeFormats: datetimeFormats の省略形
});

整形

$d メソッドを使って日付を指定の形式で整形することができます。

$d メソッドの第2引数には、定義した日時のフォーマットのキー(short や long などのフォーマット名)を指定できます。オプションで第3引数にロケールなどを指定することができます(その他いろいろな引数の指定方法があります)。

<div id="app">
  <p>{{ $d(new Date(), 'short') }}</p>
  <p>{{ $d(new Date(), 'long') }}</p>
  <p>{{ $d(new Date(), 'short', 'en') }}</p>
  <p>{{ $d(new Date(), 'long', 'en') }}</p>
</div>

locale が ja の場合、例えば以下のように出力されます。

<div id="app" data-v-app="">
  <p>2022年10月23日</p>
  <p>2022年10月23日(日) 午後4:30</p>
  <p>Oct 23, 2022</p>
  <p>Sun, Oct 23, 2022, 4:30 PM</p>
</div>

$d メソッドは Legacy API モードと Composition API モードのどちらでも利用できます。Composition API モードでは、useI18n() から d メソッドを取得して同様に使用することができます。

HTML
<div id="app">
  <p>{{ d(new Date(), 'short') }}</p>
  <p>{{ d(new Date(), 'long') }}</p>
  <p>{{ d(new Date(), 'short', 'en') }}</p>
  <p>{{ d(new Date(), 'long', 'en') }}</p>
</div>
JavaScript(Composition API)
const datetimeFormats = {
  'en': {
    short: {
      year: 'numeric', month: 'short', day: 'numeric'
    },
    long: {
      year: 'numeric', month: 'short', day: 'numeric',
      weekday: 'short', hour: 'numeric', minute: 'numeric'
    }
  },
  'ja': {
    short: {
      year: 'numeric', month: 'short', day: 'numeric'
    },
    long: {
      year: 'numeric', month: 'short', day: 'numeric',
      weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true
    }
  }
}

const messages = {
  en: {
    message: {
      hello: 'Hello'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは'
    }
  }
}
const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
  datetimeFormats
});
const app = Vue.createApp({
  setup() {
    // d メソッドを取得して返す
    const { t, d } = VueI18n.useI18n();
    return { t, d };
  }
});
app.use(i18n);
app.mount("#app");
数値のフォーマット

Vue I18n では日時のフォーマット同様、数値のフォーマットを定義してロケールにあわせてローカライズすることができます。Number Formatting

以下のような名前付きの数値形式 (currency や decimal など) を定義することができます。

const numberFormats = {
  'en': {
    currency: {
      style: 'currency', currency: 'USD', notation: 'standard'
    },
    decimal: {
      style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2
    },
    percent: {
      style: 'percent', useGrouping: false
    }
  },
  'ja': {
    currency: {
      style: 'currency', currency: 'JPY', useGrouping: true, currencyDisplay: 'symbol'
    },
    decimal: {
      style: 'decimal', minimumSignificantDigits: 3, maximumSignificantDigits: 5
    },
    percent: {
      style: 'percent', useGrouping: false
    }
  }
}

関連(参考):MDN / Intl.NumberFormat

フォーマットは日時と同様、以下のような形式で定義します。フォーマット名は任意の名前を指定できますが、通常は currency や decimal、percent などを使います。

const 変数名 = {
  言語名: {
    フォーマット名: {
      オプション名: 値
      ・・・
    },
    ・・・
  },
  ・・・
}

フォーマットの有効化

フォーマットを定義したら、createI18n() の numberFormats オプションに渡します。

const i18n = VueI18n.createI18n({
  legacy: false,
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
  numberFormats  //numberFormats: numberFormats の省略形
});

整形

$n メソッドを使って数値を指定の形式で整形することができます。

$n メソッドの第2引数には、定義した数値のフォーマットのキー(currency や percent、decimal などのフォーマット名)を指定できます。オプションで第3引数にロケールを指定することができます($d メソッド同様、その他にもいろいろな引数の指定方法があります)。

<div id="app">
  <p>{{ $n(10000, 'currency') }}</p>
  <p>{{ $n(10000, 'currency', 'ja') }}</p>
  <p>{{ $n(10000, 'currency', 'ja', { useGrouping: false }) }}</p>
  <p>{{ $n(987654321, 'currency', { notation: 'compact' }) }}</p>
  <p>{{ $n(0.99123, 'percent') }}</p>
  <p>{{ $n(0.99123, 'percent', { minimumFractionDigits: 2 }) }}</p>
  <p>{{ $n(12.11612345, 'decimal') }}</p>
  <p>{{ $n(12145281111, 'decimal', 'ja') }}</p>
</div>

locale が ja の場合、例えば以下のように出力されます。

<div id="app" data-v-app="">
  <p>¥10,000</p>
  <p>¥10,000</p>
  <p>¥10000</p>
  <p>¥9.9億</p>
  <p>99%</p>
  <p>99.12%</p>
  <p>12.116</p>
  <p>12,145,000,000</p>
</div>

$n メソッドは $d メソッド同様、Legacy API モードと Composition API モードのどちらでも利用できます。Composition API モードでは、useI18n() から n メソッドを取得して同様に使用できます。

<div id="app">
  <p>{{ n(10000, 'currency') }}</p>
  <p>{{ n(10000, 'currency', 'ja') }}</p>
  <p>{{ n(10000, 'currency', 'ja', { useGrouping: false }) }}</p>
  <p>{{ n(987654321, 'currency', { notation: 'compact' }) }}</p>
  <p>{{ n(0.99123, 'percent') }}</p>
  <p>{{ n(0.99123, 'percent', { minimumFractionDigits: 2 }) }}</p>
  <p>{{ n(12.11612345, 'decimal') }}</p>
  <p>{{ n(12145281111, 'decimal', 'ja') }}</p>
</div>
const i18n = VueI18n.createI18n({
  legacy: false,  //Composition API
  locale: 'ja',
  fallbackLocale: 'en',
  messages,
  numberFormats
});
const app = Vue.createApp({
  setup() {
    // n メソッドを取得して返す
    const { t, n } = VueI18n.useI18n();
    return { t, n };
  }
});
app.use(i18n);
app.mount("#app");

プラグインの作成

プラグインは自分で作成することもできます。

プラグインは install() メソッドを持つオブジェクトか、またはインストール関数として動作する関数なので、コンソールにメッセージを出力するだけの単純なプラグインは以下のように定義することができます。

install() メソッドを持つオブジェクトとして定義
const MyPlugin = {
  install() {
    console.log('Hello from MyPlugin!');
  },
};
インストール関数として動作する関数として定義
const MyPlugin = () => {
  console.log('Hello from MyPlugin!');
};

プラグインを利用するには、createApp() で Vue のアプリケーションインスタンスを生成後、use() メソッドでアプリケーションにプラグインを追加します。

以下を記述すると、コンソールに「Hello from MyPlugin!」と出力されます。

const MyPlugin = {
  install() {
    console.log('Hello from MyPlugin!');
  },
};

//アプリケーションを生成
const app = Vue.createApp({});

//use() メソッドでアプリケーションにプラグインを追加
app.use(MyPlugin);

定義したプラグインを、例えば plugins フォルダに MyPlugin.js というファイル名で保存した場合は、以下のように読み込みます。

<!-- Vue の読み込み -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- MyPlugin.js の読み込み -->
<script src="plugins/MyPlugin.js"></script>
<script>
  const app = Vue.createApp({});
  //プラグイン MyPlugin をアプリケーションに追加
  app.use(MyPlugin);
</script>

以下は前述のプラグインをモジュールとして定義してデフォルトエクスポートする例です。

plugins/MyPlugin.js
export default {
  install() {
    console.log('Hello from MyPlugin!');
  },
}

利用する側のファイルでは定義したプラグインをインポートします。

以下ではインポートマップを使って Vue を読み込んでいますが、Chrome 以外のブラウザでは対応していないので、ES Module Shims を最初に読み込んでいます。

<script async src="https://ga.jspm.io/npm:es-module-shims@1.6.2/dist/es-module-shims.js"></script>
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>

<script type="module">
  import { createApp } from 'vue';
  import MyPlugin from './plugins/MyPlugin.js';
  const app = createApp({});
  app.use(MyPlugin);
</script>
パラメータ

プラグインは以下の2つのパラメータ(引数)を受け取ることができます。

  1. app:createApp() により生成されるアプリケーションのインスタンス
  2. options:app.use() に渡される追加のオプション

以下はプラグインの install() に渡されるパラメータをコンソールに出力して確認する例です。

const MyPlugin = {
  install(app, options) {
    // パラメータの値を確認
    console.log(app);  //以下の画像を参照
    console.log(app.version);  //3.2.41(Vue のバージョン)
    console.log(options);  //{name: 'name option'}
    console.log(options.name);  //name option
  },
};

const app = Vue.createApp({});
// use() にオプションを指定
app.use(MyPlugin, {name: 'name option'});

以下が出力例です。app には Vue アプリケーションのインスタンス、options には use() の第2引数に指定したオブジェクトが確認できます。

プラグインを関数として定義する場合も同じです。

//プラグインを関数として定義
const MyPlugin = (app, options) => {
  console.log(app);  //上記の画像を参照
  console.log(app.version);  //3.2.41(Vue のバージョン)
  console.log(options);  //{name: 'name option'}
  console.log(options.name);  //name option
};

const app = Vue.createApp({});
app.use(MyPlugin, {name: 'name option'});
サンプル

プラグインの install() メソッドの中では、app.component()app.directive() を使って、1つもしくは複数のグローバルなコンポーネントやカスタムディレクティブを登録したり、app.config.globalProperties にグローバルなインスタンスプロパティやメソッドを追加するなどができます。

以下はカスタムディレクティブの項で作成した v-text-color ディレクティブを定義する単純な自作のプラグイン MyTextColorPlugin の例です。

JavaScript
// プラグイン MyTextColorPlugin の定義
const MyTextColorPlugin = {
  install(app) {
    //アプリケーションにグローバルなカスタムディレクティブを定義(登録)
    app.directive('text-color', (el, binding) => {
      //カスタムディレクティブを指定した要素のテキストをその値の色に
      el.style.color = binding.value;
    });
  },
};

// アプリケーションの生成
const app = Vue.createApp({
  setup() {
    //テキストの色の初期値(データ)
    const myColor = Vue.ref('green');
    return { myColor }
  }
});
// プラグイン MyTextColorPlugin をアプリケーションに追加
app.use(MyTextColorPlugin);
// アプリケーションをマウント
app.mount('#app');

以下ではルートコンポーネントのテンプレートで、要素にプラグインのカスタムディレクティブを指定して、テキストの色を変更できるようにしています。

HTML
<div id="app">
  <div v-text-color="'purple'">It's Purple.</div>
  <select v-model="myColor">
    <option value="green">Green</option>
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="orange">Orange</option>
  </select>
  <h3 v-text-color="myColor">Color Title</h3>
</div>

最初の div 要素はカスタムディレクティブ v-text-color に指定した値('purple')により紫色に、h3 要素の色はセレクトボックスで選択された色になります(myColor の初期値は緑)。

出力
<div id="app" data-v-app="">
  <div style="color: purple;">It's Purple.</div>
  <select>
    <option value="green">Green</option>
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="orange">Orange</option>
  </select>
  <h3 style="color: green;">Color Title</h3>
</div>

以下は前述のプラグインにコンポーネントを追加した例です。

install() メソッドの第2パラメータ(options)に textColor プロパティを持つオブジェクトを期待しています。options に textColor プロパティが指定されていれば、その値を文字色の初期値として使用します。

プラグインを use() メソッドを使ってインストールする際に、オプションに textColor プロパティを持つオブジェクトを指定しています。オプションが省略されている場合は、文字色の初期値として green を使用します。

JavaScript
// プラグイン MyTextColorPlugin の定義
const MyTextColorPlugin = {
  install(app, options) {
    app.directive('text-color', (el, binding) => {
      el.style.color = binding.value;
    })
    .component('color-select-box', {
      template: `
      <select v-model="textColor">
        <option value="green">Green</option>
        <option value="blue">Blue</option>
        <option value="red">Red</option>
        <option value="orange">Orange</option>
      </select>
      <h3 v-text-color="textColor">Color Title</h3>
      `,
      setup() {
        //options に textColor プロパティが指定されていれば、その値を文字色に
        const textColor = options && options.textColor ?
          Vue.ref(options.textColor) : Vue.ref('green');
        return { textColor }
      }
    });
  },
};

// アプリケーションの生成
const app = Vue.createApp({});
// オプションにオブジェクトを指定してプラグインをアプリケーションに追加
app.use(MyTextColorPlugin, { textColor: 'red' });
app.mount('#app');

テンプレートでプラグインで登録したコンポーネント color-select-box を呼び出します。

HTML
<div id="app">
  <color-select-box></color-select-box>
</div>

以下が出力され、h3 要素の文字色がセレクトボックスで選択した色になります。

<div id="app" data-v-app="">
  <select>
    <option value="green">Green</option>
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="orange">Orange</option>
  </select>
  <h3 style="color: red;">Color Title</h3>
</div>