decoratorを自作する機会があり、そのときにdecorator実装例としてvue-property-decoratorを読んでました。
意外と簡単だったので、サンプル実装を交えて比較的シンプルな@Ref, @PropSync, @Watchを軽く解説していこうと思います。
目次
前提
基本コードの説明するだけですが、サンプル実装動かすには以下のライブラリが必要です。
- vue
- TypeScript
- vue-class-component
- vue-property-decorator
@Ref
変数に@Ref(<参照したいタグに指定したrefの値>)を設定することで「this.@Refで指定した変数名」で参照できるようになるというデコレータ。
this.$refs.<ref値>でも同様のことはできるが、@Refを使うと型指定ができるし良いしコードが少なく済む。
<template>
<div ref="elm">
...
</template>
// これでもアクセスできる
(this.$refs.elm as HTMLDivElement).~
@Ref("elm")
elm!: HTMLDivElement;
this.elm.~
サンプル実装
以下のサンプル実装は、ボタンを押すとdiv要素の高さを取得して表示するという内容。
divElmをref=”elm”で指定しており、@Ref(“elm”)を指定したdivElm変数を使ってアクセスすることができるようになる。
(this.$refs.divElm as HTMLDivElement)がthis.divElmでアクセスできるのでだいぶ便利。
<template>
<div>
<h2>Ref test</h2>
<h3>div高さ</h3>
<p>{{ divHeight }}</p>
<button @click="showHeight">div高さ表示</button>
<div ref="elm" style="height: 130px; background-color: red"></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Ref } from 'vue-property-decorator'
@Component
export default class RefSample extends Vue {
@Ref('elm')
divElm!: HTMLDivElement
divHeight: number = 0
showHeight() {
this.divHeight = this.divElm.getBoundingClientRect().height
// これでもできる
// this.divHeight = (this.$refs.divElm as HTMLDivElement).getBoundingClientRect().height
}
}
</script>
@Refの中身
実にシンプル。
Vueインスタンスにcomputed[@Refで指定した変数名]を追加し、getterで$refsを使った結果を返すだけ。
this.$refs.~でアクセスした結果と全く同じ。
やってることは非常に単純だがとても便利でよい。
https://github.com/kaorun343/vue-property-decorator/blob/master/src/decorators/Ref.ts
import Vue from 'vue'
import { createDecorator } from 'vue-class-component'
/**
* decorator of a ref prop
* @param refKey the ref key defined in template
*/
export function Ref(refKey?: string) {
return createDecorator((options, key) => {
options.computed = options.computed || {}
options.computed[key] = {
cache: false,
get(this: Vue) {
return this.$refs[refKey || key]
},
}
})
}
@PropSync
子コンポーネントのProp変数に@PropSync指定すると、子コンポーネント上で指定した変数が変更されたときに親の変数も変更されるというデコレータ。
サンプル
下にサンプル実装載せています。
以下の1つめの子コンポーネントで値(testValue)を変更すると、親のコンポーネントの変数(testv)も同時に変更されるという内容。
<template>
<div>
<h2>Test</h2>
<input v-model="testValue" />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { PropSync } from 'vue-property-decorator'
@Component
export default class Test extends Vue {
@PropSync('testProp')
testValue!: string
}
</script>
<template>
<div id="app">
sample
<child :test-prop.sync="testv" />
<p>{{ testv }}</p>
</div>
</template>
<script lang="ts">
import Child from './Child.vue'
import { Component, Vue } from 'vue-property-decorator'
@Component({
components: {
Child,
},
})
export default class App extends Vue {
testv: string = ''
}
</script>
@PropSyncの中身
こちらが該当コード。相変わらずシンプル。
https://github.com/kaorun343/vue-property-decorator/blob/master/src/decorators/PropSync.ts
import Vue, { PropOptions } from 'vue'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue/types/options'
import { applyMetadata } from '../helpers/metadata'
/**
* decorator of a synced prop
* @param propName the name to interface with from outside, must be different from decorated property
* @param options the options for the synced prop
* @return PropertyDecorator | void
*/
export function PropSync(
propName: string,
options: PropOptions | Constructor[] | Constructor = {},
) {
return (target: Vue, key: string) => {
applyMetadata(options, target, key)
createDecorator((componentOptions, k) => {
;(componentOptions.props || (componentOptions.props = {} as any))[
propName
] = options
;(componentOptions.computed || (componentOptions.computed = {}))[k] = {
get() {
return (this as any)[propName]
},
set(this: Vue, value) {
this.$emit(`update:${propName}`, value)
},
}
})(target, key)
}
}
getterは(this as any)[propName]でVueインスタンスから変数取得・返却しているだけ。
重要なのはsetter、propの値更新時にその値をemitすることで、Prop変更されたら親の値も更新できるという仕組み。
this.$emit(`update:${propName}`, value)
@Watch
関数に@Watch(<監視したい変数名>)を指定すると、監視したい変数が変更されたときに実行されるようになるデコレータ。
watchについては詳しくはVueの公式を参照。
https://jp.vuejs.org/v2/guide/computed.html
サンプル実装
入力値inputValueをWatchして、inputValueに***を追加した文字列をdecoratedTextに設定するという内容。
入力すると同時に***を追加した文字列が表示されるようになる。
<template>
<div style="height: 100vh">
<h2>Watch test</h2>
<input v-model="inputValue" />
<p>decorated : {{ decoratedText }}</p>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Watch } from 'vue-property-decorator'
@Component
export default class WatchSample extends Vue {
inputValue: string = ''
decoratedText: string = ''
@Watch('inputValue')
setDecoratedText() {
this.decoratedText = `*** ${this.inputValue} ***`
}
}
</script>
@Watchの中身
これもシンプル。
Vueインスタンスのwatchに、watch[監視したい変数] = Watch指定した関数とオプション を設定しているのみ。
https://github.com/kaorun343/vue-property-decorator/blob/master/src/decorators/Watch.ts
import { WatchOptions } from 'vue'
import { createDecorator } from 'vue-class-component'
/**
* decorator of a watch function
* @param path the path or the expression to observe
* @param watchOptions
*/
export function Watch(path: string, watchOptions: WatchOptions = {}) {
return createDecorator((componentOptions, handler) => {
componentOptions.watch ||= Object.create(null)
const watch: any = componentOptions.watch
if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
watch[path] = [watch[path]]
} else if (typeof watch[path] === 'undefined') {
watch[path] = []
}
watch[path].push({ handler, ...watchOptions })
})
}
カスタムデコレータを自作するには
以下の記事で解説しているのでみてみてください。