HerringtonDarkholme / av-ts

A modern, type-safe, idiomatic Vue binding library
MIT License
216 stars 11 forks source link

Awesome Vue TS Build Status


screenshot

Try av-ts in your browser!

Why:

Awesome Vue.TS aims at getting type safety as much as possible, while still keeping TypeScript concise and idiomatic. To achieve this, av-ts exploits many techniques, tricks and hacks in TypeScript, which makes av-ts a good tour of TypeScript features.

Note: The target vue version is 2.0.

You can read more for av-ts' raison d'etre here

Quick start

  1. Try av-ts in your browser!

  2. use template by vue-cli

npm install vue-cli -g
vue init HerringtonDarkholme/av-ts-template myproject
cd myproject
npm install
npm run dev

Usage:

  1. component is declared via a decorated class. extends should work. (mixin support is added in 0.3.0!)

  2. data, methods and computed can be declared by property initializer, member methods and property accessors in class body, respectively.

  3. props are declared via @Prop decorator and property initializer.

  4. render and life cycle handler like created is declared by decorated methods. They are declared in class body because these handlers use this. But you cannot invoke them on the instance itself. So they are decorated to remind users. When declaring custom methods, you should avoid these reserved names.

  5. watch handlers are declared by @Watch(propName) decorator, handler type is checked by keyof lookup type.

  6. All other options are considered as component's meta info. So users should declare them in the @Component decorator function.

  7. You can also extend av-ts by Component.registering new decorators. Useful for libraries like Vuex, Vue-router.

Example:

import {
  Component, Prop, Watch, Lifecycle, p
} from 'av-ts'

// vue options in `Component` decorator
@Component({
  filters: {},
  name: 'my-component',
  delimiter: ['{{', '}}'],
})
export class MyComponent extends Vue { // extends Vue or your own component
  // instance variable is in `data`
  myData = '123'

  // props declaration
  @Prop myProp = p({
    type: Object,
    required: true,
    default() {
      return {a: 123, b: 456}
    }
  })

  // method is `method`
  myMethod() {
    console.log('myMethod called!')
  }

  // accessor is `computed`
  get myGetter() {
    return this.myProp
  }

  // watch handler is declared by decorator
  myWatchee = 'watch me!'
  @Watch('myWatchee')
  handler(newVal, oldVal) {
    console.log(this.myWatchee + 'changed!')
  }

  // lifecycle hook is speical so it is decorated
  @Lifecycle beforeCreate() {}
}

which is equivalent to


let MyComponent = Vue.extend({
  filters: {},
  name: 'my-component',
  delimiter: ['{{', '}}'],
  data() {
    return {
      myData: '123',
      myWatchee: 'watch me!'
    }
  },
  props: {
    myProp: {
      type: Object,
      required: true,
      default() {
        return {a: 123, b: 456}
      }
    }
  },
  methods: {
    myMethod() {
      console.log('my method called!')
    }
  },
  computed: {
    myGetter: {
      get() {
        return this.myProp
      }
    }
  },
  watch: {
    myWatchee() {
      console.log(this.myWatchee + 'changed!')
    }
  },
  beforeCreate() {}
})

mixin examples

Contrary to other libraries, av-ts supports first class Mixin! Example adapted from here

// define mixin trait by `Trait` decorator
@Trait class VegetableSearchable extends Vue {
  vegetableName = 'tomato'
  searchVegetable() { alert('find vegi!')}
}

@Trait class FruitSearchable extends Vue {
  vegetableName = 'apple'
  searchVegetable() { alert('find fruits!')}
}

// Mixin them!
@Component
class App extends Mixin(VegetableSearchable, FruitSearchable) {}

Voila! No implements, No repeating code. And it looks like real mixins in ES6.

N.B.: Requires TypeScript 2.2.

TSX example

You need to first understand how TypeScript checkes JSX. https://www.typescriptlang.org/docs/handbook/jsx.html You also need to know the difference between VueJSX and React JSX. https://github.com/vuejs/babel-plugin-transform-vue-jsx


import Foo from './foo.vue'

@Component
class Bar extends Vue {
  // $props is JSX.ElementAttributesProperty
  $props: {
    name: string
  }
  defaultName = 'John'
  render() {
    return (<Foo><Bar name={this.defaultName}>name attribute is required</Bar></Foo>)
  }
}

API

For full type signature, please refer to av-ts.d.ts. They are most up-to-date.

Class Decorators


Component


Type: ClassDecorator | (option) => ClassDecorator

It can be directly applied on component class as decorator, or take one option argument and return a decorator function.

@Component
class VueComp extends Vue {}

@Component({
  directives: {},
  components: {},
  filters: {},
  name: 'my-awesome-component',
  delimiters: ['{{', '}}'],
})
class MyComponent extends Vue {}

Trait


Type: ClassDecorator | (option) => ClassDecorator

An alias of Component,used for defining Vue traits to be mixed in. At runtime, these decorators transform constructor to vue option and then feed to Vue.extend. So there is no semantic difference between Component and Trait. Placing Component on a class to be used as mixin just feels too strange. This alias is solely for API aesthetic.

To use a Trait, declare a class that extends Mixin(...Traits). See example in Mixin section.

Property Decorators


Prop


Type: PropertyDecorator

Decorated properties should be the return value of utility function p. p is a function takes property option and return a fake type placeholder that will specify the property type. The fake type placeholder, at runtime, is just the config option object you feed to the argument.

@Prop
myProp = p({
  type: Number,
  default: 123
})

// p(option) returns a `number` type placeholder
// so the following code compiles
var num: number = p({
  type: Number
})

// will print {type: Number}
console.log(num)

// you can also use a shorthand form of `p`
@Prop shortHand = p(String)

Watch


Same as vue-typescript, @Watch is applied to a watched handler. Watch takes watchee name, or an array of key-path to a nested property, as the first argument, and an optional config object as the second one.

// watch handler is declared by decorator
properyBeingWatched = 123
@Watch('properyBeingWatched', {deep: true})
handler(newVal, oldVal) {
  console.log('the delta is ' + (newVal - oldVal))
}

// the key path length is 4 at most
@Watch(['nested', 'path', 'property'])
handler(newVal, oldVal) {
  console.log('the delta is ' + (newVal - oldVal))
}
// ....

is equivalent to

watch: {
  properyBeingWatched: {
    handler: function(newVal, oldVal) {
      console.log('the delta is ' + (newVal - oldVal))
    },
    deep: true
  }
}

Lifecycle and Render


Type: TypedPropertyDecorator

mark decorated methods as special hooks in vue. Caveat: You cannot call lifecycle/render in other methods.

// lifecycle hook is speical so it is decorated
@Lifecycle mounted() {
  console.log('called in lifecycle code!')
}

// this decorator can only decorate method with name same as lifecycle
// @Lifecycle willNotCompile() {}

Lifecycle from vue-router is also supported as

import {
  Component, Lifecycle, NextFunc, NextFuncVm, p, Prop
} from 'av-ts'
import { Route } from 'vue-router'

@Component({
  name: 'my-page',
})
export default class MyPage extends Vue {
  @Prop id = p({ type: String, required: true })

  // "beforeRouteEnter" can use "NextFuncVm<T>"
  @Lifecycle
  async beforeRouteEnter(to: Route, from: Route, next: NextFuncVm<MyPage>) {
    if (normalCase) {
      next()
    } else if (redirection) {
      next({ name: 'other-page' })
    } else if (cancel) {
      next(false)
    } else if (needCallback) {
      next((vm) => {
        console.log(vm.id)
      })
    }
  }

  // "beforeRouteUpdate" & "beforeRouteLeave" can only use "NextFunc"
  @Lifecycle
  async beforeRouteUpdate(to: Route, from: Route, next: NextFunc) {
    if (normalCase) {
      next()
    } else if (redirection) {
      next({ name: 'other-page' })
    } else if (cancel) {
      next(false)
    }
  }
}

Transition


Type: TypedPropertyDecorator

mark method as a callback of transition component. method is still called in other instance methods. This decorator is solely for type checking.

// solely for type checking! beforeEnter can be called in other methods
@Transition beforeEnter(el: HTMLElement) {
  el.style.opacity = 0
  el.style.height = 0
}

Data


Type: TypedPropertyDecorator

Collecting instance properties is heavy and hacky. It needs to find all props and other properties for you. If you want to make instance creation faster you can skip data collection. Here comes the Data decorator. When Data decorator is applied to a method, the method will be extracted as data function in vue's option, with this injected. And none instance property is counted as data option.

Example:

@Component
class TestData extends Vue {
  @Prop a = p(Number)
  b =  456 // this initializer will be ignored

  @Data data() {
    return {
      b: this.a // b will be initialized to prop value
    }
  }
}

let instance = new TestData({propsData: {a: 777}})
instance.b === 777 // true

Utility Functions


Mixin

has roughly type: <V>(parentConstructor: typeof Vue, ...traitConstructor: (typeof Vue)[]): {new(): V}

a function to mix all Traits decorated constructors into one Vue constructor.

To use Mixin correctly, you need to declare one interface to extend all traits you need. Then pass it as a generic type argument to Mixin<MixedInterface>(...traits). This is TypeScript's limitation.

In new version, you can just use Mixin(trait1, trait2). Note: Mixin supports at most four traits. More traits requires manuall type argument annotation.

It's return value is parentConstructor.extend({mixins: traitConstructor}): extending the first trait as parentConstructor and pack all remaining traits in mixins option.

See source for more specific type.

Example:

@Trait class Pen extends Vue {
  havePen() { alert('I have a pen')}
}
@Trait class Apple extends Vue {
  haveApple() { alert('I have an apple')}
}

// compiles under TS2.2
@Component class ApplePen extends Mixin(Apple, Pen) {
  Uh() {
    this.havePen()
    this.haveApple()
    alert('Apple pen')
  }
}

is equivalent to

var Pen = Vue.extend({
  methods: {
    havePen() { alert('I have a pen')}
  }
})
var Apple = Vue.extend({
  methods: {
    haveApple() { alert('I have an apple')}
  }
})

var Mixin = Pen.extend({
  mixins: [ Apple ]
})

var ApplePen = Mixin.extend({
  methods: {
    Uh() {
      this.havePen()
      this.haveApple()
      alert('Apple pen')
    }
  }
})

Implementing PineapplePen and PenPineappleApplePen is left for exercise.

Explicit annotation example:

// Five traits and more reuire explicit annotation
interface GodLike extends FirstBlood, DoubleKill, KillingSpree, Rampage, Unstoppable {}

@Component
class LegendaryClass extends Mixin<GodLike>(FirstBlood, DoubleKill, KillingSpree, Rampage, Unstoppable) {
  dominate() {
    console.log('Mooooooooonster Kill')
  }
}

Component.register


// has type
Component.register: (key: $$Prop, logic: DecoratorProcessor) => void
// $$Prop is a special string type that means you have to prefix the key with `$$`
// DecoratorProcessor can access prototype, instance and options of the decorated class
// where
type $$Prop = string & {'$$Prop Brand': never}
type DecoratorProcessor = (proto: Vue, instance: Vue, options: ComponentOptions<Vue>) => void;

Component.register is for advanced users.

Sometimes you need to extend Vue's functionality by adding new instance option. Those new options usually are not type-safe. For example, render is a special method can access this but cannot be put in methods option at the same time.

To implement a new decorator. You need first to know how av-ts works underhood. The comment is quite a good start. Also you can find some example implmentation.

common tricks

Please see FAQ

Difference

Added Feature: