Skip to content

Commit dcf5fcd

Browse files
committed
prop: further differentiate one-time, oneway-up and oneway-down
1 parent 6b6402b commit dcf5fcd

File tree

4 files changed

+120
-73
lines changed

4 files changed

+120
-73
lines changed

src/compiler/compile.js

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -399,17 +399,21 @@ function makeChildLinkFn (linkFns) {
399399
* @return {Function} propsLinkFn
400400
*/
401401

402-
// regex to test if a path is "settable"
403-
// if not the prop binding is automatically one-way.
402+
var dataAttrRE = /^data-/
404403
var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
405404
var literalValueRE = /^true|false|\d+$/
405+
var identRE = require('../parsers/path').identRE
406406

407407
function compileProps (el, attrs, propNames) {
408408
var props = []
409409
var i = propNames.length
410-
var name, value, prop
410+
var name, value, path, prop, settable, single
411411
while (i--) {
412412
name = propNames[i]
413+
// props could contain dashes, which will be
414+
// interpreted as minus calculations by the parser
415+
// so we need to camelize the path here
416+
path = _.camelize(name.replace(dataAttrRE, ''))
413417
if (/[A-Z]/.test(name)) {
414418
_.warn(
415419
'You seem to be using camelCase for a component prop, ' +
@@ -419,26 +423,48 @@ function compileProps (el, attrs, propNames) {
419423
'http://vuejs.org/api/options.html#props'
420424
)
421425
}
426+
if (!identRE.test(path)) {
427+
_.warn(
428+
'Invalid prop key: "' + name + '". Prop keys ' +
429+
'must be valid identifiers.'
430+
)
431+
}
422432
value = attrs[name]
423433
/* jshint eqeqeq:false */
424434
if (value != null) {
425435
prop = {
426436
name: name,
427-
raw: value
437+
raw: value,
438+
path: path
428439
}
429440
var tokens = textParser.parse(value)
430441
if (tokens) {
431442
if (el && el.nodeType === 1) {
432443
el.removeAttribute(name)
433444
}
445+
// important so that this doesn't get compiled
446+
// again as a normal attribute binding
434447
attrs[name] = null
435448
prop.dynamic = true
436-
prop.value = textParser.tokensToExp(tokens)
449+
prop.parentPath = textParser.tokensToExp(tokens)
450+
// check prop binding type.
451+
single = tokens.length === 1
452+
settable =
453+
settablePathRE.test(prop.parentPath) &&
454+
!literalValueRE.test(prop.parentPath)
455+
// one time: {{* prop}}
437456
prop.oneTime =
438-
tokens.length > 1 ||
439-
tokens[0].oneTime ||
440-
!settablePathRE.test(prop.value) ||
441-
literalValueRE.test(prop.value)
457+
!single ||
458+
!settable ||
459+
tokens[0].oneTime
460+
// one way down: {{> prop}}
461+
prop.oneWayDown =
462+
single &&
463+
tokens[0].oneWay === 62 // >
464+
// one way up: {{< prop}}
465+
prop.oneWayUp =
466+
tokens[0].oneWay === 60 && // <
467+
settable
442468
}
443469
props.push(prop)
444470
}
@@ -453,25 +479,22 @@ function compileProps (el, attrs, propNames) {
453479
* @return {Function} propsLinkFn
454480
*/
455481

456-
var dataAttrRE = /^data-/
457-
458482
function makePropsLinkFn (props) {
459483
return function propsLinkFn (vm, el) {
460484
var i = props.length
461485
var prop, path
462486
while (i--) {
463487
prop = props[i]
464-
// props could contain dashes, which will be
465-
// interpreted as minus calculations by the parser
466-
// so we need to wrap the path here
467-
path = _.camelize(prop.name.replace(dataAttrRE, ''))
488+
path = prop.path
468489
if (prop.dynamic) {
469490
if (vm.$parent) {
470-
vm._bindDir('prop', el, {
471-
arg: path,
472-
expression: prop.value,
473-
oneWay: prop.oneTime
474-
}, propDef)
491+
if (prop.onetime) {
492+
// one time binding
493+
vm.$set(path, vm.$parent.$get(prop.parentPath))
494+
} else {
495+
// dynamic binding
496+
vm._bindDir('prop', el, prop, propDef)
497+
}
475498
} else {
476499
_.warn(
477500
'Cannot bind dynamic prop on a root instance' +
@@ -480,7 +503,7 @@ function makePropsLinkFn (props) {
480503
)
481504
}
482505
} else {
483-
// just set once
506+
// literal, just set once
484507
vm.$set(path, _.toNumber(prop.raw))
485508
}
486509
}

src/directives/prop.js

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
var _ = require('../util')
22
var Watcher = require('../watcher')
3-
var identRE = require('../parsers/path').identRE
43

54
module.exports = {
65

76
bind: function () {
87

98
var child = this.vm
109
var parent = child.$parent
11-
var childKey = this.arg
12-
var parentKey = this.expression
13-
14-
if (!identRE.test(childKey)) {
15-
_.warn(
16-
'Invalid prop key: "' + childKey + '". Prop keys ' +
17-
'must be valid identifiers.'
18-
)
19-
}
10+
// passed in from compiler directly
11+
var prop = this._descriptor
12+
var childKey = prop.path
13+
var parentKey = prop.parentPath
2014

2115
// simple lock to avoid circular updates.
2216
// without this it would stabilize too, but this makes
@@ -30,26 +24,28 @@ module.exports = {
3024
locked = false
3125
}
3226

33-
this.parentWatcher = new Watcher(
34-
parent,
35-
parentKey,
36-
function (val) {
37-
if (!locked) {
38-
lock()
39-
// all props have been initialized already
40-
child[childKey] = val
27+
if (!prop.oneWayUp) {
28+
this.parentWatcher = new Watcher(
29+
parent,
30+
parentKey,
31+
function (val) {
32+
if (!locked) {
33+
lock()
34+
// all props have been initialized already
35+
child[childKey] = val
36+
}
4137
}
42-
}
43-
)
44-
45-
// set the child initial value first, before setting
46-
// up the child watcher to avoid triggering it
47-
// immediately.
48-
child.$set(childKey, this.parentWatcher.value)
38+
)
39+
40+
// set the child initial value first, before setting
41+
// up the child watcher to avoid triggering it
42+
// immediately.
43+
child.$set(childKey, this.parentWatcher.value)
44+
}
4945

5046
// only setup two-way binding if this is not a one-way
5147
// binding.
52-
if (!this._descriptor.oneWay) {
48+
if (!prop.oneWayDown) {
5349
this.childWatcher = new Watcher(
5450
child,
5551
childKey,
@@ -60,6 +56,11 @@ module.exports = {
6056
}
6157
}
6258
)
59+
60+
// set initial value for one-way up binding
61+
if (prop.oneWayUp) {
62+
parent.$set(parentKey, this.childWatcher.value)
63+
}
6364
}
6465
},
6566

@@ -71,5 +72,4 @@ module.exports = {
7172
this.childWatcher.teardown()
7273
}
7374
}
74-
75-
}
75+
}

src/parsers/text.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ exports.parse = function (text) {
7070
}
7171
var tokens = []
7272
var lastIndex = tagRE.lastIndex = 0
73-
var match, index, value, first, oneTime
73+
var match, index, value, first, oneTime, oneWay
7474
/* jshint boss:true */
7575
while (match = tagRE.exec(text)) {
7676
index = match.index
@@ -82,15 +82,17 @@ exports.parse = function (text) {
8282
}
8383
// tag token
8484
first = match[1].charCodeAt(0)
85-
oneTime = first === 0x2A // *
86-
value = oneTime
85+
oneTime = first === 42 // *
86+
oneWay = first === 62 || first === 60 // > or <
87+
value = oneTime || oneWay
8788
? match[1].slice(1)
8889
: match[1]
8990
tokens.push({
9091
tag: true,
9192
value: value.trim(),
9293
html: htmlRE.test(match[0]),
93-
oneTime: oneTime
94+
oneTime: oneTime,
95+
oneWay: oneWay ? first : 0
9496
})
9597
lastIndex = index + match[0].length
9698
}
@@ -169,4 +171,4 @@ function inlineFilters (exp, single) {
169171
',false)' // write?
170172
}
171173
}
172-
}
174+
}

test/unit/specs/compiler/compile_spec.js

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ if (_.inBrowser) {
153153
'data-some-attr',
154154
'some-other-attr',
155155
'multiple-attrs',
156-
'oneway',
156+
'onetime',
157+
'oneway-up',
158+
'oneway-down',
157159
'with-filter',
158160
'camelCase',
159161
'boolean-literal'
@@ -164,49 +166,69 @@ if (_.inBrowser) {
164166
el.setAttribute('data-some-attr', '{{a}}')
165167
el.setAttribute('some-other-attr', '2')
166168
el.setAttribute('multiple-attrs', 'a {{b}} c')
167-
el.setAttribute('oneway', '{{*a}}')
169+
el.setAttribute('onetime', '{{*a}}')
170+
el.setAttribute('oneway-up', '{{<a}}')
171+
el.setAttribute('oneway-down', '{{>a}}')
168172
el.setAttribute('with-filter', '{{a | filter}}')
169173
el.setAttribute('boolean-literal', '{{true}}')
170174
transclude(el, options)
171175
compiler.compileAndLinkRoot(vm, el, options)
172176
// should skip literals and one-time bindings
173-
expect(vm._bindDir.calls.count()).toBe(5)
177+
expect(vm._bindDir.calls.count()).toBe(7)
174178
// data-some-attr
175179
var args = vm._bindDir.calls.argsFor(0)
176180
expect(args[0]).toBe('prop')
177181
expect(args[1]).toBe(null)
178-
expect(args[2].arg).toBe('someAttr')
179-
expect(args[2].expression).toBe('a')
182+
expect(args[2].path).toBe('someAttr')
183+
expect(args[2].parentPath).toBe('a')
180184
expect(args[3]).toBe(def)
181185
// multiple-attrs
182186
args = vm._bindDir.calls.argsFor(1)
183187
expect(args[0]).toBe('prop')
184188
expect(args[1]).toBe(null)
185-
expect(args[2].arg).toBe('multipleAttrs')
186-
expect(args[2].expression).toBe('"a "+(b)+" c"')
189+
expect(args[2].path).toBe('multipleAttrs')
190+
expect(args[2].parentPath).toBe('"a "+(b)+" c"')
187191
expect(args[3]).toBe(def)
188-
// oneway
192+
// one time
189193
args = vm._bindDir.calls.argsFor(2)
190194
expect(args[0]).toBe('prop')
191195
expect(args[1]).toBe(null)
192-
expect(args[2].arg).toBe('oneway')
193-
expect(args[2].oneWay).toBe(true)
194-
expect(args[2].expression).toBe('a')
196+
expect(args[2].path).toBe('onetime')
197+
expect(args[2].oneTime).toBe(true)
198+
expect(args[2].parentPath).toBe('a')
195199
expect(args[3]).toBe(def)
196-
// with-filter
200+
// one way up
197201
args = vm._bindDir.calls.argsFor(3)
198202
expect(args[0]).toBe('prop')
199203
expect(args[1]).toBe(null)
200-
expect(args[2].arg).toBe('withFilter')
201-
expect(args[2].expression).toBe('this._applyFilters(a,null,[{"name":"filter"}],false)')
204+
expect(args[2].path).toBe('onewayUp')
205+
expect(args[2].oneWayUp).toBe(true)
206+
expect(args[2].oneWayDown).toBe(false)
207+
expect(args[2].parentPath).toBe('a')
202208
expect(args[3]).toBe(def)
203-
// boolean-literal
209+
// one way down
204210
args = vm._bindDir.calls.argsFor(4)
205211
expect(args[0]).toBe('prop')
206212
expect(args[1]).toBe(null)
207-
expect(args[2].arg).toBe('booleanLiteral')
208-
expect(args[2].expression).toBe('true')
209-
expect(args[2].oneWay).toBe(true)
213+
expect(args[2].path).toBe('onewayDown')
214+
expect(args[2].oneWayUp).toBe(false)
215+
expect(args[2].oneWayDown).toBe(true)
216+
expect(args[2].parentPath).toBe('a')
217+
expect(args[3]).toBe(def)
218+
// with-filter
219+
args = vm._bindDir.calls.argsFor(5)
220+
expect(args[0]).toBe('prop')
221+
expect(args[1]).toBe(null)
222+
expect(args[2].path).toBe('withFilter')
223+
expect(args[2].parentPath).toBe('this._applyFilters(a,null,[{"name":"filter"}],false)')
224+
expect(args[3]).toBe(def)
225+
// boolean-literal
226+
args = vm._bindDir.calls.argsFor(6)
227+
expect(args[0]).toBe('prop')
228+
expect(args[1]).toBe(null)
229+
expect(args[2].path).toBe('booleanLiteral')
230+
expect(args[2].parentPath).toBe('true')
231+
expect(args[2].oneTime).toBe(true)
210232
// camelCase should've warn
211233
expect(hasWarned(_, 'using camelCase')).toBe(true)
212234
// literal and one time should've called vm.$set
@@ -408,4 +430,4 @@ if (_.inBrowser) {
408430
})
409431

410432
})
411-
}
433+
}

0 commit comments

Comments
 (0)