]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: @prop decorator
authorEvan You <yyx990803@gmail.com>
Mon, 25 Feb 2019 22:47:02 +0000 (17:47 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 25 Feb 2019 22:47:02 +0000 (17:47 -0500)
packages/decorators/.npmignore [new file with mode: 0644]
packages/decorators/README.md [new file with mode: 0644]
packages/decorators/__tests__/prop.spec.ts [new file with mode: 0644]
packages/decorators/index.js [new file with mode: 0644]
packages/decorators/package.json [new file with mode: 0644]
packages/decorators/src/index.ts [new file with mode: 0644]
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentState.ts
packages/runtime-core/src/componentUtils.ts

diff --git a/packages/decorators/.npmignore b/packages/decorators/.npmignore
new file mode 100644 (file)
index 0000000..bb5c8a5
--- /dev/null
@@ -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 (file)
index 0000000..d06feab
--- /dev/null
@@ -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 (file)
index 0000000..b2714f3
--- /dev/null
@@ -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 (file)
index 0000000..6817e44
--- /dev/null
@@ -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 (file)
index 0000000..e88e438
--- /dev/null
@@ -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 (file)
index 0000000..0989910
--- /dev/null
@@ -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
+  })
+}
index e1e98136ef1c50498ac894430f68e15672356b93..05ff8dac246b95557be615ca6765eaf9bdcbbb54 100644 (file)
@@ -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)
   }
index 3b7dcfa9a2a58cccb69a35a64465fc8f85a1ea32..01e17922ca02fc0f1d5f75350ff2046c3a75a8c4 100644 (file)
@@ -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
index 8d838dbe1f65a4578235befdc113e8ed54286a0a..8b6035c9a24a10b9caba64408bf12b1695ca6023 100644 (file)
@@ -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
index 3582b57f7795fe047cda84e930789c45b8872bf8..0341eabf08d70883faf903cf0171ff365b0f3dbd 100644 (file)
@@ -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)