compile-props.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. var _ = require('../util')
  2. var dirParser = require('../parsers/directive')
  3. var propDef = require('../directives/internal/prop')
  4. var propBindingModes = require('../config')._propBindingModes
  5. var empty = {}
  6. // regexes
  7. var identRE = require('../parsers/path').identRE
  8. var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
  9. /**
  10. * Compile props on a root element and return
  11. * a props link function.
  12. *
  13. * @param {Element|DocumentFragment} el
  14. * @param {Array} propOptions
  15. * @return {Function} propsLinkFn
  16. */
  17. module.exports = function compileProps (el, propOptions) {
  18. var props = []
  19. var names = Object.keys(propOptions)
  20. var i = names.length
  21. var options, name, attr, value, path, parsed, prop, isTitleBinding
  22. while (i--) {
  23. name = names[i]
  24. options = propOptions[name] || empty
  25. if (process.env.NODE_ENV !== 'production' && name === '$data') {
  26. _.warn('Do not use $data as prop.')
  27. continue
  28. }
  29. // props could contain dashes, which will be
  30. // interpreted as minus calculations by the parser
  31. // so we need to camelize the path here
  32. path = _.camelize(name)
  33. if (!identRE.test(path)) {
  34. process.env.NODE_ENV !== 'production' && _.warn(
  35. 'Invalid prop key: "' + name + '". Prop keys ' +
  36. 'must be valid identifiers.'
  37. )
  38. continue
  39. }
  40. prop = {
  41. name: name,
  42. path: path,
  43. options: options,
  44. mode: propBindingModes.ONE_WAY
  45. }
  46. // IE title issues
  47. isTitleBinding = false
  48. if (name === 'title' && (el.getAttribute(':title') || el.getAttribute('v-bind:title'))) {
  49. isTitleBinding = true
  50. }
  51. // first check literal version
  52. attr = _.hyphenate(name)
  53. value = prop.raw = _.attr(el, attr)
  54. if (value === null || isTitleBinding) {
  55. // then check dynamic version
  56. if ((value = _.getBindAttr(el, attr)) === null) {
  57. if ((value = _.getBindAttr(el, attr + '.sync')) !== null) {
  58. prop.mode = propBindingModes.TWO_WAY
  59. } else if ((value = _.getBindAttr(el, attr + '.once')) !== null) {
  60. prop.mode = propBindingModes.ONE_TIME
  61. }
  62. }
  63. prop.raw = value
  64. if (value !== null) {
  65. parsed = dirParser.parse(value)
  66. value = parsed.expression
  67. prop.filters = parsed.filters
  68. // check binding type
  69. if (_.isLiteral(value)) {
  70. // for expressions containing literal numbers and
  71. // booleans, there's no need to setup a prop binding,
  72. // so we can optimize them as a one-time set.
  73. prop.optimizedLiteral = true
  74. } else {
  75. prop.dynamic = true
  76. // check non-settable path for two-way bindings
  77. if (process.env.NODE_ENV !== 'production' &&
  78. prop.mode === propBindingModes.TWO_WAY &&
  79. !settablePathRE.test(value)) {
  80. prop.mode = propBindingModes.ONE_WAY
  81. _.warn(
  82. 'Cannot bind two-way prop with non-settable ' +
  83. 'parent path: ' + value
  84. )
  85. }
  86. }
  87. prop.parentPath = value
  88. // warn required two-way
  89. if (
  90. process.env.NODE_ENV !== 'production' &&
  91. options.twoWay &&
  92. prop.mode !== propBindingModes.TWO_WAY
  93. ) {
  94. _.warn(
  95. 'Prop "' + name + '" expects a two-way binding type.'
  96. )
  97. }
  98. } else if (options.required) {
  99. // warn missing required
  100. process.env.NODE_ENV !== 'production' && _.warn(
  101. 'Missing required prop: ' + name
  102. )
  103. }
  104. }
  105. // push prop
  106. props.push(prop)
  107. }
  108. return makePropsLinkFn(props)
  109. }
  110. /**
  111. * Build a function that applies props to a vm.
  112. *
  113. * @param {Array} props
  114. * @return {Function} propsLinkFn
  115. */
  116. function makePropsLinkFn (props) {
  117. return function propsLinkFn (vm, scope) {
  118. // store resolved props info
  119. vm._props = {}
  120. var i = props.length
  121. var prop, path, options, value, raw
  122. while (i--) {
  123. prop = props[i]
  124. raw = prop.raw
  125. path = prop.path
  126. options = prop.options
  127. vm._props[path] = prop
  128. if (raw === null) {
  129. // initialize absent prop
  130. _.initProp(vm, prop, getDefault(vm, options))
  131. } else if (prop.dynamic) {
  132. // dynamic prop
  133. if (vm._context) {
  134. if (prop.mode === propBindingModes.ONE_TIME) {
  135. // one time binding
  136. value = (scope || vm._context).$get(prop.parentPath)
  137. _.initProp(vm, prop, value)
  138. } else {
  139. // dynamic binding
  140. vm._bindDir({
  141. name: 'prop',
  142. def: propDef,
  143. prop: prop
  144. }, null, null, scope) // el, host, scope
  145. }
  146. } else {
  147. process.env.NODE_ENV !== 'production' && _.warn(
  148. 'Cannot bind dynamic prop on a root instance' +
  149. ' with no parent: ' + prop.name + '="' +
  150. raw + '"'
  151. )
  152. }
  153. } else if (prop.optimizedLiteral) {
  154. // optimized literal, cast it and just set once
  155. raw = _.stripQuotes(raw)
  156. value = _.toBoolean(_.toNumber(raw))
  157. _.initProp(vm, prop, value)
  158. } else {
  159. // string literal, but we need to cater for
  160. // Boolean props with no value
  161. value = options.type === Boolean && raw === ''
  162. ? true
  163. : raw
  164. _.initProp(vm, prop, value)
  165. }
  166. }
  167. }
  168. }
  169. /**
  170. * Get the default value of a prop.
  171. *
  172. * @param {Vue} vm
  173. * @param {Object} options
  174. * @return {*}
  175. */
  176. function getDefault (vm, options) {
  177. // no default, return undefined
  178. if (!options.hasOwnProperty('default')) {
  179. // absent boolean value defaults to false
  180. return options.type === Boolean
  181. ? false
  182. : undefined
  183. }
  184. var def = options.default
  185. // warn against non-factory defaults for Object & Array
  186. if (_.isObject(def)) {
  187. process.env.NODE_ENV !== 'production' && _.warn(
  188. 'Object/Array as default prop values will be shared ' +
  189. 'across multiple instances. Use a factory function ' +
  190. 'to return the default value instead.'
  191. )
  192. }
  193. // call factory function for non-Function types
  194. return typeof def === 'function' && options.type !== Function
  195. ? def.call(vm)
  196. : def
  197. }