Skip to content

Commit 6d0697b

Browse files
committed
improve param attributes
- support multiple mustache tags - support filters - support arbitrary expression - support explicit one-way binding syntax {{*parentKey}} - non-settable expressions are automatically one-way
1 parent 35e8a54 commit 6d0697b

File tree

11 files changed

+145
-63
lines changed

11 files changed

+145
-63
lines changed

component.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"src/instance/compile.js",
5252
"src/instance/events.js",
5353
"src/instance/init.js",
54+
"src/instance/misc.js",
5455
"src/instance/scope.js",
5556
"src/observer/array.js",
5657
"src/observer/dep.js",

src/compiler/compile.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -418,18 +418,9 @@ function compileParamAttributes (el, attrs, paramNames, options) {
418418
el.removeAttribute(name)
419419
}
420420
attrs[name] = null
421-
if (tokens.length > 1) {
422-
_.warn(
423-
'Invalid param attribute binding: "' +
424-
name + '="' + value + '"' +
425-
'\nDon\'t mix binding tags with plain text ' +
426-
'in param attribute bindings.'
427-
)
428-
continue
429-
} else {
430-
param.dynamic = true
431-
param.value = tokens[0].value
432-
}
421+
param.dynamic = true
422+
param.value = textParser.tokensToExp(tokens)
423+
param.oneTime = tokens.length === 1 && tokens[0].oneTime
433424
}
434425
params.push(param)
435426
}
@@ -459,14 +450,18 @@ function makeParamsLinkFn (params, options) {
459450
// so we need to wrap the path here
460451
path = _.camelize(param.name.replace(dataAttrRE, ''))
461452
if (param.dynamic) {
462-
// dynamic param attribtues are bound as v-with.
463-
// we can directly duck the descriptor here beacuse
464-
// param attributes cannot use expressions or
465-
// filters.
466-
vm._bindDir('with', el, {
467-
arg: path,
468-
expression: param.value
469-
}, def)
453+
if (param.oneTime) {
454+
vm.$set(path, vm.$parent.$get(param.value))
455+
} else {
456+
// dynamic param attribtues are bound as v-with.
457+
// we can directly duck the descriptor here beacuse
458+
// param attributes cannot use expressions or
459+
// filters.
460+
vm._bindDir('with', el, {
461+
arg: path,
462+
expression: param.value
463+
}, def)
464+
}
470465
} else {
471466
// just set once
472467
vm.$set(path, param.value)

src/directive.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ p._checkStatement = function () {
141141
var expression = this.expression
142142
if (
143143
expression && this.acceptStatement &&
144-
!expParser.pathTestRE.test(expression)
144+
!expParser.isSimplePath(expression)
145145
) {
146146
var fn = expParser.parse(expression).get
147147
var vm = this.vm

src/directives/with.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,20 @@ module.exports = {
6363
// immediately.
6464
child.$set(childKey, this.parentWatcher.value)
6565

66-
this.childWatcher = new Watcher(
67-
child,
68-
childKey,
69-
function (val) {
70-
if (!locked) {
71-
lock()
72-
parent.$set(parentKey, val)
66+
// only setup two-way binding if the parentKey is
67+
// a "settable" simple path.
68+
if (expParser.isSimplePath(parentKey)) {
69+
this.childWatcher = new Watcher(
70+
child,
71+
childKey,
72+
function (val) {
73+
if (!locked) {
74+
lock()
75+
parent.$set(parentKey, val)
76+
}
7377
}
74-
}
75-
)
78+
)
79+
}
7680
}
7781
},
7882

src/instance/misc.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
var _ = require('../util')
2+
3+
/**
4+
* Apply a filter to a list of arguments.
5+
* This is only used internally inside expressions with
6+
* inlined filters.
7+
*
8+
* @param {String} id
9+
* @param {Array} args
10+
* @return {*}
11+
*/
12+
13+
exports._applyFilter = function (id, args) {
14+
var registry = this.$options.filters
15+
var filter = registry[id]
16+
_.assertAsset(filter, 'filter', id)
17+
return (filter.read || filter).apply(this, args)
18+
}

src/parsers/expression.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,24 @@ exports.parse = function (exp, needSet) {
239239
// but that's too rare and we don't care.
240240
// also skip boolean literals and paths that start with
241241
// global "Math"
242-
var res =
243-
pathTestRE.test(exp) &&
244-
// don't treat true/false as paths
245-
!booleanLiteralRE.test(exp) &&
246-
// Math constants e.g. Math.PI, Math.E etc.
247-
exp.slice(0, 5) !== 'Math.'
248-
? compilePathFns(exp)
249-
: compileExpFns(exp, needSet)
242+
var res = exports.isSimplePath(exp)
243+
? compilePathFns(exp)
244+
: compileExpFns(exp, needSet)
250245
expressionCache.put(exp, res)
251246
return res
252247
}
253248

254-
// Export the pathRegex for external use
255-
exports.pathTestRE = pathTestRE
249+
/**
250+
* Check if an expression is a simple path.
251+
*
252+
* @param {String} exp
253+
* @return {Boolean}
254+
*/
255+
256+
exports.isSimplePath = function (exp) {
257+
return pathTestRE.test(exp) &&
258+
// don't treat true/false as paths
259+
!booleanLiteralRE.test(exp) &&
260+
// Math constants e.g. Math.PI, Math.E etc.
261+
exp.slice(0, 5) !== 'Math.'
262+
}

src/parsers/text.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,7 @@ function formatToken (token, vm, single) {
136136
return token.tag
137137
? vm && token.oneTime
138138
? '"' + vm.$eval(token.value) + '"'
139-
: single
140-
? token.value
141-
: inlineFilters(token.value)
139+
: inlineFilters(token.value, single)
142140
: '"' + token.value + '"'
143141
}
144142

@@ -151,13 +149,16 @@ function formatToken (token, vm, single) {
151149
* to directive parser and watcher mechanism.
152150
*
153151
* @param {String} exp
152+
* @param {Boolean} single
154153
* @return {String}
155154
*/
156155

157156
var filterRE = /[^|]\|[^|]/
158-
function inlineFilters (exp) {
157+
function inlineFilters (exp, single) {
159158
if (!filterRE.test(exp)) {
160-
return '(' + exp + ')'
159+
return single
160+
? exp
161+
: '(' + exp + ')'
161162
} else {
162163
var dir = dirParser.parse(exp)[0]
163164
if (!dir.filters) {
@@ -169,9 +170,7 @@ function inlineFilters (exp) {
169170
var args = filter.args
170171
? ',"' + filter.args.join('","') + '"'
171172
: ''
172-
filter = 'this.$options.filters["' + filter.name + '"]'
173-
exp = '(' + filter + '.read||' + filter + ')' +
174-
'.apply(this,[' + exp + args + '])'
173+
exp = 'this._applyFilter("' + filter.name + '",[' + exp + args + '])'
175174
}
176175
return exp
177176
}

src/vue.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ extend(p, require('./instance/init'))
7070
extend(p, require('./instance/events'))
7171
extend(p, require('./instance/scope'))
7272
extend(p, require('./instance/compile'))
73+
extend(p, require('./instance/misc'))
7374

7475
/**
7576
* Mixin public API methods

test/unit/specs/compiler/compile_spec.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ if (_.inBrowser) {
2929
},
3030
$interpolate: function (value) {
3131
return data[value]
32+
},
33+
$parent: {
34+
_directives: [],
35+
$get: function (v) {
36+
return 'from parent: ' + v
37+
}
3238
}
3339
}
3440
spyOn(vm, '_bindDir').and.callThrough()
@@ -151,27 +157,54 @@ if (_.inBrowser) {
151157
it('param attributes', function () {
152158
var options = merge(Vue.options, {
153159
_asComponent: true,
154-
paramAttributes: ['a', 'data-some-attr', 'some-other-attr', 'invalid', 'camelCase']
160+
paramAttributes: [
161+
'a',
162+
'data-some-attr',
163+
'some-other-attr',
164+
'multiple-attrs',
165+
'onetime',
166+
'with-filter',
167+
'camelCase'
168+
]
155169
})
156170
var def = Vue.options.directives['with']
157171
el.setAttribute('a', '1')
158172
el.setAttribute('data-some-attr', '{{a}}')
159173
el.setAttribute('some-other-attr', '2')
160-
el.setAttribute('invalid', 'a {{b}} c') // invalid
174+
el.setAttribute('multiple-attrs', 'a {{b}} c')
175+
el.setAttribute('onetime', '{{*a}}')
176+
el.setAttribute('with-filter', '{{a | filter}}')
161177
transclude(el, options)
162178
var linker = compile(el, options)
163179
linker(vm, el)
164-
// should skip literal & invliad
165-
expect(vm._bindDir.calls.count()).toBe(1)
180+
// should skip literals and one-time bindings
181+
expect(vm._bindDir.calls.count()).toBe(3)
182+
// data-some-attr
166183
var args = vm._bindDir.calls.argsFor(0)
167184
expect(args[0]).toBe('with')
168185
expect(args[1]).toBe(null)
169186
expect(args[2].arg).toBe('someAttr')
187+
expect(args[2].expression).toBe('a')
188+
expect(args[3]).toBe(def)
189+
// multiple-attrs
190+
args = vm._bindDir.calls.argsFor(1)
191+
expect(args[0]).toBe('with')
192+
expect(args[1]).toBe(null)
193+
expect(args[2].arg).toBe('multipleAttrs')
194+
expect(args[2].expression).toBe('"a "+(b)+" c"')
170195
expect(args[3]).toBe(def)
171-
// invalid and camelCase should've warn
172-
expect(_.warn.calls.count()).toBe(2)
173-
// literal should've called vm.$set
196+
// with-filter
197+
args = vm._bindDir.calls.argsFor(2)
198+
expect(args[0]).toBe('with')
199+
expect(args[1]).toBe(null)
200+
expect(args[2].arg).toBe('withFilter')
201+
expect(args[2].expression).toBe('this._applyFilter("filter",[a])')
202+
expect(args[3]).toBe(def)
203+
// camelCase should've warn
204+
expect(_.warn.calls.count()).toBe(1)
205+
// literal and one time should've called vm.$set
174206
expect(vm.$set).toHaveBeenCalledWith('a', '1')
207+
expect(vm.$set).toHaveBeenCalledWith('onetime', 'from parent: a')
175208
expect(vm.$set).toHaveBeenCalledWith('someOtherAttr', '2')
176209
})
177210

test/unit/specs/instance/misc_spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
var Vue = require('../../../../src/vue')
2+
3+
describe('misc', function () {
4+
5+
it('_applyFilter', function () {
6+
var vm = new Vue({
7+
filters: {
8+
a: {
9+
read: function (a, b) {
10+
return a + b
11+
}
12+
},
13+
b: function (a, b) {
14+
return a - b
15+
}
16+
}
17+
})
18+
expect(vm._applyFilter('a', [1, 1])).toBe(2)
19+
expect(vm._applyFilter('b', [1, 1])).toBe(0)
20+
})
21+
22+
})

test/unit/specs/parsers/text_spec.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ describe('Text Parser', function () {
101101
expect(exp).toBe('"view-"+(test + 1)+"-test-"+(ok + "|")')
102102
})
103103

104+
it('tokens to expression, single expression', function () {
105+
var tokens = textParser.parse('{{test}}')
106+
var exp = textParser.tokensToExp(tokens)
107+
// should not have parens so it can be treated as a
108+
// simple path by the expression parser
109+
expect(exp).toBe('test')
110+
})
111+
104112
it('tokens to expression with oneTime tags & vm', function () {
105113
var vm = new Vue({
106114
data: { test: 'a', ok: 'b' }
@@ -110,16 +118,10 @@ describe('Text Parser', function () {
110118
expect(exp).toBe('"view-"+"a"+"-test-"+(ok)')
111119
})
112120

113-
it('tokens to expression with filters, single expression', function () {
114-
var tokens = textParser.parse('{{test | abc}}')
115-
var exp = textParser.tokensToExp(tokens)
116-
expect(exp).toBe('test | abc')
117-
})
118-
119121
it('tokens to expression with filters, multiple expressions', function () {
120-
var tokens = textParser.parse('a {{b | c d}} e')
122+
var tokens = textParser.parse('a {{b | c d | f}} e')
121123
var exp = textParser.tokensToExp(tokens)
122-
expect(exp).toBe('"a "+(this.$options.filters["c"].read||this.$options.filters["c"]).apply(this,[b,"d"])+" e"')
124+
expect(exp).toBe('"a "+this._applyFilter("f",[this._applyFilter("c",[b,"d"])])+" e"')
123125
})
124126

125127
})

0 commit comments

Comments
 (0)