From 91455a6618c4c34e5d8b3f8299f427d3b612c142 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Apr 2016 20:24:59 -0400 Subject: [PATCH] prop validation --- src/runtime/instance/render.js | 25 +++-- src/runtime/instance/state.js | 27 ++--- src/runtime/observer/watcher.js | 4 +- src/runtime/util/index.js | 1 + src/runtime/util/options.js | 31 +++--- src/runtime/util/props.js | 171 ++++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 51 deletions(-) create mode 100644 src/runtime/util/props.js diff --git a/src/runtime/instance/render.js b/src/runtime/instance/render.js index 78a67eed..7d18970d 100644 --- a/src/runtime/instance/render.js +++ b/src/runtime/instance/render.js @@ -1,7 +1,6 @@ -import { extend, resolveAsset, hasOwn, isArray, isObject } from '../util/index' +import { extend, resolveAsset, hasOwn, isArray, isObject, getPropValue } from '../util/index' import { createElement, patch, updateListeners, flatten } from '../vdom/index' import { callHook } from './lifecycle' -import { getPropValue } from './state' export const renderState = { activeInstance: null, @@ -60,11 +59,11 @@ export function renderMixin (Vue) { // but if no props changed, nothing happens updateProps(this, parentData) updateEvents(this, parentData, oldParentData) - } - // diff parent data (attrs on the placeholder) and queue update - // if anything changed - if (parentDataChanged(parentData, oldParentData)) { - this.$forceUpdate() + // diff parent data (attrs on the placeholder) and queue update + // if anything changed + if (parentDataChanged(parentData, oldParentData)) { + this.$forceUpdate() + } } } @@ -176,7 +175,7 @@ function mergeParentData (vm, data, parentData) { if (parentData.attrs) { const attrs = data.attrs || (data.attrs = {}) for (let key in parentData.attrs) { - if (!hasOwn(props, key)) { + if (!props[key]) { attrs[key] = parentData.attrs[key] } } @@ -184,7 +183,7 @@ function mergeParentData (vm, data, parentData) { if (parentData.props) { const props = data.props || (data.props = {}) for (let key in parentData.props) { - if (!hasOwn(props, key)) { + if (!props[key]) { props[key] = parentData.props[key] } } @@ -215,8 +214,12 @@ function mergeParentData (vm, data, parentData) { function updateProps (vm, data) { if (data.attrs || data.props) { - for (let key in vm.$options.props) { - vm[key] = getPropValue(data, key) + let keys = vm.$options.propKeys + if (keys) { + for (let i = 0; i < keys.length; i++) { + let key = keys[i] + vm[key] = getPropValue(data, key, vm) + } } } } diff --git a/src/runtime/instance/state.js b/src/runtime/instance/state.js index 39a81dfa..b56e032a 100644 --- a/src/runtime/instance/state.js +++ b/src/runtime/instance/state.js @@ -10,8 +10,8 @@ import { hasOwn, isReserved, isPlainObject, - hyphenate, - bind + bind, + getPropValue } from '../util/index' export function initState (vm) { @@ -27,31 +27,16 @@ function initProps (vm) { const data = vm.$options._renderData const props = vm.$options.props if (props) { + const keys = vm.$options.propKeys = Object.keys(props) withoutConversion(() => { - for (let key in props) { - defineReactive(vm, key, getPropValue(data, key)) + for (let i = 0; i < keys.length; i++) { + let key = keys[i] + defineReactive(vm, key, getPropValue(data, key, vm)) } }) } } -export function getPropValue (data, key) { - if (!data) return - const altKey = hyphenate(key) - const attrVal = getPropValueFromHash(data.attrs, key, altKey) - return attrVal === undefined - ? getPropValueFromHash(data.props, key, altKey) - : attrVal -} - -function getPropValueFromHash (hash, key, altKey) { - return hash - ? hasOwn(hash, key) - ? hash[key] - : hash[altKey] - : undefined -} - function initData (vm) { var data = vm.$options.data data = vm._data = typeof data === 'function' diff --git a/src/runtime/observer/watcher.js b/src/runtime/observer/watcher.js index 22a42e78..d2722cb4 100644 --- a/src/runtime/observer/watcher.js +++ b/src/runtime/observer/watcher.js @@ -58,8 +58,10 @@ export default function Watcher (vm, expOrFn, cb, options) { if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( + 'Failed watching path: ' + expOrFn + 'Watcher only accepts simple dot-delimited paths. ' + - 'For full control, use a function instead.' + 'For full control, use a function instead.', + vm ) } } diff --git a/src/runtime/util/index.js b/src/runtime/util/index.js index e8674473..a1cd3a26 100644 --- a/src/runtime/util/index.js +++ b/src/runtime/util/index.js @@ -3,4 +3,5 @@ export * from './env' export * from './dom' export * from './options' export * from './debug' +export * from './props' export { defineReactive } from '../observer/index' diff --git a/src/runtime/util/options.js b/src/runtime/util/options.js index 7f8fb3ac..eeedbb3e 100644 --- a/src/runtime/util/options.js +++ b/src/runtime/util/options.js @@ -9,8 +9,7 @@ import { isArray, isPlainObject, hasOwn, - camelize, - hyphenate + camelize } from './lang' /** @@ -216,9 +215,6 @@ function guardComponents (options) { var components = options.components var ids = Object.keys(components) var def - if (process.env.NODE_ENV !== 'production') { - var map = options._componentNameMap = {} - } for (var i = 0, l = ids.length; i < l; i++) { var key = ids[i] if (isReservedTag(key)) { @@ -228,11 +224,6 @@ function guardComponents (options) { ) continue } - // record a all lowercase <-> kebab-case mapping for - // possible custom element case error warning - if (process.env.NODE_ENV !== 'production') { - map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key) - } def = components[key] if (isPlainObject(def)) { components[key] = Vue.extend(def) @@ -249,17 +240,19 @@ function guardComponents (options) { */ function guardProps (options) { - const res = {} const props = options.props - let i, val + if (!props) return + const res = {} + let i, val, name if (isArray(props)) { i = props.length while (i--) { val = props[i] if (typeof val === 'string') { - res[camelize(val)] = null - } else if (val.name) { - res[camelize(val.name)] = val + name = camelize(val) + res[name] = { type: null } + } else if (process.env.NODE_ENV !== 'production') { + warn('props must be strings when using array syntax.') } } } else if (isPlainObject(props)) { @@ -267,11 +260,13 @@ function guardProps (options) { i = keys.length while (i--) { val = props[keys[i]] - res[camelize(keys[i])] = typeof val === 'function' - ? { type: val } - : val + name = camelize(keys[i]) + res[name] = isPlainObject(val) + ? val + : { type: val } } } + console.log(res) options.props = res } diff --git a/src/runtime/util/props.js b/src/runtime/util/props.js new file mode 100644 index 00000000..8476be01 --- /dev/null +++ b/src/runtime/util/props.js @@ -0,0 +1,171 @@ +import { hyphenate, hasOwn, isArray, isObject, isPlainObject } from './lang' +import { warn } from './debug' + +export function getPropValue (data, key, vm) { + if (!data) return + const prop = vm.$options.props[key] + const altKey = hyphenate(key) + const attrVal = getPropValueFromHash(data.attrs, key, altKey) + let value = attrVal === undefined + ? getPropValueFromHash(data.props, key, altKey) + : attrVal + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key) + } + if (process.env.NODE_ENV !== 'production') { + assertProp(prop, key, value, vm) + } + return value +} + +function getPropValueFromHash (hash, key, altKey) { + return hash + ? hasOwn(hash, key) + ? hash[key] + : hash[altKey] + : undefined +} + +/** + * Get the default value of a prop. + * + * @param {Vue} vm + * @param {Object} prop + * @return {*} + */ + +function getPropDefaultValue (vm, prop, name) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + // absent boolean value defaults to false + return prop.type === Boolean + ? false + : undefined + } + var def = prop.default + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + process.env.NODE_ENV !== 'production' && warn( + 'Invalid default value for prop "' + name + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ) + } + // call factory function for non-Function types + return typeof def === 'function' && prop.type !== Function + ? def.call(vm) + : def +} + +/** + * Assert whether a prop is valid. + * + * @param {Object} prop + * @param {*} value + * @param {Vue} vm + */ + +function assertProp (prop, name, value, vm) { + if (prop.required && value == null) { + return false + } + var type = prop.type + var valid = !type + var expectedTypes = [] + if (type) { + if (!isArray(type)) { + type = [type] + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]) + expectedTypes.push(assertedType.expectedType) + valid = assertedType.valid + } + } + if (!valid) { + if (process.env.NODE_ENV !== 'production') { + warn( + 'Invalid prop: type check failed for prop "' + name + '".' + + ' Expected ' + expectedTypes.map(formatType).join(', ') + + ', got ' + formatValue(value) + '.', + vm + ) + } + return false + } + var validator = prop.validator + if (validator) { + if (!validator(value)) { + process.env.NODE_ENV !== 'production' && warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ) + return false + } + } + return true +} + +/** + * Assert the type of a value + * + * @param {*} value + * @param {Function} type + * @return {Object} + */ + +function assertType (value, type) { + var valid + var expectedType + if (type === String) { + expectedType = 'string' + valid = typeof value === expectedType + } else if (type === Number) { + expectedType = 'number' + valid = typeof value === expectedType + } else if (type === Boolean) { + expectedType = 'boolean' + valid = typeof value === expectedType + } else if (type === Function) { + expectedType = 'function' + valid = typeof value === expectedType + } else if (type === Object) { + expectedType = 'object' + valid = isPlainObject(value) + } else if (type === Array) { + expectedType = 'array' + valid = isArray(value) + } else { + valid = value instanceof type + } + return { + valid, + expectedType + } +} + +/** + * Format type for output + * + * @param {String} type + * @return {String} + */ + +function formatType (type) { + return type + ? type.charAt(0).toUpperCase() + type.slice(1) + : 'custom type' +} + +/** + * Format value + * + * @param {*} value + * @return {String} + */ + +function formatValue (val) { + return Object.prototype.toString.call(val).slice(8, -1) +}