From cbf95c642ef4df34f300be2acb98c119d476f3df Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 25 Feb 2019 17:47:02 -0500 Subject: [PATCH] feat: @prop decorator --- packages/decorators/.npmignore | 3 + packages/decorators/README.md | 1 + packages/decorators/__tests__/prop.spec.ts | 59 +++++++++++++++++++ packages/decorators/index.js | 7 +++ packages/decorators/package.json | 21 +++++++ packages/decorators/src/index.ts | 22 +++++++ packages/runtime-core/src/componentOptions.ts | 17 ++++++ packages/runtime-core/src/componentProps.ts | 17 +++--- packages/runtime-core/src/componentState.ts | 15 ++++- packages/runtime-core/src/componentUtils.ts | 2 +- 10 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 packages/decorators/.npmignore create mode 100644 packages/decorators/README.md create mode 100644 packages/decorators/__tests__/prop.spec.ts create mode 100644 packages/decorators/index.js create mode 100644 packages/decorators/package.json create mode 100644 packages/decorators/src/index.ts diff --git a/packages/decorators/.npmignore b/packages/decorators/.npmignore new file mode 100644 index 00000000..bb5c8a54 --- /dev/null +++ b/packages/decorators/.npmignore @@ -0,0 +1,3 @@ +__tests__/ +__mocks__/ +dist/packages \ No newline at end of file diff --git a/packages/decorators/README.md b/packages/decorators/README.md new file mode 100644 index 00000000..d06feabb --- /dev/null +++ b/packages/decorators/README.md @@ -0,0 +1 @@ +# @vue/decorators \ No newline at end of file diff --git a/packages/decorators/__tests__/prop.spec.ts b/packages/decorators/__tests__/prop.spec.ts new file mode 100644 index 00000000..b2714f3f --- /dev/null +++ b/packages/decorators/__tests__/prop.spec.ts @@ -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) +}) diff --git a/packages/decorators/index.js b/packages/decorators/index.js new file mode 100644 index 00000000..6817e443 --- /dev/null +++ b/packages/decorators/index.js @@ -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') +} diff --git a/packages/decorators/package.json b/packages/decorators/package.json new file mode 100644 index 00000000..e88e438a --- /dev/null +++ b/packages/decorators/package.json @@ -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" +} \ No newline at end of file diff --git a/packages/decorators/src/index.ts b/packages/decorators/src/index.ts new file mode 100644 index 00000000..09899105 --- /dev/null +++ b/packages/decorators/src/index.ts @@ -0,0 +1,22 @@ +import { PropValidator, Component } from '@vue/runtime-core' + +export function prop( + target: Component | PropValidator, + key?: string +): any { + if (key) { + applyProp(target, key) + } else { + const options = target as PropValidator + return (target: any, key: string) => { + applyProp(target, key, options) + } + } +} + +function applyProp(target: any, key: string, options: PropValidator = {}) { + // here `target` is the prototype of the component class + Object.defineProperty(target, `__prop_${key}`, { + value: options + }) +} diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index e1e98136..05ff8dac 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -104,6 +104,10 @@ export const reservedMethods: ReservedKeys = { 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 // set on the instance as $options. 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) for (const key in instanceDescriptors) { const { get, value } = instanceDescriptors[key] @@ -132,13 +142,20 @@ export function resolveComponentOptionsFromClass( // as it's already defined on the prototype } else if (isFunction(value) && key !== 'constructor') { if (key in reservedMethods) { + // lifecycle hooks / reserved methods options[key] = value } else { + // normal methods ;(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) { options.props = normalizePropsOptions(options.props) } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 3b7dcfa9..01e17922 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -90,7 +90,7 @@ export function resolveProps( const hasDefault = opt.hasOwnProperty('default') const currentValue = props[key] // default values - if (hasDefault && currentValue === void 0) { + if (hasDefault && currentValue === undefined) { const defaultValue = opt.default props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue } @@ -106,7 +106,7 @@ export function resolveProps( } } // runtime validation - if (__DEV__) { + if (__DEV__ && rawData) { validateProp(key, unwrap(rawData[key]), opt, isAbsent) } } @@ -138,11 +138,14 @@ export function normalizePropsOptions( for (const key in raw) { const opt = raw[key] const prop = (normalized[camelize(key)] = - isArray(opt) || isFunction(opt) ? { type: opt } : opt) as NormalizedProp - const booleanIndex = getTypeIndex(Boolean, prop.type) - const stringIndex = getTypeIndex(String, prop.type) - prop[BooleanFlags.shouldCast] = booleanIndex > -1 - prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex + isArray(opt) || isFunction(opt) ? { type: opt } : opt) + if (prop) { + const booleanIndex = getTypeIndex(Boolean, prop.type) + const stringIndex = getTypeIndex(String, prop.type) + ;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1 + ;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] = + booleanIndex < stringIndex + } } } return normalized diff --git a/packages/runtime-core/src/componentState.ts b/packages/runtime-core/src/componentState.ts index 8d838dbe..8b6035c9 100644 --- a/packages/runtime-core/src/componentState.ts +++ b/packages/runtime-core/src/componentState.ts @@ -1,6 +1,7 @@ import { ComponentInstance } from './component' import { observable } from '@vue/observer' import { isReservedKey } from '@vue/shared' +import { warn } from './warning' export function initializeState( instance: ComponentInstance, @@ -20,10 +21,22 @@ export function extractInitializers( data: any = {} ): any { const keys = Object.keys(instance) + const props = instance.$options.props for (let i = 0; i < keys.length; i++) { const key = keys[i] 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 diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts index 3582b57f..0341eabf 100644 --- a/packages/runtime-core/src/componentUtils.ts +++ b/packages/runtime-core/src/componentUtils.ts @@ -140,7 +140,7 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode { export function renderFunctionalRoot(vnode: VNode): VNode { 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 try { subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode)