Vue.js: 子コンポーネントを親コンポーネントの外で使いたい

Vue.jsで、親コンポーネントに埋め込むために作った子コンポーネントを、親コンポーネントの外側で使いたいことがあります。HTML上で離れたところに置く必要があるときです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308595/97e5c630-830a-e9a9-f2a0-06a21c012187.png)

## 子コンポーネントの例：エラーメッセージ

例として、フォームの送信時に使うようなエラーメッセージのコンポーネントを作りました。

props に title と messages を指定せずに、オブジェクト errors を使っているのは、外側から操作しやすいようにするためです。

```alert.vue
<template>
  <div>
    <div class="alert alert-danger" role="alert" v-if="title || messages.length">
      <div v-if="title">{{title}}</div>
      <ul v-if="messages.length" class="mb-0">
        <li v-for="(message, idx) in messages" :key="idx">{{message}}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  props: { errors: Object },
  computed: {
    title() {
      return this.errors.title;
    },
    messages() {
      return this.errors.messages || [];
    }
  }
}
</script>
```

## 普通の使い方

まず、普通に親コンポーネントの中に子コンポーネント（エラーメッセージ）を置く使い方です。

```sample1.html
<div id="sample1"></div>
```

```app.js
import Vue from 'vue';
import Sample1 from './sample1.vue';

document.addEventListener('DOMContentLoaded', () => {
  new Vue(Sample1).$mount('#sample1');
});
```

```sample1.vue
<template>
  <div>
    <alert :errors="errors"></alert>
    <p><input type="email" v-model="email" class="form-control" /></p>
    <p><input type="button" value="送信" @click="submit" class="btn btn-primary" /></p>
  </div>
</template>

<script>
import Alert from './alert.vue';

export default {
  components: { 'alert': Alert },
  data() {
    return {
      email: '',
      errors: {
        title: null,
        messages: []
      }
    };
  },
  methods: {
    submit() {
      this.errors.title = '保存に失敗しました。';
      this.errors.messages = ['メールアドレスが不正です。'];
    }
  }
}
</script>
```

## 外に置くときの使い方

次は、子コンポーネント（エラーメッセージ）を親コンポーネントの外に置くときの使い方です。props 用のオブジェクト errors を作って2つのコンポーネントで共有します。

キモは、[Vue.observable](https://vuejs.org/v2/api/#Vue-observable) を使ってオブジェクトをリアクティブにすることです。`observable` を使わないと title や messages の変化がエラーメッセージに伝わりません。

```sample2.html
<div id="alert"></div>
<div id="sample2"></div>
```

```app.js
import Vue from 'vue';
import Alert from './alert.vue';
import Sample2 from './sample2.vue';

document.addEventListener('DOMContentLoaded', () => {
  const errors = Vue.observable({ title: '', messages: [] });

  new Vue({
    render: h => h(Alert, { props: { errors: errors } })
  }).$mount('#alert');

  new Vue({
    render: h => h(Sample2, { props: { errors: errors } })
  }).$mount('#sample2');
});
```

```sample2.vue
<template>
  <div>
    <p><input type="email" v-model="email" class="form-control" /></p>
    <p><input type="button" value="送信" @click="submit" class="btn btn-primary" /></p>
  </div>
</template>

<script>
export default {
  props: { errors: Object },
  data() {
    return {
      email: ''
    };
  },
  methods: {
    submit() {
      this.errors.title = '保存に失敗しました。';
      this.errors.messages = ['メールアドレスが不正です。'];
    }
  }
}
</script>
```

なお、Vueのバージョン3では、`observable` の代わりに `reactive` を使います。

```app.js
import { createApp, reactive, h } from 'vue';
import Alert from './alert.vue';
import Sample2 from './sample2.vue';

document.addEventListener('DOMContentLoaded', () => {
  const errors = reactive({ title: '', messages: [] });

  createApp({
    render() { return h(Alert, { errors: errors }); }
  }).mount('#alert');

  createApp({
    render() { return h(Sample2, { errors: errors }); }
  }).mount('#sample2');
});
```

## 親コンポーネントなしで使う

`observable` または `reactive` を使うと、親コンポーネントなしでエラーメッセージを使うこともできます。次は、jQueryを使った例です。

```sample3.html
<div id="alert"></div>
<div>
  <p><input type="email" v-model="email" class="form-control" /></p>
  <p><input type="button" value="送信" id="submit" class="btn btn-primary" /></p>
</div>
```

```app.js
import Vue from 'vue';
import $ from 'jquery';
import Alert from './alert.vue';

document.addEventListener('DOMContentLoaded', () => {
  const errors = Vue.observable({ title: '', messages: [] });

  new Vue({
    render: h => h(Alert, { props: { errors: errors } })
  }).$mount('#alert');

  $('#submit').on('click', (e) => {
    errors.title = '保存に失敗しました。';
    errors.messages = ['メールアドレスが不正です。'];
  });
});
```