ぬまのそこ

namazuのゆるいエンジニアブログ

vue.js: functional componentとscoped css

最近はFF14をしたり、某社でフロントやphpを書いたりしています。 Vueをメインに書いていますが、その中でちょっとハマった点があったのでまとめがてら記載します。

vue (2.5.17) にはvueloaderのscoped cssをfunctional componentでうまく扱えない挙動があります。 その対策をメモ的に書いておきます。

現象

vueにはインスタンスを持たない軽量なコンポーネントとしてfunctionalというコンポーネントを作ることができるようなりました。 これは画期的で、大量に使うようなbaseコンポーネントを負荷を恐れず簡単に分離し大量に用いることができます。

そこで私はいろいろと共通化できる処理を細かい単位でfunctional-componentとして分離して共通化していっているのですが、その過程でちょっと困ったことになりました。 functionalでない普通のコンポーネントからfunctionalなコンポーネントを利用するとscoped cssやクラスがうまく付きませんでした。

まとめると、

です。

コードにすれば、

<template functional>
  <span v-bind="data.attrs">{{ props.text }}</span>
</template>

<script>
export default {
  name: 'functional-component',
  props: {
    text: String
  }
}
</script>

のようなfunctilnal componentに対して

<template>
  <div>
     <functilnal-component text='hoge' class='red'/>
  </div>
<template>

<style scoped>
   .hoge {
       color: red;
   }
</style>

をしたとき文字が赤色にならないということです。

原因

  1. 静的なクラスとして与えたhogeはfunctional-componentでは通常コンポーネントで行われるクラスのマージが行われないため,functional-componentのルート要素spanに付与されません。

  2. vueのscoped cssdata-v-xxxxのような属性をrender時に全ての要素に付与し, cssを属性セレクタ付きに書き換えることで実現していますが, 上記のように利用した場合 functional-component側でv属性はマージされず, functional-component側だけのv属性がDOMに付与されるため, cssセレクタに一致せずスタイルが適用されません。

これはclassや属性をマージする一般的なhowtoである、functionalコンポーネント側でv-bind=data.attrsのようなことをしても解決しません。

対策

  1. functionalコンポーネントでは(というよりvnodeでは)静的なクラスとして付与された文字列はdata.staticClassで参照可能ですそのためv-bind:class="data.staticClass"とすることで普通に渡したクラスをfunctionalコンポーネント側の要素に付与することができます。

  2. これは厄介ですがvue-template-loaderによって実現されるscopedCssに使うscopeId (data-v-xxx)はvueインスタンス$options._scope_idで取得することができます。 つまりこれをバインドしてやればfunctional-compnent側でdata-v-xxxな値をマージすることができます。

これらを合わせると上記の問題を解決するfunctional-componentのtemplateは以下になります。

<template functional>
  <span
    v-bind="(() => { const res = data.attrs; /* scopeIdをマージする */ res[`${parent.$options._scopeId}`] = ''; return res; })()"
    :class="data.staticClass"
  >{{ props.text }}</span>
</template>

このようにv-bindとclassbindingを書いておくと, functional-componentを自然に普通のコンポーネントから利用できるようになります。

v-bindの中が黒魔術

他にはcss-moduleを利用するなどのscopedcssに頼らない根本的な解決策がありますが、私はscoped-cssがシンプルでvueらしく好きなので一旦これで進むことにしました。