vue-property-decoratorを読んでみる – Ref, PropSync, Watch

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 })
  })
}

カスタムデコレータを自作するには


以下の記事で解説しているのでみてみてください。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です