compile-props.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. var _ = require('../util')
  2. var textParser = require('../parsers/text')
  3. var propDef = require('../directives/prop')
  4. var propBindingModes = require('../config')._propBindingModes
  5. // regexes
  6. var identRE = require('../parsers/path').identRE
  7. var dataAttrRE = /^data-/
  8. var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
  9. var literalValueRE = /^(true|false)$|^\d.*/
  10. /**
  11. * Compile props on a root element and return
  12. * a props link function.
  13. *
  14. * @param {Element|DocumentFragment} el
  15. * @param {Array} propOptions
  16. * @return {Function} propsLinkFn
  17. */
  18. // TODO: 1.0.0 we can just loop through el.attributes and
  19. // check for prop- prefixes.
  20. module.exports = function compileProps (el, propOptions) {
  21. var props = []
  22. var i = propOptions.length
  23. var options, name, attr, value, path, prop, literal, single
  24. while (i--) {
  25. options = propOptions[i]
  26. name = options.name
  27. if (process.env.NODE_ENV !== 'production') {
  28. if (name === '$data') {
  29. _.deprecation.DATA_AS_PROP()
  30. }
  31. }
  32. // props could contain dashes, which will be
  33. // interpreted as minus calculations by the parser
  34. // so we need to camelize the path here
  35. path = _.camelize(name.replace(dataAttrRE, ''))
  36. if (!identRE.test(path)) {
  37. process.env.NODE_ENV !== 'production' && _.warn(
  38. 'Invalid prop key: "' + name + '". Prop keys ' +
  39. 'must be valid identifiers.'
  40. )
  41. continue
  42. }
  43. attr = _.hyphenate(name)
  44. value = el.getAttribute(attr)
  45. if (value === null) {
  46. value = el.getAttribute('data-' + attr)
  47. if (value !== null) {
  48. attr = 'data-' + attr
  49. if (process.env.NODE_ENV !== 'production') {
  50. _.deprecation.DATA_PROPS(attr, value)
  51. }
  52. }
  53. }
  54. // create a prop descriptor
  55. prop = {
  56. name: name,
  57. raw: value,
  58. path: path,
  59. options: options,
  60. mode: propBindingModes.ONE_WAY
  61. }
  62. if (value !== null) {
  63. // important so that this doesn't get compiled
  64. // again as a normal attribute binding
  65. el.removeAttribute(attr)
  66. var tokens = textParser.parse(value)
  67. if (tokens) {
  68. if (process.env.NODE_ENV !== 'production') {
  69. _.deprecation.PROPS(attr, value)
  70. }
  71. prop.dynamic = true
  72. prop.parentPath = textParser.tokensToExp(tokens)
  73. // check prop binding type.
  74. single = tokens.length === 1
  75. literal = literalValueRE.test(prop.parentPath)
  76. // one time: {{* prop}}
  77. if (literal || (single && tokens[0].oneTime)) {
  78. prop.mode = propBindingModes.ONE_TIME
  79. } else if (
  80. !literal &&
  81. (single && tokens[0].twoWay)
  82. ) {
  83. if (settablePathRE.test(prop.parentPath)) {
  84. prop.mode = propBindingModes.TWO_WAY
  85. } else {
  86. process.env.NODE_ENV !== 'production' && _.warn(
  87. 'Cannot bind two-way prop with non-settable ' +
  88. 'parent path: ' + prop.parentPath
  89. )
  90. }
  91. }
  92. }
  93. } else {
  94. // new syntax
  95. attr = 'bind-' + attr
  96. value = prop.raw = el.getAttribute(attr)
  97. if (value !== null) {
  98. // mark it so we know this is a bind
  99. prop.bindSyntax = true
  100. el.removeAttribute(attr)
  101. value = value.trim()
  102. // check binding type
  103. if (literalValueRE.test(value)) {
  104. prop.mode = propBindingModes.ONE_TIME
  105. } else {
  106. prop.dynamic = true
  107. if (value.charAt(0) === '*') {
  108. prop.mode = propBindingModes.ONE_TIME
  109. value = value.slice(1).trim()
  110. } else if (value.charAt(0) === '@') {
  111. value = value.slice(1).trim()
  112. if (settablePathRE.test(value)) {
  113. prop.mode = propBindingModes.TWO_WAY
  114. } else {
  115. process.env.NODE_ENV !== 'production' && _.warn(
  116. 'Cannot bind two-way prop with non-settable ' +
  117. 'parent path: ' + value
  118. )
  119. }
  120. }
  121. }
  122. }
  123. prop.parentPath = value
  124. }
  125. // warn required two-way
  126. if (
  127. process.env.NODE_ENV !== 'production' &&
  128. options.twoWay &&
  129. prop.mode !== propBindingModes.TWO_WAY
  130. ) {
  131. _.warn(
  132. 'Prop "' + name + '" expects a two-way binding type.'
  133. )
  134. }
  135. // warn missing required
  136. if (value === null && options && options.required) {
  137. process.env.NODE_ENV !== 'production' && _.warn(
  138. 'Missing required prop: ' + name
  139. )
  140. }
  141. // push prop
  142. props.push(prop)
  143. }
  144. return makePropsLinkFn(props)
  145. }
  146. /**
  147. * Build a function that applies props to a vm.
  148. *
  149. * @param {Array} props
  150. * @return {Function} propsLinkFn
  151. */
  152. function makePropsLinkFn (props) {
  153. return function propsLinkFn (vm, scope) {
  154. // store resolved props info
  155. vm._props = {}
  156. var i = props.length
  157. var prop, path, options, value
  158. while (i--) {
  159. prop = props[i]
  160. path = prop.path
  161. vm._props[path] = prop
  162. options = prop.options
  163. if (prop.raw === null) {
  164. // initialize absent prop
  165. _.initProp(vm, prop, getDefault(options))
  166. } else if (prop.dynamic) {
  167. // dynamic prop
  168. if (vm._context) {
  169. if (prop.mode === propBindingModes.ONE_TIME) {
  170. // one time binding
  171. value = (scope || vm._context).$get(prop.parentPath)
  172. _.initProp(vm, prop, value)
  173. } else {
  174. // dynamic binding
  175. vm._bindDir('prop', null, prop, propDef, null, scope)
  176. }
  177. } else {
  178. process.env.NODE_ENV !== 'production' && _.warn(
  179. 'Cannot bind dynamic prop on a root instance' +
  180. ' with no parent: ' + prop.name + '="' +
  181. prop.raw + '"'
  182. )
  183. }
  184. } else {
  185. // literal, cast it and just set once
  186. var raw = prop.raw
  187. if (options.type === Boolean && raw === '') {
  188. value = true
  189. } else if (raw.trim()) {
  190. value = _.toBoolean(_.toNumber(raw))
  191. if (process.env.NODE_ENV !== 'production' &&
  192. !prop.bindSyntax &&
  193. value !== raw) {
  194. _.deprecation.PROP_CASTING(prop.name, prop.raw)
  195. }
  196. } else {
  197. value = raw
  198. }
  199. _.initProp(vm, prop, value)
  200. }
  201. }
  202. }
  203. }
  204. /**
  205. * Get the default value of a prop.
  206. *
  207. * @param {Object} options
  208. * @return {*}
  209. */
  210. function getDefault (options) {
  211. // no default, return undefined
  212. if (!options.hasOwnProperty('default')) {
  213. // absent boolean value defaults to false
  214. return options.type === Boolean
  215. ? false
  216. : undefined
  217. }
  218. var def = options.default
  219. // warn against non-factory defaults for Object & Array
  220. if (_.isObject(def)) {
  221. process.env.NODE_ENV !== 'production' && _.warn(
  222. 'Object/Array as default prop values will be shared ' +
  223. 'across multiple instances. Use a factory function ' +
  224. 'to return the default value instead.'
  225. )
  226. }
  227. // call factory function for non-Function types
  228. return typeof def === 'function' && options.type !== Function
  229. ? def()
  230. : def
  231. }