Browse Source

prop assertions WIP

Evan You 11 years ago
parent
commit
775c982874
6 changed files with 127 additions and 21 deletions
  1. 25 10
      src/compiler/compile.js
  2. 1 1
      src/instance/init.js
  3. 22 6
      src/instance/scope.js
  4. 16 1
      src/util/lang.js
  5. 62 2
      src/util/misc.js
  6. 1 1
      test/unit/specs/directives/prop_spec.js

+ 25 - 10
src/compiler/compile.js

@@ -396,21 +396,29 @@ function makeChildLinkFn (linkFns) {
  *
  * @param {Element|DocumentFragment} el
  * @param {Object} attrs
- * @param {Array} propNames
+ * @param {Array} propDescriptors
  * @return {Function} propsLinkFn
  */
 
 var dataAttrRE = /^data-/
 var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
-var literalValueRE = /^(true|false|\d+)$/
+var literalValueRE = /^(true|false)$|\d.*/
 var identRE = require('../parsers/path').identRE
 
-function compileProps (el, attrs, propNames) {
+function compileProps (el, attrs, propDescriptors) {
   var props = []
-  var i = propNames.length
-  var name, value, path, prop, literal, single
+  var i = propDescriptors.length
+  var descriptor, name, assertions, value, path, prop, literal, single
   while (i--) {
-    name = propNames[i]
+    descriptor = propDescriptors[i]
+    // normalize prop string/descriptor
+    if (typeof descriptor === 'object') {
+      name = descriptor.name
+      assertions = descriptor.assertions
+    } else {
+      name = descriptor
+      assertions = null
+    }
     // props could contain dashes, which will be
     // interpreted as minus calculations by the parser
     // so we need to camelize the path here
@@ -437,6 +445,7 @@ function compileProps (el, attrs, propNames) {
         name: name,
         raw: value,
         path: path,
+        assertions: descriptor,
         mode: propBindingModes.ONE_WAY
       }
       var tokens = textParser.parse(value)
@@ -485,7 +494,7 @@ function compileProps (el, attrs, propNames) {
 function makePropsLinkFn (props) {
   return function propsLinkFn (vm, el) {
     var i = props.length
-    var prop, path
+    var prop, path, value
     while (i--) {
       prop = props[i]
       path = prop.path
@@ -493,7 +502,10 @@ function makePropsLinkFn (props) {
         if (vm.$parent) {
           if (prop.mode === propBindingModes.ONE_TIME) {
             // one time binding
-            vm.$set(path, vm.$parent.$get(prop.parentPath))
+            value = vm.$parent.$get(prop.parentPath)
+            if (_.assertProp(prop, value)) {
+              vm.$set(path, value)
+            }
           } else {
             // dynamic binding
             vm._bindDir('prop', el, prop, propDef)
@@ -506,8 +518,11 @@ function makePropsLinkFn (props) {
           )
         }
       } else {
-        // literal, just set once
-        vm.$set(path, _.toNumber(prop.raw))
+        // literal, cast it and just set once
+        value = _.toBoolean(_.toNumber(prop.raw))
+        if (_.assertProp(prop, value)) {
+          vm.$set(path, value)
+        }
       }
     }
   }

+ 1 - 1
src/instance/init.js

@@ -89,4 +89,4 @@ exports._init = function (options) {
   if (options.el) {
     this.$mount(options.el)
   }
-}
+}

+ 22 - 6
src/instance/scope.js

@@ -11,6 +11,7 @@ var Dep = require('../observer/dep')
  */
 
 exports._initScope = function () {
+  this._initProps()
   this._initData()
   this._initComputed()
   this._initMethods()
@@ -18,25 +19,40 @@ exports._initScope = function () {
 }
 
 /**
- * Initialize the data. 
+ * Initialize props.
  */
 
-exports._initData = function () {
-  // proxy data on instance
-  var data = this._data
-  var i, key
+exports._initProps = function () {
   // make sure all props properties are observed
+  var data = this._data
   var props = this.$options.props
+  var prop, key, i
   if (props) {
     i = props.length
     while (i--) {
-      key = _.camelize(props[i])
+      prop = props[i]
+      // props can be strings or object descriptors
+      key = _.camelize(
+        typeof prop === 'string'
+          ? prop
+          : prop.name
+      )
       if (!(key in data) && key !== '$data') {
         data[key] = undefined
       }
     }
   }
+}
+
+/**
+ * Initialize the data. 
+ */
+
+exports._initData = function () {
+  // proxy data on instance
+  var data = this._data
   var keys = Object.keys(data)
+  var i, key
   i = keys.length
   while (i--) {
     key = keys[i]

+ 16 - 1
src/util/lang.js

@@ -41,6 +41,21 @@ exports.toNumber = function (value) {
     : Number(value)
 }
 
+/**
+ * Convert string boolean literals into real booleans.
+ *
+ * @param {*} value
+ * @return {*|Boolean}
+ */
+
+exports.toBoolean = function (value) {
+  return value === 'true'
+    ? true
+    : value === 'false'
+      ? false
+      : value
+}
+
 /**
  * Strip quotes from a string
  *
@@ -266,4 +281,4 @@ exports.cancellable = function (fn) {
     cb.cancelled = true
   }
   return cb
-}
+}

+ 62 - 2
src/util/misc.js

@@ -1,7 +1,64 @@
 var _ = require('./index')
 var config = require('../config')
-var commonTagRE = /^(div|p|span|img|a|br|ul|ol|li|h1|h2|h3|h4|h5|code|pre)$/
-var tableElementsRE = /^caption|colgroup|thead|tfoot|tbody|tr|td|th$/
+
+/**
+ * Assert whether a prop is valid.
+ *
+ * @param {Object} prop
+ * @param {*} value
+ */
+
+exports.assertProp = function (prop, value) {
+  var assertions = prop.assertions
+  if (!assertions) {
+    return true
+  }
+  var type = assertions.type
+  var valid = true
+  var expectedType
+  if (type) {
+    if (type === String) {
+      expectedType = 'string'
+      valid = typeof value === expectedType
+    } else if (type === Number) {
+      expectedType = 'number'
+      valid = typeof value === 'number'
+    } else if (type === Boolean) {
+      expectedType = 'boolean'
+      valid = typeof value === 'boolean'
+    } else if (type === Function) {
+      expectedType = 'function'
+      valid = typeof value === 'function'
+    } else if (type === Object) {
+      expectedType = 'object'
+      valid = _.isPlainObject(value)
+    } else if (type === Array) {
+      expectedType = 'array'
+      valid = _.isArray(value)
+    } else {
+      valid = value instanceof type
+    }
+  }
+  if (!valid) {
+    _.warn(
+      'Invalid prop: type check failed for ' +
+      prop.path + '="' + prop.raw + '".' +
+      (expectedType ? ' Expected ' + expectedType + '.' : '')
+    )
+    return false
+  }
+  var validator = assertions.validator
+  if (validator) {
+    if (!validator.call(null, value)) {
+      _.warn(
+        'Invalid prop: custom validator check failed for ' +
+        prop.path + '="' + prop.raw + '"'
+      )
+      return false
+    }
+  }
+  return true
+}
 
 /**
  * Check if an element is a component, if yes return its
@@ -12,6 +69,9 @@ var tableElementsRE = /^caption|colgroup|thead|tfoot|tbody|tr|td|th$/
  * @return {String|undefined}
  */
 
+var commonTagRE = /^(div|p|span|img|a|br|ul|ol|li|h1|h2|h3|h4|h5|code|pre)$/
+var tableElementsRE = /^caption|colgroup|thead|tfoot|tbody|tr|td|th$/
+
 exports.checkComponent = function (el, options) {
   var tag = el.tagName.toLowerCase()
   if (tag === 'component') {

+ 1 - 1
test/unit/specs/directives/prop_spec.js

@@ -10,7 +10,7 @@ if (_.inBrowser) {
       spyOn(_, 'warn')
     })
 
-    it('one way down binding', function (done) {
+    it('one way binding', function (done) {
       var vm = new Vue({
         el: el,
         data: {