Skip to content

Commit 39a755f

Browse files
committed
feat($compiler): add support of v-local directive to provide a way to set local variable in template
fix #6913
1 parent f8cb3a2 commit 39a755f

File tree

6 files changed

+272
-2
lines changed

6 files changed

+272
-2
lines changed

flow/compiler.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ declare type ASTElement = {
114114
else?: true;
115115
ifConditions?: ASTIfConditions;
116116

117+
locals?: Array<{ alias: string; value: string }>;
118+
localsProcessed?: boolean;
119+
117120
for?: string;
118121
forProcessed?: boolean;
119122
key?: string;

src/compiler/codegen/index.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function genElement (el: ASTElement, state: CodegenState): string {
5656
return genOnce(el, state)
5757
} else if (el.for && !el.forProcessed) {
5858
return genFor(el, state)
59+
} else if (el.locals && !el.localsProcessed) {
60+
return genLocalVariable(el, state)
5961
} else if (el.if && !el.ifProcessed) {
6062
return genIf(el, state)
6163
} else if (el.tag === 'template' && !el.slotTarget) {
@@ -164,6 +166,20 @@ function genIfConditions (
164166
}
165167
}
166168

169+
function genLocalVariable (el: ASTElement, state: CodegenState): string {
170+
const locals = el.locals || []
171+
const localNames: Array<string> = []
172+
const localValues: Array<string> = []
173+
for (let i = 0, l = locals.length; i < l; ++i) {
174+
localNames.push(locals[i].alias)
175+
localValues.push(locals[i].value)
176+
}
177+
el.localsProcessed = true
178+
return `((function(${localNames.join(',')}){` +
179+
`return ${genElement(el, state)}` +
180+
`})(${localValues.join(',')}))`
181+
}
182+
167183
export function genFor (
168184
el: any,
169185
state: CodegenState,
@@ -351,7 +367,9 @@ function genScopedSlot (
351367
`return ${el.tag === 'template'
352368
? el.if
353369
? `${el.if}?${genChildren(el, state) || 'undefined'}:undefined`
354-
: genChildren(el, state) || 'undefined'
370+
: el.locals && !el.localsProcessed
371+
? genLocalVariableScopeSlot(el, state)
372+
: genChildren(el, state) || 'undefined'
355373
: genElement(el, state)
356374
}}`
357375
return `{key:${key},fn:${fn}}`
@@ -373,6 +391,24 @@ function genForScopedSlot (
373391
'})'
374392
}
375393

394+
function genLocalVariableScopeSlot (
395+
el: ASTElement,
396+
state: CodegenState
397+
): string {
398+
const locals = el.locals || []
399+
const localNames: Array<string> = []
400+
const localValues: Array<string> = []
401+
for (let i = 0, l = locals.length; i < l; ++i) {
402+
localNames.push(locals[i].alias)
403+
localValues.push(locals[i].value)
404+
}
405+
406+
el.localsProcessed = true
407+
return `((function(${localNames.join(',')}){` +
408+
`return ${genChildren(el, state) || 'undefined'}` +
409+
`})(${localValues.join(',')}))`
410+
}
411+
376412
export function genChildren (
377413
el: ASTElement,
378414
state: CodegenState,

src/compiler/helpers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export function addRawAttr (el: ASTElement, name: string, value: any) {
3232
el.attrsList.push({ name, value })
3333
}
3434

35+
export function addLocalVariable (el: ASTElement, alias: string, value: string) {
36+
(el.locals || (el.locals = [])).push({ alias, value })
37+
}
38+
3539
export function addDirective (
3640
el: ASTElement,
3741
name: string,

src/compiler/parser/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import {
1616
addDirective,
1717
getBindingAttr,
1818
getAndRemoveAttr,
19-
pluckModuleFunction
19+
pluckModuleFunction,
20+
addLocalVariable
2021
} from '../helpers'
2122

2223
export const onRE = /^@|^v-on:/
24+
export const localRE = /^v-local:/
2325
export const dirRE = /^v-|^@|^:/
2426
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
2527
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
@@ -552,6 +554,9 @@ function processAttrs (el) {
552554
} else {
553555
addAttr(el, name, value)
554556
}
557+
} else if (localRE.test(name)) {
558+
name = name.replace(localRE, '')
559+
addLocalVariable(el, name, value)
555560
} else if (onRE.test(name)) { // v-on
556561
name = name.replace(onRE, '')
557562
addHandler(el, name, value, modifiers, false, warn)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import Vue from 'vue'
2+
3+
describe('Directive v-local', () => {
4+
it('should work', () => {
5+
const vm = new Vue({
6+
template: '<div><span v-local:v="foo.bar">{{v.baz}}</span></div>',
7+
data: {
8+
foo: {
9+
bar: {
10+
baz: 1
11+
}
12+
}
13+
}
14+
}).$mount()
15+
expect(vm.$el.innerHTML).toBe('<span>1</span>')
16+
})
17+
18+
it('should update', done => {
19+
const vm = new Vue({
20+
template: '<div><span v-local:v="foo.bar">{{v.baz}}</span></div>',
21+
data: {
22+
foo: {
23+
bar: {
24+
baz: 1
25+
}
26+
}
27+
}
28+
}).$mount()
29+
expect(vm.$el.innerHTML).toBe('<span>1</span>')
30+
vm.foo.bar.baz = 2
31+
waitForUpdate(() => {
32+
expect(vm.$el.innerHTML).toBe('<span>2</span>')
33+
vm.foo.bar.baz = 'a'
34+
}).then(() => {
35+
expect(vm.$el.innerHTML).toBe('<span>a</span>')
36+
}).then(done)
37+
})
38+
39+
it('should work with v-if', done => {
40+
const vm = new Vue({
41+
template: '<div><span v-if="v.show" v-local:v="foo.bar">{{v.baz}}</span></div>',
42+
data: {
43+
foo: {
44+
bar: {
45+
baz: 1,
46+
show: true
47+
}
48+
}
49+
}
50+
}).$mount()
51+
expect(vm.$el.innerHTML).toBe('<span>1</span>')
52+
vm.foo.bar.show = false
53+
waitForUpdate(() => {
54+
expect(vm.$el.innerHTML).toBe('<!---->')
55+
vm.foo.bar.baz = 2
56+
vm.foo.bar.show = true
57+
}).then(() => {
58+
expect(vm.$el.innerHTML).toBe('<span>2</span>')
59+
}).then(done)
60+
})
61+
62+
it('should work with v-for', done => {
63+
const vm = new Vue({
64+
template: '<div><span v-for="item in items" v-local:v="item.foo.bar">{{v.baz}}</span></div>',
65+
data: {
66+
items: [
67+
{
68+
foo: {
69+
bar: {
70+
baz: 1
71+
}
72+
}
73+
}
74+
]
75+
}
76+
}).$mount()
77+
expect(vm.$el.innerHTML).toBe('<span>1</span>')
78+
vm.items.push({
79+
foo: {
80+
bar: {
81+
baz: 2
82+
}
83+
}
84+
})
85+
waitForUpdate(() => {
86+
expect(vm.$el.innerHTML).toBe('<span>1</span><span>2</span>')
87+
vm.items.shift()
88+
}).then(() => {
89+
expect(vm.$el.innerHTML).toBe('<span>2</span>')
90+
vm.items = []
91+
}).then(() => {
92+
expect(vm.$el.innerHTML).toBe('')
93+
}).then(done)
94+
})
95+
96+
it('should been called once', () => {
97+
const spy = jasmine.createSpy()
98+
const vm = new Vue({
99+
template: '<div><span v-local:v="getValue()">{{v.foo}}-{{v.bar}}</span></div>',
100+
data: {
101+
foo: {
102+
bar: {
103+
baz: 1
104+
}
105+
}
106+
},
107+
methods: {
108+
getValue () {
109+
spy()
110+
return {
111+
foo: this.foo.bar.baz + 1,
112+
bar: this.foo.bar.baz + 2
113+
}
114+
}
115+
}
116+
}).$mount()
117+
expect(vm.$el.innerHTML).toBe('<span>2-3</span>')
118+
expect(spy).toHaveBeenCalledTimes(1)
119+
})
120+
121+
it('should work with scoped-slot', done => {
122+
const vm = new Vue({
123+
template: `
124+
<test ref="test">
125+
<template slot="item" slot-scope="props" v-local:v="props.text.foo.bar.baz">
126+
<span>{{ v }}</span>
127+
</template>
128+
</test>
129+
`,
130+
components: {
131+
test: {
132+
data () {
133+
return {
134+
items: [{
135+
foo: {
136+
bar: {
137+
baz: 1
138+
}
139+
}
140+
}]
141+
}
142+
},
143+
template: `
144+
<div>
145+
<slot v-for="item in items" name="item" :text="item"></slot>
146+
</div>
147+
`
148+
}
149+
}
150+
}).$mount()
151+
expect(vm.$el.innerHTML).toBe('<span>1</span>')
152+
vm.$refs.test.items.push({
153+
foo: {
154+
bar: {
155+
baz: 2
156+
}
157+
}
158+
})
159+
waitForUpdate(() => {
160+
expect(vm.$el.innerHTML).toBe('<span>1</span><span>2</span>')
161+
vm.$refs.test.items = []
162+
}).then(() => {
163+
expect(vm.$el.innerHTML).toBe('')
164+
}).then(done)
165+
})
166+
})

test/unit/modules/compiler/codegen.spec.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,62 @@ describe('codegen', () => {
7474
)
7575
})
7676

77+
it('generate v-local directive', () => {
78+
assertCodegen(
79+
'<div v-local:value="some.deep.prop">{{value}}</div>',
80+
`with(this){return ((function(value){return _c('div',{},[_v(_s(value))])})(some.deep.prop))}`
81+
)
82+
})
83+
84+
it('generate multi v-local directive', () => {
85+
assertCodegen(
86+
'<div v-local:foo="some.deep.prop" v-local:bar="other.deep.prop">{{foo}}{{bar}}</div>',
87+
`with(this){return ((function(foo,bar){return _c('div',{},[_v(_s(foo)+_s(bar))])})(some.deep.prop,other.deep.prop))}`
88+
)
89+
})
90+
91+
it('generate v-local directive with v-if', () => {
92+
assertCodegen(
93+
'<div v-local:foo="some.deep.prop" v-if="foo.show">{{foo.value}}</div>',
94+
`with(this){return ((function(foo){return (foo.show)?_c('div',{},[_v(_s(foo.value))]):_e()})(some.deep.prop))}`
95+
)
96+
})
97+
98+
it('generate v-local directive with v-for', () => {
99+
assertCodegen(
100+
'<div><span v-for="item in items" v-local:foo="item.deep.prop">{{foo}}</span></div>',
101+
`with(this){return _c('div',_l((items),function(item){return ((function(foo){return _c('span',{},[_v(_s(foo))])})(item.deep.prop))}))}`
102+
)
103+
})
104+
105+
it('generate v-local directive with other v-local', () => {
106+
assertCodegen(
107+
'<div v-local:foo="some.deep.prop"><span v-local:bar="foo.deep.prop">{{bar}}</span></div>',
108+
`with(this){return ((function(foo){return _c('div',{},[((function(bar){return _c('span',{},[_v(_s(bar))])})(foo.deep.prop))])})(some.deep.prop))}`
109+
)
110+
})
111+
112+
it('generate v-local directive with scoped-slot', () => {
113+
assertCodegen(
114+
'<foo><div v-local:baz="bar.deep.prop" slot-scope="bar">{{baz}}</div></foo>',
115+
`with(this){return _c('foo',{scopedSlots:_u([{key:"default",fn:function(bar){return ((function(baz){return _c('div',{},[_v(_s(baz))])})(bar.deep.prop))}}])})}`
116+
)
117+
})
118+
119+
it('generate v-local directive with template tag', () => {
120+
assertCodegen(
121+
'<div><template v-local:v="some.deep.prop"><span>{{ v }}</span></template></div>',
122+
`with(this){return _c('div',[((function(v){return [_c('span',[_v(_s(v))])]})(some.deep.prop))],2)}`
123+
)
124+
})
125+
126+
it('generate v-local directive with scoped-slot and template tag', () => {
127+
assertCodegen(
128+
'<test><template slot="item" slot-scope="props" v-local:v="props.text"><span>{{ v }}</span></template></test>',
129+
`with(this){return _c('test',{scopedSlots:_u([{key:"item",fn:function(props){return ((function(v){return [_c('span',[_v(_s(v))])]})(props.text))}}])})}`
130+
)
131+
})
132+
77133
it('generate v-if directive', () => {
78134
assertCodegen(
79135
'<p v-if="show">hello</p>',

0 commit comments

Comments
 (0)