Vuetifyを使ってViewを組み立てていくと、自前のコンポーネントでもテーマを設定できるようにしたくなります。
そんなときのTipsです。
Vuetifyにおけるテーマとは
Vuetifyではライト・ダークのテーマ設定をすることができます。
テーマに応じたスタイルが全体に適用されるので、フラグ一つでアプリケーション全体のテーマを切り替えることができます。
Application theming — Vuetify.js
また、Appレベルだけでなく、各コンポーネントでも個別のテーマを設定することができます。
例えば、全体ではライトテーマだけど、一部のカードだけはダークテーマにしたいということができます。
v-app(light)
...
v-card(dark)
v-card-text
// ここはダーク
テーマを設定できるコンポーネントは、自分自身に設定されていればそれを利用しますが、何も設定されていない場合は近い親のテーマが引き継がれます。
v-card(light)
// ここはライト
v-card(dark)
// ここはダーク
v-card
// ここは近い親のを引き継ぐのでダーク
この中で、自作のコンポーネントもテーマに応じてスタイルを変えるのはどのようにすればよいのでしょうか。
Vuetifyのソースを眺めてみる
まずどんな風に実現しているのかVuetifyのソースを見てみましょう。
テーマの処理は、Themeable
というミックスインが司どっています。
https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/mixins/themeable/index.ts
Themeable
ではこんなことをしてくれます。
テーマの受け取り
props
でテーマを受け取ることができます。
props: {
dark: {
type: Boolean,
default: null
} as PropValidator<boolean | null>,
light: {
type: Boolean,
default: null
} as PropValidator<boolean | null>
},
祖先のどこからかprovideされるテーマの受け取り
(いきなりちょっと難しいですが。。。)
祖先のどこからか提供されるテーマをinject
機能を使って受け取ります。
どこからも提供されない場合はデフォルトでライトテーマです。
inject: {
theme: {
default: {
isDark: false
}
}
},
現在のテーマの判定
computed
プロパティで現在のテーマを判定します。
props
で渡されるものが最優先で、指定がなければ祖先からprovide
された値を利用します。
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
というオブジェクトにセットします。
watch: {
isDark: {
handler (newVal, oldVal) {
if (newVal !== oldVal) {
this.themeableProvide.isDark = this.isDark
}
},
immediate: true
}
}
そのthemeableProvide
を子孫に向けてprovide
します。
provide (): object {
return {
theme: this.themeableProvide
}
},
provide/injectって?
いきなり出てきましたが、provide
やinject
は、Vue.jsの2.2から追加された機能です。
この 1 組のオプションは、コンポーネントの階層がどれほど深いかにかかわらず、それらが同じ親チェーン内にある限り、祖先コンポーネントが、自身の子孫コンポーネント全てに対する依存オブジェクトの注入役を務めることができるようにするために利用されます。React に精通している人は、 React のコンテキストの特徴と非常によく似ていると捉えると良いでしょう。
この機能を使って各コンポーネントは親のテーマを受け取って、自身のテーマを決定しているのですね。
自作コンポーネントで同じようなことするには?
自作のコンポーネントでも同じようなことをしたかったら、Vuetifyのコンポーネントから渡されるテーマを受け取ってしまえばよさそうですね。
つまり、VuetifyのThemeable
を利用するか、またはThemeable
相当のものを自作して、provide
されているものを受け取ってやりましょう。
ちなみにTypeScriptでvue-mixin-decorator
とvue-property-decorator
を使う場合はこんな感じで書けます。
(provide
はさぼってます )
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,
};
}
}
使うときはこちら。
<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のコントリビュート活動はじめました