Vue v-slot での select が無効化されている問題解決

#はじめに
Vue.js でのフロントのスプレッドシート画面を開発する際に、cell 内の select が無効化されていることが大きな壁になりました。その課題は Vue.js での親コンポーネントの v-slot 部分に子コンポーネントの scopeId が継承できないという共通の課題であり、本文ではその課題の原因の考察と解決法を共有します。

#Vue画面の仕組み

```
<template>
  <app-layout>
      <v-spread
          v-model="dataset"
          :fields="fields"
          nameKey="apiName"
          :cellClass="cellClass"
          :cellReadonly="cellReadonly"
          ref="vspread"
          @mounted="onload"
      >
        <template v-slot:input="{field, item}">
          <div>
            <select v-model="item[field.apiName]">
              <option disabled value="">選択して下さい</option>
              <option v-for="option in options" :value="option.id">
                {{ option.name }}
              </option>
            </select>
          </div>
        </template>
      </v-spread>
  </app-layout>
</template>

<script type="text/javascript" defer=true>
import AppLayout from '@/Layouts/AppLayout'
import VSpread from '@/Shared/VueSpread'

export default {
  components: {
    AppLayout,
    VSpread
  },
}
</script>

<style scoped>

</style>
```

以上はメイン画面の一部です。メイン画面では VSpread という子コンポーネントをインポートしています、template v-slot を利用し、子コンポーネントの cell 内に select を適用している仕組みになります。以上のコードで、メイン画面でスプレッドシートの cell 内選択ボックスが機能していると期待していますが、実際に選択ボックスが無効化されていることが判明しました。なぜそのようなことが起きましたのか、次は HTML element について考察してみます。

#HTML elementの考察

```
<form id="form" data-v-40046a6d="">
    <div data-v-b1b3ea5c="">
        <select class="select" data-v-b1b3ea5c="">
            <option disabled="" value="" data-v-b1b3ea5c="">選択して下さい</option>
            <option value="1" data-v-b1b3ea5c="">1</option>
            <option value="2" data-v-b1b3ea5c="">2</option>
            <option value="3" data-v-b1b3ea5c="">3</option>
        </select>
    </div>
</form>
```
以上は HTML element の一部です。form は子コンポーネントに所属する element、select は親コンポーネントに所属する element 、以上のコードで、select element のscopeId と form element の scopeId が違うことが判明しました。その原因で select element が子コンポーネント VSpread に定義されている CSS style が適用されてなく、メイン画面で操作できなくなることが分かりました。では、なぜ template v-slot の部分に子コンポーネントの scopeId を継承できないだろうか。それを解明するために、 Vue の Scope CSS のメカニズムについて解説します。

#vue-loader について

全ての.vue ファイルに対して、最初は vue-loader によって処理されます。スコープ CSS に対して、簡単にまとめると、vue-loader は次の3つのことを行います。

* コンポーネントを分析し、template、script、style に対応しているコードを抽出します。
* コンポーネントのインスタンスを構築し、インスタンスに scopeId をバインドします。
* scopeId を使用してセレクターの属性に設定し、style の CSS コードをコンパイルします。

vue-loader から scopeId の生成する方法が分かったことで、scopeId の継承とどんな関係があるだろうか？

#scopeId の継承順

みんな分かっていることと思いますが、Vue の Scope CSS に対して、子コンポーネントの root element は必ず親コンポーネントから scopeId を継承するという仕組みがあります。この点から、上記のサンプルに戻ると、.vue ファイルはメイン画面の.vue ファイル -> 子コンポーネントの VueSpread.vue という順番で解析されることが分かりました。
つまり、vue-loader がメイン画面の.vue ファイルをコンパイルする時点では、子コンポーネントはまだ解析されていない、子コンポーネントの scopeId はまだ生成されていない状態なので、メイン画面での template v-slot　に子コンポーネントの scopeId を渡すには不可能なことである。
自動的に scopeId を追加できないことが判明したことで、問題解決するためには手動で select に scopeId を追加しなければなりません。ここでは２つの方法を紹介します。

# setAttribute で scopeId 属性を追加。

scopeId 属性を追加する前に、先ずは子コンポーネントの scopeId を取得しなければなりません。ここでは子コンポーネントの mounted() に emit を追加する方法で取得します。mounted() と emit について具体的には[ここに](https://v3.ja.vuejs.org/guide/composition-api-lifecycle-hooks.html)参照してください。

```
<template>
  <app-layout>
      <v-spread
          v-model="dataset"
          :fields="fields"
          nameKey="apiName"
          :cellClass="cellClass"
          :cellReadonly="cellReadonly"
          ref="vspread"
          @mounted="onload"
      >
          <select ref="myselect" v-model="item[field.apiName]"/>
      
      </v-spread>
  </app-layout>
</template>

<script type="text/javascript" defer=true>
import AppLayout from '@/Layouts/AppLayout'
import VSpread from '@/Shared/VueSpread'

export default {
  components: {
    AppLayout,
    VSpread
  },
  data: () => ({
    scopeID: '',
  }),
  methods: {
    onload() {
      let vspread = this.$refs.vspread;
      let app = vspread.$refs.app
      this.scopeID = app.attributes[1].name
      let select = this.$refs.myselect
      select.setAttribute(this.ScopeID,'')
    },
  }
}
</script>

<style scoped>

</style>
```

以上のような子コンポーネントが mounted した時点でトリガーを送信し、onload() メソッドで attributes[1].name で子コンポーネントの scopeID を取得し、 select.setAttribute で select に子コンポーネントの scopeID を追加します。

#動的属性で scopeID を追加

Vue のライフサイクルの関係で、子コンポーネントの  mounted 完成した時点でも親コンポーネントでは、特定な DOM element はまだ作成されていない可能性があるので、その場合 setAttribute が使えなくなり、その時に対応できる方法としては動的属性で scopeID を追加することです。

```
<template>
  <app-layout>
      <v-spread
          v-model="dataset"
          :fields="fields"
          nameKey="apiName"
          :cellClass="cellClass"
          :cellReadonly="cellReadonly"
          ref="vspread"
          @mounted="onload"
      >
        <template v-slot:input="{field, item}">
          <div>
            <select v-bind:[scopeID]="scopeID" v-model="item[field.apiName]">
              <option disabled value="">選択して下さい</option>
              <option v-for="option in options" :value="option.id">
                {{ option.name }}
              </option>
            </select>
          </div>
        </template>
      </v-spread>
  </app-layout>
</template>

<script type="text/javascript" defer=true>
import AppLayout from '@/Layouts/AppLayout'
import VSpread from '@/Shared/VueSpread'

export default {
  components: {
    AppLayout,
    VSpread
  },
  data: () => ({
    scopeID: '',
  }),
  methods: {
    onload() {
      let vspread = this.$refs.vspread;
      let app = vspread.$refs.app
      this.scopeID = app.attributes[1].name
    },
  }
}
</script>

<style scoped>

</style>
```

上と同じような方法で子コンポーネントの scopeID を取得しています、こちらは select に v-bind:[scopeID]="scopeID" で動的属性 scopeID を設定しています。