Vue.js ✕ CSS Modules のコンポーネントのつくり方

さいきん流行りのコンポーネント化をすすめるにあたり、
弊社（ビザスク）では Vue.js ✕ CSS Modules を採用しています。

これをマークアップするのがちょっと難しかったので、
初心者向けに **マークアップのコツ** と **コンポーネントの使い方** をまとめてみました。

※ 担当外なので、script内のことや、vue.js自体の話はしません
※ なぜCSS Modulesを使っているかは以下を参照。
https://qiita.com/mascii/items/3202b9e18fd4a7366ac1

## 基本説明

以下がコンポーネントファイルの基本の構成です。

```vue:ComponentName.vue
<template>
  <div>
    <!-- ここにコンポーネントの中身のHTMLを書く -->
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'ComponentName', // ここにコンポーネントの名前を記載
});
</script>

<style lang="scss" module>
/* ここにscssを書く */
/* "lang=scss"を取り除くとcssで書ける */
</style>
```

上段の `<template>` で囲まれた部分にHTMLを書き、
中段の `<script>` で囲まれた部分にTypeScriptを書き、
下段の `<style>` で囲まれた部分にscssを書くことで、
コンポーネントが作ることができます。


## `<template>` の書き方

`<template>`の直下は **ひとかたまりにする必要があります**。

```vue:こういうのはできない
<template>
  <h1>タイトル</h1>
  <p>テキスト</p>
</template>
```

```vue:こうする
<template>
  <div>
    <h1>タイトル</h1>
    <p>テキスト</p>
  </div>
</template>
```

## CSS Modulesのクラスの指定方法

HTMLでの通常の書き方とは異なります。
複数のクラスを使いたい時などが特にややこしいので注意が必要です。

```vue:1.基本の書き方
<!-- :class と $style がミソ -->
<div :class="$style.class_name"></div>
```

```vue:2.複数のクラスを付けたい時
<!-- Arrayに入れる -->
<div :class="[$style.class1, $style.class2]"></div>
```

```vue:3.条件によってクラスをつけたりつけなかったりしたい時
<!-- Objectにする / 左側は [$style.class_name] のように [] で包む -->
<div :class="{[$style.class_name]: isHoge === true}"></div>
```

```vue:4.👆2と3を組み合わせたいとき
<!-- Arrayの中にObjectを入れる -->
<div :class="[{[$style.hoge1]: isFuga}, $style.hoge2]"></div>
```

```vue:コンポーネントにも付けられる
<ComponentName :class="$style.class_name"></ComponentName>
```

`:class` ではなく `class` を使うと通常のglobalなcssも使えます。
当然ながらglobalなcssを書き換えられた際に影響を受けます。
コンポーネント化している意味が薄れるので、よっぽどのことがなければ使わない方がいいと思います。

ちなみに `<template>`と `<slot />` (後述)には **クラスが付けられません**。
`<div>` で囲ったり直下に `<div>` を置いたりして回避してください。

## CSS Modules のscssの書き方
```vue
<style lang="scss" module>
.container {
  background: #fff;
  font-size: 16px;
}
</style>
```

先程クラスを `:class="$style.class_name"` と指定しましたが、
その内の `.class_name` をクラス名とし、
`<style>` 内で普通のscss（css）を書くとstyleが反映されます。

CSS Modulesには「外部の影響を受けない」「外部に影響しない」という特徴があるため、
同コンポーネント内で名前が被らない限りは **汎用的なクラス名を使うことができます**。

例： `.container` `.heading` `.form` `.text` など

## 他のコンポーネントの使い方

```vue
<script lang="ts">
import Vue from 'vue';
import ComponentName from '@/components/path/ComponentFileName.vue';

export default Vue.extend({
  name: 'Name',
  components: {
    ComponentName,
  }
});
</script>
```

`<script>` 内で
使いたいコンポーネントを `import` して（2行目）
その名前を`components` として指定する（7行目）と使えるようになります。

コンポーネントは `<template>` 内のお好みの箇所で、以下のように使用します。

```html
<template>
  <div>
     <ComponentName />
  </div>
</template>
```

## コンポーネントの中身を可変にする　その1

同じコンポーネントでも中の文字は変えたい、
外側は同じstyleだけど中身は変えたい、といったときに使います。

```vue:コンポーネント側
<template>
  <div :class="$style.container">
    <slot />
  </div>
</template>
```

```html:コンポーネントを使う側
<div>
  <ComponentName>
    <!-- ここに書いたものが <slot /> のところに入る -->
  </ComponentName>
</div>
```

このように書くと `<slot />` 部分が可変になり、
コンポーネントの要素で囲った部分が `<slot />`に代入されます。

## コンポーネントの中身を可変にする　その2

slotに名前をつけることで、複数の箇所を可変にできます。
コンポーネントを使うときは `<template #name>` でどのslotに代入するか指定します。

なお、用意したslotは使わなくても大丈夫です。
その場合 `<slot />` 部分に何も代入されなくなります。

```vue:コンポーネント側
<template>
  <div :class="$style.container">
    <h1 :class="$style.title">
      <slot name="title" />
    </h1>
    <div :class="$style.contents">
      <slot name="contents" />
    </div>
  </div>
</template>
```

```vue:コンポーネントを使う側
<div>
  <ComponentName>
    <template #title>タイトル</template>
    <template #contents>中身</template>
  </ComponentName>
</div>
```

## コンポーネントの中身を可変にする　その3

用意したslotを使わなかった場合、styleだけ当たった空の要素が発生することがあります。
そういうときは「slotがあるときだけこの要素を表示」みたいなロジックを添えるとよいです。

```vue:
<template>
  <div :class="$style.container">
    <h1 :class="$style.title" v-if="$slots.title">
      <slot name="title" />
    </h1>
  </div>
</template>
```

上記の例では `v-if="$slots.title"` を付け足しているので、
`<template #title>`がないときは `<h1>` 自体が表示されなくなります。

## コンポーネントの中身を可変にする　その4

`props`を使う方法もあります。
その場合は `<script>`内に追加の記述が必要です。


```vue:コンポーネント側
<template>
  <div :class="$style.container">
    {{ label }} <!-- ここが可変 -->
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'ComponentName',
  props: { // これを追記
    label: {
      type: String,
      required: true,
    },
  },
});
</script>
```

```vue:コンポーネントを使う側
<div>
  <ComponentName label="ここに書いたものが代入される" />
</div>
```

上記の例では文字列を代入させています。
コンポーネント側の記述量が増えますが、使用する側の記述量は減っています。
短い文字列だけを渡したい時などはこの方法が便利です。


## コンポーネントの中身を可変にする　その５

その4に記載した `props` で `boolean` などを渡すと、
コンポーネント内の表示を切り替えたりすることもできます。

以下の例では「ラベル付きのフォーム」をコンポーネント化しています。
コンポーネント側で作成した `required` 属性を利用することで、任意項目と必須項目の表示が切り換わるようになっています。


```vue:コンポーネント側
<template>
  <div> 
    <label>{{ label }}</label>

    <!-- 以下が切り替わる -->
    <span v-if="required" >(必須)</span>
    <span v-else>(任意)</span>

    <slot />
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'FormComponent',
  props: {
    label: { 
      type: String,
      required: true,
    },
    required: { // これを追記
      type: Boolean,
      required: true,
    },
  },
});
</script>
```

```vue:コンポーネントを使う側
<div>
  <FormComponent label="パスワード" :required="true">
    <input type="password" />
  </FormComponent>
</div>
```

※ 文字列以外を渡す際は **属性に `:` を付ける必要がある** ことに注意してください。



## おわりに

コンポーネント化が進むと同じコードを何度も記述しなくて済むようになりますが、
コンポーネント化すること自体の難易度が高く「コピペしたほうが楽だから…」と後回しにしがちです。

このぐらいまで覚えると簡単なコンポーネントであれば一人で作れるようになります。
弊社の様にマークアップをデザイナーが担当している会社もあるかと思いますが、
コンポーネントを作成・編集する際、エンジニアに依頼しなくてもデザイナー自身が行えると開発効率が上がると思います。

マークアップを担当するデザイナーやVue.js ✕ CSS Modulesの環境で開発したいエンジニアさんなど、お役に立てれば幸いです！