torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

Vueで直接DOM生成してクラス割り当てようとしたらできなかった話

こんにちは、とりかつです。 今回は、VueでDOMを生成してクラスを割り当てようとしたら、スタイルが適用されなくてハマりました。 そこで、ハマリから得られた知見を共有したいと思います。

やろうとしていたこと

SFCで以下のようなことをしようとしていました。 付与するクラスinner_elementのスタイルは、SFC内の<css scoped>で定義してあります。

  1. mounted()のタイミングで動的に<span></span>を生成 1 生成した要素にnewElementというクラスを付与
  2. $refsの子要素として追加

コードにすると以下のようになります。

mounted() {
  const el = this.$refs.hoge as HTMLElement
  const newElement = document.createElement('span')
  newElement.className = "inner_element"
  el.append(newElement)
}

いざ実行してみると<span>はしっかり追加されており、class="inner_elementも確認することができました。 しかし、スタイルは適用される気配がありません。 うまくいかない原因はなんでしょうか。

なんでVueでVanilaJS使ってDOMを追加しようとしとるねんってことはあえて考えないようにしましょう。

VueのCSSが読み込まれるタイミング

スタイルが適用されなくて悩んでる時にscoped cssの仕様についてふと思い出しました。

scoped 属性をもつ <style> タグを利用するとき、その CSS は現在のコンポーネントの要素にのみ適用されます。これは Shadow DOM のスタイルのカプセル化に似ています。いくつか注意点がありますが、polyfill は必要ありません。PostCSS を使用して以下を変換することによって実現しています[^1]

同ページには以下のようなスニペットも掲載されていました。(一部抜粋)

example[data-v-f3f3eg9] {
  color: red;
}

要約すると、SFCのスコープ付きCSSのクラス名はコンパイルされて、.inner_element.inner_element[data-v-jfoieh]のようになるといった感じです。

では、CSSコンパイルされるタイミングはいつでしょうか。 以下はVue.js公式ドキュメントに掲載されていたライフサイクルの図です。[^2]

f:id:torikatsu923:20201127210526p:plain
Vueライフサイクル

この図からはSFCテンプレートがコンパイルされるタイミングがcreated()beforeMount()の間であることが読み取れます。 ですが、CSSコンパイルされるタイミングは読み取ることができませんでした。 推測ではありますが、この間でCSSコンパイルされている可能性がありそうです。

ここで気づいたのですが、CSSコンパイルされるのと同時にコンパイルによって付与されたID的なものがVueによって生成されるテンプレート内の要素にも付与されるようです。 以下のようなSFCを考えます。

<templete>
  <button class="button">
</templete>
<style scoped>
  .button {
    /* some style */
  }
</style>

Vueによってマウントされたとき、.button.button[data-v-horekjkd]となるとします。 このとき、Vueによって生成されるbuttonは次にようになります。

<...>
  <button data-v-horekjkd class="button"></button>
</...>

このようにVueによって生成されたIDはHTMLのタグに反映?されるようです。

実際にインスペクタを見てみるとv-data-.....buttonに付与されていることがわかります。

f:id:torikatsu923:20201127203001p:plain
buttonタグ内にdata-v-24921c38が存在していることが確認できます。


少し冗長になってしまいました。 ここまでわかったことをまとめます。

  • スコープ付きCSSはVueによってコンパイルされ、一意なIDが付与される
  • IDはテンプレート内のタグにも付与される
  • CSSコンパイルされるタイミングははっきりとわからない
  • テンプレートはcreated()beforeMounted()の間でコンパイルされ、このタイミングでIDが付与される

CSSコンパイルされるタイミングをはっきりと掴むことができませんでした。 しかし、CSSコンパイルで付与されるIDがテンプレート内のタグに付与されることと、テンプレートがコンパイルされるタイミングは明らかになりました。 これによりmounted()で生成した要素にスタイルが付与されなかったことの説明がつきそうです。

CSSコンパイルされるタイミングは追々調べようと思います。

まとめ

当初、私はmounted()<span class="inner_element"></span>を生成していました。 しかし、この時点でSFCCSSクラス名はコンパイルされ.inner_element[v-data-hgoejdklgha]のように変化していました。 また、テンプレートのコンパイルも終了していました。 スタイルを適用するには、クラスに付与されたIDをテンプレートの要素にも付与する必要がありましたが、私の生成したspanはIDが付与されていませんでした。 これによってスタイルが適用されなかったと考えられます。

おわりに

ここまで、Vueのmounted()でVanilaJSによって生成した要素にスタイルが適用されない原因について考えてきました。

思い返してみると... Vueでクラス名をバインドする際は、

:class="{classA: true, classB: false}"

みたいに書く必要がありました。 いままで、以下のように書けたらいいのにな。なんだか気持ち悪いなとか思ってました。

$refs.class = "classC"

ですが、テンプレートがコンパイルされるタイミングでIDが付与されることを考慮すると、 あらかじめクラス名をバインドしておいて、vueのdata等の状態の切り替えで、クラスの付与のアクティブ/パッシブを切り替えに納得が行きました。


[^1] https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html [^2] https://jp.vuejs.org/v2/guide/instance.html