はじめに
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 を追加しなければなりません。ここでは2つの方法を紹介します。
setAttribute で scopeId 属性を追加。
scopeId 属性を追加する前に、先ずは子コンポーネントの scopeId を取得しなければなりません。ここでは子コンポーネントの mounted() に emit を追加する方法で取得します。mounted() と emit について具体的にはここに参照してください。
<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 を設定しています。