Decorating Vue with TypeScript Decorators

Introduction

Trouble in Paradise

Taking Code to Nest Level

  • Codebase started to grow
  • Micro Services were becoming more complex
  • Express began to show its age
  • Wanted to use Nestjs
  • Experimental Feature in Typescript
  • Help to extend functionality of your code
  • Use the expression @expression
  • @expression must evaluate to a function

What are Decorators?

Types of Decorators

  • Class Decorators
  • Method Decorators
  • Accessor Decorators
  • Property Decorators
  • Parameter Decorators

Class Decorators


@Component
export default class myFormInVue extends Vue {

  // do something here...
  
} 

Method Decorators

@Component
export default class myFormInVue extends Vue {
	
   showErrorMessages: boolean = false
  
   //do something when showErrorMessages 
   @Watch('errorMessages')
    onErrorMessage(val) {
        if(val.length > 0) {
            let vm = this
            vm.showErrorMessages = true
            vm.onValidate = false
        }
 
    }

}

Accessor Decorators

class Point {
    private _x: number;
  
    @configurable(false)
    get x() { return this._x; }
  
}

Property Decorators

export default class FormData {
    
    @Required
    firstName: String

    lastName: String

    @Required
    @Password
    password: String
}

Parameter Decorators

@Component
export default class myFormInVue extends Vue {

    formData = new FormData()

    showErrorMessages: boolean = false

    onValidate: boolean = false

    @Watch('errorMessages')
    onErrorMessage(val) {
        if(val.length > 0) {
            let vm = this
            vm.showErrorMessages = true
            vm.onValidate = false
        }
 
    } 

    validateForm() {
        let vm = this
        vm.errorMessages = Validate(@Required vm.formData)
        vm.onValidate = true
    }
} 

Vue + TypeScript

Recommended Configuration

//tsconfig.json
{
  "compilerOptions": {
    // this aligns with Vue's browser support
    "target": "es5",
    // this enables stricter inference 
    // for data properties on `this`
    "strict": true,
    // if using webpack 2+ or rollup,
    // to leverage tree shaking:
    "module": "es2015",
    "moduleResolution": "node"
  }
}

Using Vue-Cli

 
#Install Vue Cli if not already installed
npm install --global @vue/cli

# Create a new project, 
# then choose the "Manually select features" option
vue create my-project-name

Vue Components in Typescript

//Vue Components using Vue.extend

import Vue from 'vue'

const Component = Vue.extend({
  // type inference enabled
})

const Component = {
  // this will NOT have type inference,
}
//Class Styled Vue Components
@Component
export default class myFormInVue extends Vue {
	//data objects are declared as class properties
    myBool: boolean = false
  
  	//computed Properties are declared as getters
  	get myComputedProperty {
      
    }
	//methods are declared as methods on a class
    myMethods(val) {
  
    } 
	
	//lifecyclehooks are declared as normal methods
	created () {
   
    }
} 

Caveats of Class Components

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

@Component
export default class MyComp extends Vue {
  
  dontKillBary = false

  // DO NOT do this
  sendMessage = () => {
    // Does not update the expected property.
    // `this` value is not a Vue instance in fact.
    // Barry will be killed
    this.dontKillBary = true
  }
  
  //to save Barry do this
  sendMessage () {
	this.dontKillBarry = true
  }
}
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class Posts extends Vue {
  numberOfTimesToShootBarry = 0

  // DO NOT do this
  // because of how Vue Components behave, gets executed twice
  // Poor Barry is Shot two times
  constructor() {
   	this.numberOfTimesToShootBarry += 1
  }
  
  //use lifecycle hooks instead
  created() {
    //now he will be shot just once
	this.numberOfTimesToShootBarry += 1
  }
}

Decorators in Vue

Enable Decorators

//tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

Vue Class Component 

  • Used to write Vue components in class-style syntax
  • Provides the @Component decorator
  • Also provides the createDecorator helper for custom decorators

Vue Property Decorator

  • Depends on Vue Class Component
  • Provides decorators to use like:                                
@Prop
@PropSync
@Model
@Watch
@Provide
@Inject
 
@ProvideReactive
@InjectReactive
@Emit
@Ref
@Component (provided by vue-class-component)
 

@Prop

class ToDoItem extends Vue {
  
  	@Prop
  	Todo;
  
	@Prop(String)
    name;

    @Prop([String, Null])
    title;

    @Prop({ default: true })
    showDetails;
}

@Emit

@Component
class ToDoItem extends Vue {
  
  	@Emit()
	addTodo() {
  		return this.newTodo;
	}
  
}

@Watch

@Component
class ToDoItem extends Vue {
  
  	@Watch('myProp')
	onMyPropChanged(val: string, oldVal: string) {
  		// ...
	}

	@Watch('myObject', { immediate: true, deep: true })
	onMyObjectChanged(val: MyObject, oldVal: MyObject) { }
  
}

Form Validation Example

//decorators.ts
import 'reflect-metadata'


const addValidationRule = function(target, propertyKey, rule) {
    let rules = Reflect.getMetadata("validation", target, propertyKey) || []
    rules.push(rule)
    
    let properties: string[] = Reflect.getMetadata("validation", target) || []
    if(properties.indexOf(propertyKey) < 0) {
        properties.push(propertyKey)
    }

    Reflect.defineMetadata("validation", properties, target)
    Reflect.defineMetadata("validation", rules, target, propertyKey)
}

//decorators.ts

...

const requiredRule = {
    evaluate (target: any, value: any, key: string): string | null {
        if(value) {
            return null
        }
        return `${key} is required`
    }
}


const Required = function(target, propertyKey) {
    addValidationRule(target, propertyKey, requiredRule)
}
//decorators.ts

...

const passwordRule = {
    evaluate(target: any, value: any, key: string) {
        let passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/

        if(passwordRegex.test(value)) {
            return null
        }

        return `${key} is not valid`
    }
}


const Password = function(target, propertyKey) {
    addValidationRule(target, propertyKey, passwordRule)
}
//decorators.ts

...

const Validate = function(target) {
    const keys = Reflect.getMetadata('validation', target) as string[]
    let errorMessages: string[] = []
    if(Array.isArray(keys)) {
        for(const key of keys) {
            const rules = Reflect.getMetadata("validation", target, key) 
            if (!Array.isArray(rules)) {
                continue;
            }
            for(const rule of rules) {
                const error = rule.evaluate(target, target[key], key)
                if(error) {
                    errorMessages.push(error)
                }
            }
        }
    }
    return errorMessages
}

export { Required, Password, Validate }
//formData.ts

import { Required, Password } from '@/decorators/decorators'

export default class FormData {
    
    @Required
    firstName: String

    lastName: String

    @Required
    @Password
    password: String
}
// Vue Form Component

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import {  Validate } from '@/decorators/decorators'
import FormData from '@/modules/formData'

@Component
export default class myFormInVue extends Vue {

    formData = new FormData()
    
    errorMessages: string [] = []

    showErrorMessages: boolean = false

    @Watch('errorMessages')
    onErrorMessage(val) {
        if(val.length > 0) {
            let vm = this
            vm.showErrorMessages = true
        }
 
    } 

    submitForm() {
        let vm = this
        vm.errorMessages = Validate(vm.formData)
    }
} 
</script>