Help us understand the problem. What is going on with this article?

条件によってテキストが変わるコンポーネントを分割して共通スタイルを適用する

こちらは、弁護士ドットコム Advent Calendar 2019 - Qiita の 21 日目の記事です。

要望

条件によって中身が変わるからコンポーネントは分けたいけど、スタイルは共通化したい。
JS によるロジックは特にない。

具体的なケース

ユーザーの権限によってテキストが変わるコンポーネント。

共通のスタイルを使用したいのですが、一つのコンポーネントにまとめようとすると v-if の嵐になってしまい、可読性がかなり落ちてしまいます。

イメージ
<p v-if="condition" class="style1">◯◯権限を持っているので、☓☓が出来ます。</p>
<p v-else class="style1">◯◯権限を持っていないので、☓☓が出来ません。</p>

<p v-if="condition" class="style2">△△権限を持っているので、□□が出来ます。</p>
<p v-else class="style2">ただし△△権限を持っていないので、□□が出来ません。</p>

<p class="style3">どのユーザーも☆☆は出来ます。</p> <!-- どの権限でも同じ -->

解決策

条件ごとにコンポーネントを分け、条件分岐・共通スタイル定義を親コンポーネントで行います。

スタイルを共通化させると、v-if をコンポーネントの切り替えの 1 つだけにできるので可読性が上がります。

実装

親コンポーネントにスタイルを持たせて、条件によって子コンポーネントを切り替えるようにします。

親コンポーネント
<template>
  <div class="wrapper">
    <component :is="componentName" v-bind="propData" />
  </div>
</template>

<script>
import Component1 from './Component1.vue'
import Component2 from './Component2.vue'

export default {
  components: {
    Component1,
    Component2
  },
  props: {
    condition: {
      type: String,
      required: true
    },
    username: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      propData: {
        username: this.username
      }
    }
  },
  computed: {
    componentName() {
      switch (this.condition) {
        case 'cond1':
          return Component1
        case 'cond2':
          return Component2
        default:
          return null
      }
    }
  }
}
</script>

<style scoped>
.wrapper >>> .style1 {
  font-size: 20px;
}
.wrapper >>> .style2 {
  font-size: 16px;
}
.wrapper >>> .style3 {
  font-size: 16px;
  font-weight: bold;
}
</style>
Component1.vue
<template>
  <div>
    <h1>{{ username }}さん</h1>
    <p class="style1">閲覧権限を持っているので、プロパティの閲覧が出来ます。</p>
    <p class="style2">ただし編集権限を持っていないので、プロパティの編集が出来ません。</p>
    <p class="style3">どのユーザーもログインは出来ます。</p>
  </div>
</template>

<script>
export default {
  props: {
    username: {
      type: String,
      required: true
    }
  }
}
</script>
Component2.vue
<template>
  <div>
    <h1>{{ username }}さん</h1>
    <p class="style1">閲覧権限を持っているので、プロパティの閲覧が出来ます。</p>
    <p class="style2">編集権限を持っているので、プロパティの編集が出来ます。</p>
    <p class="style3">どのユーザーもログインは出来ます。</p>
  </div>
</template>

<script>
export default {
  props: {
    username: {
      type: String,
      required: true
    }
  }
}
</script>

注意

>>>(ディープセレクタ)は子孫要素すべてを対象とするので、scoped にしているからといって同じクラス名を使っているとスタイルがあたってしまいます。
コンポーネント名を prefix として付ける、BEM などの命名規則を適用する、などの対策が必要です。

また、SCSS 等を使用している場合は、>>> ではなく /deep/ を使用する必要があります。

他の選択肢

CSS を外部ファイル化して @import で読み込む

HTML, CSS, JavaScript が一緒に管理できる SFC の利点が消えてしまうので見送りました。
「全く違う場所で利用するコンポーネントだけどスタイルは共通化させたい」というときは Minxin 的に使えるかもしれません。

共通化しない

個々のコンポーネントとして取り扱えたほうがいいことが往々にしてあるので、選択肢としてはありだと思います。
今回は、共通に定義したスタイルを片方だけ変更するということが基本的にないことがわかっていたので、共通化したほうが後々楽だと判断しました。

あとがき

自分が実装したケースではこのやり方がフィットしましたが、子コンポーネント側のコードが二重管理になってしまうので、ケースごとに検討することが必要だと思います。

他に同じような悩みを抱えている人の糧になれば幸いです。

参考

talog
JavaScript と日々戯れているフロントエンドエンジニア。TypeScript, JavaScript, Scala あたりが好きです。編集リクエスト・コメント歓迎。
https://www.tee-talog.com/
bengo4
「専門家をもっと身近に」を理念として、人々と専門家をつなぐポータルサイト「弁護士ドットコム」「弁護士ドットコムニュース」「税理士ドットコム」を提供。
https://corporate.bengo4.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away