【Vuetify】テーマ設定に応じてスタイルを変える

Vuetifyを使ってViewを組み立てていくと、自前のコンポーネントでもテーマを設定できるようにしたくなります。
そんなときのTipsです。

## Vuetifyにおけるテーマとは

Vuetifyではライト・ダークのテーマ設定をすることができます。
テーマに応じたスタイルが全体に適用されるので、フラグ一つでアプリケーション全体のテーマを切り替えることができます。
[Application theming — Vuetify.js](https://vuetifyjs.com/ja/framework/theme)

また、Appレベルだけでなく、各コンポーネントでも個別のテーマを設定することができます。
例えば、全体ではライトテーマだけど、一部のカードだけはダークテーマにしたいということができます。

```pug
v-app(light)
  ...
    v-card(dark)
      v-card-text 
        // ここはダーク
```

テーマを設定できるコンポーネントは、自分自身に設定されていればそれを利用しますが、何も設定されていない場合は近い親のテーマが引き継がれます。

```pug
v-card(light)
  // ここはライト
  v-card(dark)
    // ここはダーク
    v-card
      // ここは近い親のを引き継ぐのでダーク
```

この中で、自作のコンポーネントもテーマに応じてスタイルを変えるのはどのようにすればよいのでしょうか。:thinking: 

## Vuetifyのソースを眺めてみる

まずどんな風に実現しているのかVuetifyのソースを見てみましょう。
テーマの処理は、`Themeable`というミックスインが司どっています。
https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/mixins/themeable/index.ts

`Themeable`ではこんなことをしてくれます。

### テーマの受け取り

`props`でテーマを受け取ることができます。

```ts
  props: {
    dark: {
      type: Boolean,
      default: null
    } as PropValidator<boolean | null>,
    light: {
      type: Boolean,
      default: null
    } as PropValidator<boolean | null>
  },
```


### 祖先のどこからかprovideされるテーマの受け取り

(いきなりちょっと難しいですが。。。)
祖先のどこからか提供されるテーマを`inject`機能を使って受け取ります。
どこからも提供されない場合はデフォルトでライトテーマです。

```ts
  inject: {
    theme: {
      default: {
        isDark: false
      }
    }
  },
```

### 現在のテーマの判定

`computed`プロパティで現在のテーマを判定します。
`props`で渡されるものが最優先で、指定がなければ祖先から`provide`された値を利用します。

```ts
  computed: {
    isDark (): boolean {
      if (this.dark === true) {
        // explicitly dark
        return true
      } else if (this.light === true) {
        // explicitly light
        return false
      } else {
        // inherit from parent, or default false if there is none
        return this.theme.isDark
      }
    },
    // ...
```

### 判定したテーマを子孫に提供

上記の`isDark`を`watch`していて、ミックスインの内部に持っている`themeableProvide`というオブジェクトにセットします。

```ts
  watch: {
    isDark: {
      handler (newVal, oldVal) {
        if (newVal !== oldVal) {
          this.themeableProvide.isDark = this.isDark
        }
      },
      immediate: true
    }
  }
```

その`themeableProvide`を子孫に向けて`provide`します。

```ts
 provide (): object {
    return {
      theme: this.themeableProvide
    }
  },
```

## provide/injectって？

いきなり出てきましたが、`provide`や`inject`は、Vue.jsの2.2から追加された機能です。

> この 1 組のオプションは、コンポーネントの階層がどれほど深いかにかかわらず、それらが同じ親チェーン内にある限り、祖先コンポーネントが、自身の子孫コンポーネント全てに対する依存オブジェクトの注入役を務めることができるようにするために利用されます。React に精通している人は、 React のコンテキストの特徴と非常によく似ていると捉えると良いでしょう。

https://jp.vuejs.org/v2/api/index.html#provide-inject


この機能を使って各コンポーネントは親のテーマを受け取って、自身のテーマを決定しているのですね。

## 自作コンポーネントで同じようなことするには？

自作のコンポーネントでも同じようなことをしたかったら、Vuetifyのコンポーネントから渡されるテーマを受け取ってしまえばよさそうですね。
つまり、Vuetifyの`Themeable`を利用するか、または`Themeable`相当のものを自作して、`provide`されているものを受け取ってやりましょう。

ちなみにTypeScriptで`vue-mixin-decorator`と`vue-property-decorator`を使う場合はこんな感じで書けます。
(`provide`はさぼってます :joy:)


```ts
import Vue from 'vue';
import { Mixin } from 'vue-mixin-decorator';
import { Prop, Inject } from 'vue-property-decorator';

interface Theme {
    isDark: boolean;
}

@Mixin
export default class Themeable extends Vue {
    @Inject({ from: 'theme', default: { isDark: false } }) theme: Theme;
    @Prop() dark: string;
    @Prop() light: string;

    get isDark() {
        if (this.dark) {
            return true;
        }
        if (this.light) {
            return false;
        }
        return this.theme.isDark;
    }

    get themeClasses() {
        return {
            'theme--dark': this.isDark,
            'theme--light': !this.isDark,
        };
    }
}
```

使うときはこちら。

```vue
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import { Prop } from 'vue-property-decorator';
import { Mixins } from 'vue-mixin-decorator';

import Themeable from '../../mixins/Themeable';

@Component
export default class extends Mixins<Themeable>(Themeable) {
  // 略
}
</script>

<template lang="pug">
div.hogehoge(:class="themeClasses")
  // 略
</template>

<style lang="stylus" scoped>
.hogehoge
  // ライトテーマのスタイル
  color: #000

  &.theme--dark
    // ダークテーマのスタイル
    color: #fff;
</style>
```

それでは良きVuetifyライフを！！！

ps.
Vuetifyのコントリビュート活動はじめました :sunglasses: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97227/806bff4a-d35b-951f-8a36-43004d18374b.png)