<template> に存在していない Vue コンポーネントを動的に追加

Vue インスタンスを作成して $mount すればおk。

add() {
  const instance = new Bar({
    propsData: {
      date: new Date().valueOf()
    }
  })
    .$on("click", date => console.log(date))
    .$mount();
  document.body.appendChild(instance.$el);
}
  • vm.$mount
    • vm.$mount() は アンマウントな Vue インスタンスのマウンティングを手動で開始するために使用することができます。

    • elementOrSelector 引数が提供されない場合、テンプレートはドキュメント要素外で描画され、ドキュメントにそれを挿入するためにあなた自身でネイティブ DOM API を使用しなければなりません。

$mount に elementOrSelector を設定、あるいは Vue インスタンスの生成時に el を設定しても一応可能ですが、その場合「追加」ではなく「置換」になるので注意が必要です。

vue-append-instance-programmatically - StackBlitz

利用ケース

デバッグ用の View を追加したいけど、<DebugView v-if="isDebug" /> のように既存のコードに手を加えたくない場合にこんな感じで対応しました。

Nuxt の場合 plugin を利用して以下のように記述できると思います。

// plugins/routes-viewer/index.ts
// 登録されている全てのルートを表示するコンポーネント
import { Context } from '@nuxt/types';
import VueRouter, { RouterOptions } from 'vue-router';
import Viewer from './Viewer.vue';

export default (context: Context) => {
  const router = context.app.router;
  if (!router) {
    return;
  }
  const removeGuard = router.afterEach(() => {
    const instance = new Viewer({
      propsData: {
        paths: pickPaths(router),
      },
    })
      .$on('click', (path: string) => router.push(path))
      .$mount();
    document.body.appendChild(instance.$el);
    removeGuard();
  });
};

const pickPaths = (router: VueRouter) => {
  const routes = (router.options as RouterOptions).routes;
  if (!routes) {
    return [];
  }
  return routes.map(route => route.path).sort();
};

メモ

$el

$el は Vue インスタンスが管理しているルート DOM 要素です。$mount される前はまだ存在しないです。

const instance = new Bar({ ... });
console.log(instance.$el);
// => undefined
instance.$mount();
console.log(instance.$el);
// => <div>Bar<br>1578449862325</div>

new Bar() or new Vue() ?

Bar がプレーンなオブジェクトなのか、Vue.extend を利用して生成された Vue コンストラクタのサブクラスなのか。

Bar が Vue コンストラクタのサブクラスならばサンプル通りに new Bar() が利用できます。そうではない場合、当然ですが new Bar() ではエラーになります。new Vue({ ...Bar, /* */ }); のようになります。

生成した Vue インスタンス内で Router などが使えない

新たに作成した Vue インスタンスは、既存のルート Vue インスタンスとは別の管理になります。つまり、このままでは this.$routerthis.$store にはアクセスできません。
新たに inject することもできますが、今回のようなケースでは追加されるコンポーネントには機能をあまりもたせず、追加する側で対応する方がよさそうに思えます。