【Nuxt.js】layoutsに設定をまとめつつ、タイトルはpagesから設定したい

これって、Qiita記事の種になりませんか？

# つまり、こういうことになります
- `/layouts`にヘッダーのコンポーネントを含めつつ
- `/pages`のファイルから`/layouts`で使用しているコンポーネントの値を設定する（値を渡す）

# 基本構成
- レイアウト内でヘッダーコンポーネントを読み込み、表示している。
- 上記レイアウトを適用してpagesを表示。
- `layout(components) > pages`の親子関係

## フォルダ
```
/components
  - Header.vue 
/layouts
  - ore.vue
/pages
  - index.vue 
```

## ファイル
```vue:components/Header.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
```

```vue:layouts/ore.vue
<template>
  <div>
    <Header />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  }
}
</script>
```

```vue:pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore'
}
</script>
```

# 方法
## 1. page側から$emitでlayout側に値を渡す。
1. dataに値を定義。
2. methodsに$emitで渡すメソッドを定義。
3. mountedのタイミングで2のメソッドを実行。

```vue:pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore',
  data(){
    return {
      header: {
        title: 'ページタイトル'
      }
    }
  },
  mounted() {
    this.updateHeader()
  },
  methods: {
    updateHeader() {
      // タイトルとして使いたい情報を渡す
      this.$nuxt.$emit('updateHeader', this.header.title)
    }
  }
}
</script>
```

# 2. layout側でイベントを受け取るようにする
- dataにタイトルの初期値を定義。
-  **created**のタイミングでイベントリスナーを設定。`$nuxt.on`で定義する。
  -  要素に`@eventName`の形式で記述する形だとmounted以後にしか設定されない。
  -  そのため、mounted直後の子要素のイベント発火を検知できなかった。
- イベントを検知した後、タイトルを書き換える処理を追加。


```vue:layouts/ore.vue
<template>
  <div>
    <Header />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  },
data() {
    return {
      title: ''
    }
  },
  created() {
    this.setListener()
  },
  methods: {
    setListener() {
      // emitで発火させたイベント名にする
      this.$nuxt.$on('updateHeader', this.setHeader)
    },
    setHeader(title) {
      // 第1引数にはemitで渡した値が入ってくる。
      // 第2引数以降を渡す場合も同様に、それ以降の引数で受け取れる
      this.title = title || ''
    }
  }
}
</script>
```

# 3.受け取った値を反映させる
- レイアウトのHeaderコンポーネントに対し、`:propsの名前="dataで定義している値"` の記述を追加する

```vue:layouts/ore.vue
<template>
  <div>
    <Header :title="title" />
    <nuxt />
  </div>
</template>

...略
```

# 最終的なファイルの状態
※コンポーネントは最初と変化なし

```vue:components/Header.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
```

```vue:layouts/ore.vue
<template>
  <div>
    <Header :title="title" />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  },
data() {
    return {
      title: ''
    }
  },
  created() {
    this.setListener()
  },
  methods: {
    setListener() {
      this.$nuxt.$on('updateHeader', this.setHeader)
    },
    setHeader(title) {
      this.title = title || ''
    }
  }
}
</script>
```

```vue:pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore',
  data(){
    return {
      header: {
        title: 'ページタイトル'
      }
    }
  },
  mounted() {
    this.updateHeader()
  },
  methods: {
    updateHeader() {
      this.$nuxt.$emit('updateHeader', this.header.title)
    }
  }
}
</script>
```

# まとめ
- `$nuxt.$emit`でイベント発火＋親に値を渡す。
- `$nuxt.$on('イベント名')`で、イベントと値を受け取る。
- イベントリスナーはcreatedのタイミングで付与しないとまともにイベントが受け取れない。
- `$nuxt`の存在を知らないと設定方法で詰む
- propsの直接の書き換えは怒られるので、dataでやる。

# さらにカスタマイズ
- $emitで複数渡す場合はlayout側のdataを増やし、コンポーネントのバインドも複数にすればOK。
- setListenerに複数のイベントリスナーを定義すれば、いろんな更新イベントを受け取れる。
- 今回はヘッダーしか更新しないから`updateHeader`だけど、複数のコンポーネントを更新するなら`updateLayout`になりそう。
  - イベント発火はコンポーネント単位で分割したほうが良い
  - 複数のイベント発火をする処理を`updateLayout`で実行するイメージ

# 参考文献
- [【Vue.js】子コンポーネントから親コンポーネントにデータを受け渡す方法](https://hafilog.com/vue-emit-listeners)
- [Vue.js / Nuxt.js を採用した Web 開発で得た知見](https://rakuishi.com/archives/vuejs-nuxtjs/)