feat: @prop decorator
This commit is contained in:
parent
daf166553b
commit
cbf95c642e
3
packages/decorators/.npmignore
Normal file
3
packages/decorators/.npmignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
__tests__/
|
||||||
|
__mocks__/
|
||||||
|
dist/packages
|
1
packages/decorators/README.md
Normal file
1
packages/decorators/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @vue/decorators
|
59
packages/decorators/__tests__/prop.spec.ts
Normal file
59
packages/decorators/__tests__/prop.spec.ts
Normal 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)
|
||||||
|
})
|
7
packages/decorators/index.js
Normal file
7
packages/decorators/index.js
Normal 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')
|
||||||
|
}
|
21
packages/decorators/package.json
Normal file
21
packages/decorators/package.json
Normal 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"
|
||||||
|
}
|
22
packages/decorators/src/index.ts
Normal file
22
packages/decorators/src/index.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
if (prop) {
|
||||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||||
const stringIndex = getTypeIndex(String, prop.type)
|
const stringIndex = getTypeIndex(String, prop.type)
|
||||||
prop[BooleanFlags.shouldCast] = booleanIndex > -1
|
;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1
|
||||||
prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex
|
;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] =
|
||||||
|
booleanIndex < stringIndex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalized
|
return normalized
|
||||||
|
@ -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,11 +21,23 @@ 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)) {
|
||||||
|
// 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]
|
data[key] = (instance as any)[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user