feat: @prop decorator

This commit is contained in:
Evan You 2019-02-25 17:47:02 -05:00
parent daf166553b
commit cbf95c642e
10 changed files with 155 additions and 9 deletions

View File

@ -0,0 +1,3 @@
__tests__/
__mocks__/
dist/packages

View File

@ -0,0 +1 @@
# @vue/decorators

View File

@ -0,0 +1,59 @@
import { prop } from '../src/index'
import { Component, createInstance } from '@vue/runtime-test'
test('without options', () => {
let capturedThisValue
let capturedPropsValue
class Foo extends Component<{ p: number }> {
@prop
p: number
created() {
capturedThisValue = this.p
capturedPropsValue = this.$props.p
}
}
createInstance(Foo, {
p: 1
})
expect(capturedThisValue).toBe(1)
expect(capturedPropsValue).toBe(1)
// explicit override
createInstance(Foo, {
p: 2
})
expect(capturedThisValue).toBe(2)
expect(capturedPropsValue).toBe(2)
})
test('with options', () => {
let capturedThisValue
let capturedPropsValue
class Foo extends Component<{ p: number }> {
@prop({
default: 1
})
p: number
created() {
capturedThisValue = this.p
capturedPropsValue = this.$props.p
}
}
// default value
createInstance(Foo)
expect(capturedThisValue).toBe(1)
expect(capturedPropsValue).toBe(1)
// explicit override
createInstance(Foo, {
p: 2
})
expect(capturedThisValue).toBe(2)
expect(capturedPropsValue).toBe(2)
})

View File

@ -0,0 +1,7 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/decorators.cjs.prod.js')
} else {
module.exports = require('./dist/decorators.cjs.js')
}

View File

@ -0,0 +1,21 @@
{
"name": "@vue/decorators",
"version": "3.0.0-alpha.1",
"description": "@vue/decorators",
"main": "index.js",
"module": "dist/decorators.esm-bundler.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/decorators#readme"
}

View File

@ -0,0 +1,22 @@
import { PropValidator, Component } from '@vue/runtime-core'
export function prop(
target: Component | PropValidator<any>,
key?: string
): any {
if (key) {
applyProp(target, key)
} else {
const options = target as PropValidator<any>
return (target: any, key: string) => {
applyProp(target, key, options)
}
}
}
function applyProp(target: any, key: string, options: PropValidator<any> = {}) {
// here `target` is the prototype of the component class
Object.defineProperty(target, `__prop_${key}`, {
value: options
})
}

View File

@ -104,6 +104,10 @@ export const reservedMethods: ReservedKeys = {
renderTriggered: 1 renderTriggered: 1
} }
// This is a special marker from the @prop decorator.
// The decorator stores prop options on the Class' prototype as __prop_xxx
const propPrefixRE = /^__prop_/
// This is called in the base component constructor and the return value is // This is called in the base component constructor and the return value is
// set on the instance as $options. // set on the instance as $options.
export function resolveComponentOptionsFromClass( export function resolveComponentOptionsFromClass(
@ -122,6 +126,12 @@ export function resolveComponentOptionsFromClass(
} }
} }
// pre-normalize array props options into object.
// we may need to attach more props to it (declared by decorators)
if (Array.isArray(options.props)) {
options.props = normalizePropsOptions(options.props)
}
const instanceDescriptors = Object.getOwnPropertyDescriptors(Class.prototype) const instanceDescriptors = Object.getOwnPropertyDescriptors(Class.prototype)
for (const key in instanceDescriptors) { for (const key in instanceDescriptors) {
const { get, value } = instanceDescriptors[key] const { get, value } = instanceDescriptors[key]
@ -132,13 +142,20 @@ export function resolveComponentOptionsFromClass(
// as it's already defined on the prototype // as it's already defined on the prototype
} else if (isFunction(value) && key !== 'constructor') { } else if (isFunction(value) && key !== 'constructor') {
if (key in reservedMethods) { if (key in reservedMethods) {
// lifecycle hooks / reserved methods
options[key] = value options[key] = value
} else { } else {
// normal methods
;(options.methods || (options.methods = {}))[key] = value ;(options.methods || (options.methods = {}))[key] = value
} }
} else if (propPrefixRE.test(key)) {
// decorator-declared props
const propName = key.replace(propPrefixRE, '')
;(options.props || (options.props = {}))[propName] = value
} }
} }
// post-normalize all prop options into same object format
if (options.props) { if (options.props) {
options.props = normalizePropsOptions(options.props) options.props = normalizePropsOptions(options.props)
} }

View File

@ -90,7 +90,7 @@ export function resolveProps(
const hasDefault = opt.hasOwnProperty('default') const hasDefault = opt.hasOwnProperty('default')
const currentValue = props[key] const currentValue = props[key]
// default values // default values
if (hasDefault && currentValue === void 0) { if (hasDefault && currentValue === undefined) {
const defaultValue = opt.default const defaultValue = opt.default
props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue
} }
@ -106,7 +106,7 @@ export function resolveProps(
} }
} }
// runtime validation // runtime validation
if (__DEV__) { if (__DEV__ && rawData) {
validateProp(key, unwrap(rawData[key]), opt, isAbsent) validateProp(key, unwrap(rawData[key]), opt, isAbsent)
} }
} }
@ -138,11 +138,14 @@ export function normalizePropsOptions(
for (const key in raw) { for (const key in raw) {
const opt = raw[key] const opt = raw[key]
const prop = (normalized[camelize(key)] = const prop = (normalized[camelize(key)] =
isArray(opt) || isFunction(opt) ? { type: opt } : opt) as NormalizedProp isArray(opt) || isFunction(opt) ? { type: opt } : opt)
const booleanIndex = getTypeIndex(Boolean, prop.type) if (prop) {
const stringIndex = getTypeIndex(String, prop.type) const booleanIndex = getTypeIndex(Boolean, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1 const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex ;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1
;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] =
booleanIndex < stringIndex
}
} }
} }
return normalized return normalized

View File

@ -1,6 +1,7 @@
import { ComponentInstance } from './component' import { ComponentInstance } from './component'
import { observable } from '@vue/observer' import { observable } from '@vue/observer'
import { isReservedKey } from '@vue/shared' import { isReservedKey } from '@vue/shared'
import { warn } from './warning'
export function initializeState( export function initializeState(
instance: ComponentInstance, instance: ComponentInstance,
@ -20,10 +21,22 @@ export function extractInitializers(
data: any = {} data: any = {}
): any { ): any {
const keys = Object.keys(instance) const keys = Object.keys(instance)
const props = instance.$options.props
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i] const key = keys[i]
if (!isReservedKey(key)) { if (!isReservedKey(key)) {
data[key] = (instance as any)[key] // it's possible for a prop to be present here when it's declared with
// decorators and has a default value.
if (props && props.hasOwnProperty(key)) {
__DEV__ &&
warn(
`Class property "${key}" is declared as a prop but also has an initializer. ` +
`If you are trying to provide a default value for the prop, use the ` +
`prop's "default" option instead.`
)
} else {
data[key] = (instance as any)[key]
}
} }
} }
return data return data

View File

@ -140,7 +140,7 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
export function renderFunctionalRoot(vnode: VNode): VNode { export function renderFunctionalRoot(vnode: VNode): VNode {
const render = vnode.tag as FunctionalComponent const render = vnode.tag as FunctionalComponent
const [props, attrs] = resolveProps(vnode.data, render.props) const { 0: props, 1: attrs } = resolveProps(vnode.data, render.props)
let subTree let subTree
try { try {
subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode) subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode)