Skip to content
Search

Vue-class-component

lastmod 2022-04-20created 2020-09-2511 edits

ES / TypeScript decorator for class-style Vue components.

Deprecated ??

Since it hasn't been updated for 2 years (as of 2022-04-20), there is talk of deprecating it. Also being discussed is something like the validity(?) of using it together with the Composition API of vue3 (<code>

<script setup>

...

</script>

</code>) ...

Build Setup

TypeScript

Create tsconfig.json on your project root and specify experimentalDecorators option so that it transpiles decorator syntax:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "strict": true,
    "experimentalDecorators": true
  }
}

Class Component

@Component decorator makes your class a Vue component:

import Vue from 'vue'
import Component from 'vue-class-component'

// HelloWorld class will be a Vue component
@Component
export default class HelloWorld extends Vue {}

Data

Initial data can be declared as class properties:

<template>
  <div>{{ message }}</div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declared as component data
  message = 'Hello World!'
}
</script>

The above component renders Hello World! in the <div> element as message is component data.

Note that if the initial value is undefined, the class property will not be reactive which means the changes for the properties will not be detected:

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will not be reactive value
  message = undefined
}

To avoid this, you can use null value or use data hook instead:

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will be reactive with `null` value
  message = null

  // See Hooks section for details about `data` hook inside class.
  data() {
    return {
      // `hello` will be reactive as it is declared via `data` hook.
      hello: undefined
    }
  }
}

Methods

Components methods can be declared directly as class prototype methods:

<template>
  <button v-on:click="hello">Click</button>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declared as component method
  hello() {
    console.log('Hello World!')
  }
}
</script>

Computed Properties

Computed properties can be declared as class property getter / setter:

<template>
  <input v-model="name">
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  firstName = 'John'
  lastName = 'Doe'

  // Declared as computed property getter
  get name() {
    return this.firstName + ' ' + this.lastName
  }

  // Declared as computed property setter
  set name(value) {
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }
}
</script>

Hooks

data, render and all Vue lifecycle hooks can be directly declared as class prototype methods as well, but you cannot invoke them on the instance itself. When declaring custom methods, you should avoid these reserved names.

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declare mounted lifecycle hook
  mounted() {
    console.log('mounted')
  }

  // Declare render function
  render() {
    return <div>Hello World!</div>
  }
}

Other Options

For all other options, pass them to the decorator function:

<template>
  <OtherComponent />
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'
import OtherComponent from './OtherComponent.vue'

@Component({
  // Specify `components` option.
  // See Vue.js docs for all available options:
  // https://vuejs.org/v2/api/#Options-Data
  components: {
    OtherComponent
  }
})
export default class HelloWorld extends Vue {}
</script>

TypeScript Example

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 데코레이터는 클래스가 Vue 컴포넌트임을 나타냅니다.
@Component({
  // 모든 컴포넌트 옵션이 이곳에 허용됩니다.
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 초기 데이터는 인스턴스 속성으로 선언할 수 있습니다.
  message: string = 'Hello!'

  // 컴포넌트 메소드는 인스턴스 메소드로 선언할 수 있습니다.
  onClick (): void {
    window.alert(this.message)
  }
}

Troubleshooting

Property not updating when changed

import {Vue, Component} from 'vue-property-decorator';

@Component
export default class  extends Vue {
  _paused = false;
  get status() {
    return this._paused ? "A" : "B";
  }
}

With code like the above, properties starting with _ turned out not to be reflected in get status()... Removing the _ fixes it.

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders

See Vue.js#Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders for details.

When this warning is printed for a Data member that is not declared as a Prop

When including a component, this error is printed if a property is duplicated in a child component.

<script lang="ts">
import {Component, Prop, Emit} from 'vue-property-decorator';
import VueBase from '@/base/VueBase';

@Component
export default class TableProjects extends VueBase {

  // ...

  loading = false;

  // ...
}
</script>

Error:

vue.esm.js?a026:628 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "loading"

found in

---> <TableProjects>
       <MainAdminProjects> at src/pages/MainAdminProjects.vue
         <MainAdmin> at src/pages/MainAdmin.vue
           <VMain>
             <VApp>
               <Main> at src/pages/Main.vue
                 <VApp>
                   <App> at src/App.vue
                     <Root>

After installing vue-devtools and checking, loading was registered as props, not data.

Debugging/tracing went as follows. Set a Breakpoint at the console.error location where the above error is printed, and gradually trace where the offending variable is assigned.

Eventually it reached the componentFactory function of vue-class-component, and the following location in that function was identified as the problem.

  // ...

  var decorators = Component.__decorators__;

  if (decorators) {
    decorators.forEach(function (fn) {
      return fn(options);
    });
    delete Component.__decorators__;
  } // find super

  // ...

In the above Component.__decorators__, all the decorators declared in other components (e.g. @Prop) were collected together, and the decorator declaration function declared in another component as @Prop() readonly loading!: boolean; was called.

The place where the above Component and Component.__decorators__ are declared was the createDecorator function of vue-class-component, implemented as follows:

export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    const Ctor = typeof target === 'function'
      ? target as DecoratedClass
      : target.constructor as DecoratedClass
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    Ctor.__decorators__.push(options => factory(options, key, index))
  }
}

Judging from the above, the problem was that the Ctor variable was shared by all components; the target on lines 3-5 should have been a VueComponent instance(needs verification) but was the class prototype.

Judging from the above, it was confirmed that the problem occurred because the VueBase class in the initial class definition was not a component.

The output of console.dir(VueBase) should be ƒ VueComponent(options), but it printed ƒ VueBase(). (For reference, console.dir(Vue) prints ƒ Vue(options).)

Finally, the code below

export default class VueBase extends Vue {
  // ...
}

was changed to the following.

@Component
export default class VueBase extends Vue {
  // ...
}

And it was solved.

TypeScript Getter malfunction error

Here is code that uses vue-router as follows.

@Component
export default class VueBase extends Vue {
    get currentRouteName(): string {
        return this.$router.currentRoute.name || '';
    }

    moveTo(name: string, params?: object) {
        if (this.currentRouteName === name) {
            return;
        }

        const rawLocation = {
            name: name,
            params: params,
        } as RawLocation;

        this.$router.push(rawLocation).catch((reason: any) => {
            if (reason.name !== 'NavigationDuplicated') {
                throw reason;
            }
        })
    }
}

The currentRouteName function above is defined as a Getter, and @Component converts it into a Computed function.

For a Computed Getter, the implementation is as follows:

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

/**
 * Depend on all deps collected by this watcher.
 */
Watcher.prototype.depend = function depend () {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
};

As analyzing the code shows, the value is cached once in watcher.value, and unless a dependent variable is modified or the value is changed through the watcher (dirty), the cached content is returned.

The problem is that the variable given as this.$router.currentRoute.name meets neither of the two conditions above. To fix this, watch this.$router.currentRoute.name with @Watch, or avoid applying computed via get.

See also

Favorite site

author Zer0 sitemap