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
was changed to the following.
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
- Github - vue-class-component
- Overview | Vue Class Component - Documentation
- TypeScript Support — Vue.js
- Writing Class-Based Components with Vue.js and TypeScript | DigitalOcean