Vueで親コンポーネントから設定した値をCSSに直接反映させる

# やりたいこと
**子コンポーネントのスタイルを親側からちょっとだけ変えたい！**
というのが今回の発端です。
共通で使っているコンポーネントの背景色だけをそれぞれ変えたい、でもそれ以外は全部同じでいいから別コンポーネントにするのもなあ…何か良い方法無いかなーって考えてました。

要は<ins>子コンポーネントのスタイルを親コンポーネントから操作する</ins>ということなのですが、意外と情報量が少なかったので備忘録を兼ねて記述しておきます。
方法自体は後述の通りいくつかありますが、個人的に一番気に入っている**親コンポーネントからpropsで渡した値を子コンポーネントの`<style>`へ反映させる**方法をまずは紹介したいと思います。

※この記事は`<script setup>`構文を使用しています。

# 具体例
ということで本題のコードになります。
簡易的な共通デザインのボタン用コンポーネントです。
```vue
<script setup>
defineProps({
  text: {
    type: String,
    default: ''
  },
  backgroundColor: {
    type: String,
    default: 'blue'
  },
  disabled: {
    type: Boolean,
    default: false
  }
});
</script>

<template>
  <button
    type="button"
    class="common_button"
    :disabled="disabled"
  >
    {{ text }}
  </button>
</template>

<style lang="scss" scoped>
.common_button {
  min-width: 120px;
  min-height: 40px;
  color: white;
  padding: 5px 20px;
  border-radius: 30px;
  background-color: v-bind(backgroundColor); // propsで設定された色を反映

  &:hover:not(:disabled) {
    opacity: 0.7;
    cursor: pointer;
  }

  &:disabled {
    background-color: gray;
  }
}
</style>
```
超シンプルなので親側の記述は省略。
肝になるのが`background-color: v-bind(backgroundColor);`の部分で、propsに設定した`backgroundColor`の値がstyleとして反映されます。
デフォルト値は`blue`なのでもし何も設定していなければ青色のボタンになりますね。
この`v-bind`自体はVueのディレクティブではあるのですが、`<style>`でも使えるという点が個人的に盲点でした。（何か書き方違うけど）

ということで、`v-bind`を使えば設定した変数は`<style>`内に反映できます。
つまり`computed`を使ったりすればさらに色々と動的にスタイルを設定できるということですね。
また、`v-bind()`内でシングルクオーテーションを使えば、プロパティ参照とかも可能になります。
```vue
<script setup>
import { computed } from 'vue';

const props = defineProps({
  text: {
    type: String,
    default: ''
  },
  backgroundColor: {
    type: String,
    default: ''
  },
  disabled: {
    type: Boolean,
    default: false
  }
});

// ボタン背景色設定
// 設定があればそれを優先し、無ければエクスクラメーションの有無で赤or青
const buttonBgColor = computed(() => {
  if (props.backgroundColor) { return props.backgroundColor; }

  return ['!', '！'].some((str) => { return props.text.includes(str); }) ? 'red' : 'blue';
});
</script>

<template>
  <button
    type="button"
    class="common_button"
    :disabled="disabled"
  >
    {{ text }}
  </button>
</template>

<style lang="scss" scoped>
.common_button {
  min-width: 120px;
  min-height: calc(v-bind('text.length') * 1px + 40px); // 文字列の長さによってボタンのサイズを変更
  color: white;
  padding: 5px 20px;
  border-radius: 30px;
  background-color: v-bind(buttonBgColor);

  &:hover:not(:disabled) {
    opacity: 0.7;
    cursor: pointer;
  }

  &:disabled {
    background-color: gray;
  }
}
</style>
```
サンプルなので超意味のないことをしていますが、こんな設定も可能ということです。
`computed()`で背景色を変えているのは分かりやすいと思いますが、`calc(v-bind('text.length') * 1px + 40px);`という形でCSSの`calc()`と組み合わせて計算させるのも可能です。
使用するタイミングは限定的だと思いますが、配列で表示する項目を渡して、その配列の長さだけグリッドレイアウトの`grid-row-template`を設定して～とかも出来ます。

総じて、スタイルにも直接`<script>`で設定した変数なりを参照できるのは便利だと感じました。
scssを駆使してもなかなか複雑な条件分岐を表現することは出来ないのですが、スクリプトを経由することで複雑な条件でも設定することが出来るようになります。Vueらしくスタイルとスクリプトが単一で記述されてその参照も直接的で分かりやすい形になっているのもいいですね。
そこまで頻繁に使うものでもないですが、手札の一つして持っておくと良さそうです。

# その他の方法
親コンポーネントからprops経由で子コンポーネントへ値を渡す所は同じですが、そこから先の方法自体はいくつかあります。
他にもあると思いますが、思いついたのを参考程度に以下載せておきます。
1. **`template`内の対象の要素にて、`style`属性へ参照させる**
一番直接的かつ単純ならこれでしょうか。
ただ、個人的な印象としてはまあ出来るけど…うーんって感じです。Vueには`<style>`というスタイルを記述すべき場所があるので、超特殊な事情でも無ければ`style`属性を使うのは避けたいですね。
　
2. **`template`内の対象の要素にて、`class`へ参照させる**
これもありですが、予め設定した`class`の値を親コンポーネントから渡す形になるのがネックですね。何なら設定できるの？ってなりそう。
そこはpropsの`validator`を使うとか工夫は色々あると思いますが、それでも柔軟性には欠ける形になります。
複数のCSSプロパティをまとめて分岐して設定したいケースならこの方法が良いと思うので、用途によって考える形になりますね。
　
3. **親コンポーネントからフォールバック属性として`class`を指定し、親コンポーネントのスタイルで記述する**
要は子コンポーネント側で設定するのを諦めて、親コンポーネント側で全部指定するやり方です。
もちろん方法の一つではありますが、あんまりやってしまうとそもそも別のコンポーネントに分離したほうがいいのでは？となります。
設定の自由度は高いですが、どちらかと言えばカオスを生み出しそうなので個人的には避けたいですね。
あと、`scoped`を使っているとそもそもルート要素にしか反映されないのでその点も注意です。
　
4. **親コンポーネントのスタイルで`:deep()`セレクタを使う**
これも3と同様に親コンポーネント側で全部指定するやり方です。
3と違うのは、`scoped`を使いながらでも子のルート要素よりさらに深い要素へ影響を及ぼせることですね。
問題点は3と同じなので、まああんまり使いたい方法ではないですね。

# 最後に
親コンポーネントから子コンポーネントのスタイルを操作する方法を書いてきましたが、当然いくつか方法があるので考えて使いましょうというお話でした。
個人的には上述の通り`v-bind`を使って`<style>`内に突っ込む、を第1候補にしていますが要件等で変えていってよいと思います。
まだまだVueの経験が浅いので、色々と知らないといけないですね。

# 参考
- [SFC CSS 機能 | Vue.js](https://ja.vuejs.org/api/sfc-css-features)
↑今回紹介した方法はこちらに記載されています。しっかり読まないと分からないですね...
- [ビルトインのディレクティブ | Vue.js](https://ja.vuejs.org/api/built-in-directives#v-bind)
- [クラスとスタイルのバインディング | Vue.js](https://ja.vuejs.org/guide/essentials/class-and-style)