【Vue3】styleを動的に生成したいときのやり方とアンチパターン

プログラミング等

どうもこじらです。

最近SFC CSSというものの存在を知りました。SFCは単一ファイルコンポーネント(Single File Component)の略らしいです。

Vue.jsにおけるstyle(css)は結構複雑というか、やれること多いんだなぁと思いました。

 

知識の積み重ねで脳みそバグってますが、cssの基礎を学んで、scssの仕組みを学んで、vueのライフサイクル学んで、vueでのstyleの扱い方を学んでやっと今にあるんですよね。

好奇心と継続力の掛け合わせのパワーは最強ですわ。

 

まぁそれは置いといて。

 

Vue.jsにおけるstyleを扱う際の構文の部分は色んなサイトでまとめてくれていますし、公式にもドキュメントが豊富にあります。

 

しかし、「こういう書き方ができるけど、こう書いた方が良いと思うよ」みたいな初心者を先導してくれるような、意見を言うような記事は少ないです。

ということで、今回は備忘録も兼ね、styleを動的に生成する方法とアンチパターンをまとめてみます。

 

環境

とりあえず、私の今の開発環境のバージョンです。一応Nuxt.jsとVuetify.jsも。

  • vue: 3.4.21
  • nuxt: 3.9.3
  • vuetify: 3.5.9

私がよく使う書き方

一旦、vueにおけるstyleの書き方の基礎部分を簡単にまとめます。

私がよく使う書き方のみ紹介します。

.vueファイルにおけるstyleタグ

基本中の基本ですが、.vueファイルの構造について。

<template>
  // 画面に描画させるためのHTMLタグやコンポーネント
</template>

<script setup lang="ts">
// 画面描画のための色んな処理
</script>

<style lang="scss" module>
// ここにcss、scssを書く。
</module>

 

上記のようにstyleタグにlang=”scss”を指定するとscssで書けます。書かないとcssとして認識されます。

 

SFC CSSでstyleタグの属性としてmoduleという句が追加されました。

元々scopedという句がありましたが、これからはscopedの代わりとしてmoduleを使うと良いんじゃないかなと思っています。

 

scopedは、「そのvueファイルのtemplate内にのみstyleを適用させるよ!」ってやつですが、moduleの場合もscopedと同様、IDのようなものを生成して、反映させるコンポーネントを特定していますし、moduleはscopedの機能を完全に内包しているように見えます。

そのため、scopedの出番は減るんじゃないかと思います。

 

それに、scopedを使用した場合、Vue3に移行してからstyleの優先順位の都合で思ったようにstyleが反映されない場合があります。

(開発モードのみの事象かもしれませんが、開発モードでその事象が起こる時点で結構つらい…。)

 

moduleの方がscopedよりもコーディングコストは少し高いですが、やれることが多く書き方の幅が広いのも魅力的です。まぁこの辺は後述します。

 

:style(v-bind:style)

「styleを動的に生成する方法」と聞いて一番最初に思い浮かぶのがこれでしょう。

style属性に変数をバインドさせるシンプルな方法です。

<template>
  <div>
    <span :style="style">あいうえお</span>
  </div>
</template>

<script setup lang="ts>
const style = ref<string>('background-color: red; color: blue;')
</script>

 

ちなみに、配列や連想配列として渡すこともできます。

const style = ref<Array<string>>(['background-color: red', 'color: blue']) // 配列
const style = ref<Record<string, any>>({ backGroundColor: 'red', color: 'blue' }) // 連想配列

 

:styleとcomputed

意外と盲点だったのが、:styleとcomputedの組み合わせです。

いや、ちょっと考えれば思いつくだろって言われそうですが、なぜかこの組み合わせを試したことがなかった…。

 

REST APIにリクエストを送って、そのレスポンスに応じてstyleを生成したいときとか、この組み合わせが最適になる場面は結構あると思います。

<template>
  <div>
    <span :style="style">あいうえお</span>
  </div>
</template>

<script setup lang="ts">
interface Res {
  color1: string,
  color2: string
}
const response = ref<Res>(null) // ここにレスポンスの値を格納するものとする。

const style = computed(() => {
  return {
    backgroundColor: response.value.color1,
    color: response.value.color2
  }
})
</script>

 

実装もシンプルですし、私の二の舞にならないよう頭に入れておくと良いでしょうw

:class(v-bind:class)

class属性もv-bindが使用できます。

styleがパターン化でき、かつそのパターンをcss(またはscss)として表した場合に量が膨大にならない場合に使えます。

<template>
  <div>
    <span :class="reverseFlg ? 'text' : 'text_reverse'">あいうえお</span>
    <v-btn @click="reverseFlg = !reverseFlg">ぼたん</v-btn> // VuetifyのVBtnコンポーネント
  </div>
</template>

<script setup lang="ts">
const reverseFlg = ref<boolean>(false)
</script>

<style>
.text {
  background-color: white;
  color: black;
}
.text_reverse {
  background-color: black;
  color: white;
}
</style>

:classはシンプルに見えて奥が深く、奥が深いように見えてシンプルです。

基本的には、style属性で記載するよりもclass属性で記載した方がvueファイルをきれいに整理できるのでv-bindの有無に関わらず積極的に使うべきです。

 

styleタグ内にv-bind

CSS FCSの真骨頂とも言える機能がこれです。

styleタグ内にv-bindを使用してCSSを埋め込むことができます!

これは個人的には革命

 

<template>
  <div>
    <span :class="text">あいうえお</span>
  </div>
</template>

<script setup lang="ts">
interface Res {
  color1: string,
  color2: string,
  thickness: string
}
const response = ref<Res>(null) // ここにレスポンスの値を格納するものとする。

const bgColor = computed(() => response.value.color1)
</script>

<style>
.text {
  background-color: v-bind(bgColor); // script内で定義したものはそのまま使える。
  color: v-bind('response.color2'); // javascriptの式になる場合は引用符で囲む。
  border: v-bind('response.thickness')' solid black'; // 部分的にv-bindすることはできない!!
}
</style>

 

上記のborderのように、一部のみをv-bindで指定することはできないので、そこは注意が必要です。

この書き方の何が良いかって、

  • 画面のオブジェクトが見たかったらtemplateね!
  • 処理が見たかったらscriptね!
  • 画面の装飾が見たかったらstyleね!

っていう.vueファイルの非常に整理された良い部分を際立たせている点です。

特に:styleを使用した際は、画面の装飾にかかわる部分であるにも関わらず、styleタグ内には一切登場してきません。そうなると可読性が悪くなりやすいです。

 

まぁ、viteが重くなって開発モードの作業がもっさりするっていうデメリットはあるんですが、ソースのきれいさを考えたら無視できるレベルかなと思います。

 

複雑なclassを動的に生成したい場合はどうしたら良い?

上記で「私が良く使う書き方」としていくつか紹介しましたが、「それじゃやりづらい処理があるんだよ!」という声があるかもしれません。

(…あるかもしれないし、ないかもしれません。)

 

この章はそういった人に向けた章です。

アンチパターン

まず、私が過去やっていたやり方で、これはアンチパターンだなと理解したやり方があります。

それは、JavaScriptで直接DOMを操作して、classを追加するやり方です。

 

以下のような関数を作成し、created時(SSRの場合はクライアント側のみ)に呼び出します。styleにはidを付けておきます。そして、画面を離れるとき(beforeDestroy, beforeUnmount)にidを使用してstyleを削除します。

// アンチパターン!!流用禁止!
// DOMを直接操作しstyleを追加する関数
const createStyleElem = (id: string, style: string) => {
  const newStyle = document.createElement('style')
  newStyle.id = id
  newStyle.innerHTML = style
  document.getElementsByTagName('head').item(0)?.appendChild(newStyle)
}

このやり方は考え付きやすいですが、明確に言えます。アンチパターンです。

処理が追いにくい上に、仮にstyleを削除し忘れた場合、SPAの特性上styleは残り続けてしまいます。

 

うんちです。

 

正しい方法

上記のようなアンチパターンのやり方を取らずとも、styleのうちの静的な部分、動的な部分を切り分ければ、「私が良く使う書き方」で挙げたやり方で記述可能だと思います。

 

複雑なclassが必要になる場面として、参加者に制限を設けていないトーナメント表を描画するコンポーネントなんかは良い例ですかね。

“試合”を1コンポーネントでその集合をトーナメント表と捉えて作成した場合、トーナメント表の横線は”試合”ごとに長さを調整する必要があります。

この場合、参加者に制限を設けていないため、トーナメント表のサイズは2^nで可変になります。しかも、nは限りなく続く可能性があるため、classを静的に定義するのは不可能です。

 

ただこういった場合であっても、静的な部分は、.vueファイルのstyleタグ内に、動的な部分は、:styleとcomputedの組み合わせで生成すれば実装できます。多分、このすみ分けが最善です。

 

class属性には何個もclassを指定できますし、この辺の構造をより実態に即した形にしていけば、案外状況が整理できます。

 

↓classを複数個指定したときの例

<template>
  <div>
    <span class="bg c">あいうえお</span>
  </div>
</div>

<style>
.bg {
  background-color: red;
}
.c {
  color: blue;
}

 

moduleの書き方

最後にmoduleの書き方です。

 

moduleの基本ルールはシンプルです。

  • templateタグ内からmodule付きのstyleを参照するときは、$styleから参照する。(module=”classes”みたいにmoduleに名前を付けた場合は、classes.textになる。)
  • scriptタグ内からmodule付きのstyleを参照するときは、useCssModule()で取得する。(module=”classes”みたいにmoduleに名前を付けた場合は、useCssModule(‘classes’)で取得する。)

 

以下にサンプルを挙げます。テキストにマウスカーソルを当てた場合に文字を太くします。

<template>
  <div>
    <span
      :class="$style.text"
      @mouseenter="onMouseEnter($event)"
      @mouseleave="onMouseLeave($event)">
      あいうえお
    </span>
  </div>
</template>

<script setup lang="ts">
const styles = useCssModule()
/** マウスカーソルを乗せた際のイベント */
const onMouseEnter = (event: MouseEvent): void => {
  const elem: Element = event.target as Element
  elem.className += ' ' + styles.text_enter
}
/** マウスカーソルを外した際のイベント */
const onMouseLeave = (event: MouseEvent): void => {
  const elem: Element = event.target as Element
  elem.className = styles.text
}

</script>

<style lang="scss" module>
.text {
  background-color: red;
  color: blue;

  &-enter {
    font-weight: bold;
  }
}
</style>

上記の例ではDOMを直接更新してますが、これはグレーなんですかね?(いや、流石に問題ないか)

 

useCssModule()で取得した値はそのままclassとして扱えることを理解すると幅が広がります。問題が起こったときの解決もスムーズになるかと思います。

 

こんな感じで、SFC CSSではほかにも色々機能があるみたいですが、moduleとstyleタグ内のv-bindがメインどころです。

module=”classes”はアンチパターンかも?

個人的見解ですが、module=”classes”の使用はアンチパターンというか、この書き方をしている時点でコンポーネントの切り分けがあまりできてないと思った方が良いんじゃないかなと思います。

まぁ、この書き方が最適解になる場面もあるんでしょうけど、使用を迫られた時点で一旦疑った方が良いのかなと。

 

少し書いて満足したので今回はこの辺で。また学びがあったら記事にします。

 

こじらでした

じゃ

 

参考

Vue.js
Vue.js - The Progressive JavaScript Framework
Vue.js
Vue.js - The Progressive JavaScript Framework
タイトルとURLをコピーしました