Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: fix #6913, feat($compiler): add support of v-local directive to provide a way to… #7325

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions flow/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ declare type ASTElement = {
else?: true;
ifConditions?: ASTIfConditions;

locals?: Array<{ alias: string; value: string }>;
localsProcessed?: boolean;

for?: string;
forProcessed?: boolean;
key?: string;
Expand Down
38 changes: 37 additions & 1 deletion src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export function genElement (el: ASTElement, state: CodegenState): string {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.locals && !el.localsProcessed) {
return genLocalVariable(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
Expand Down Expand Up @@ -164,6 +166,20 @@ function genIfConditions (
}
}

function genLocalVariable (el: ASTElement, state: CodegenState): string {
const locals = el.locals || []
const localNames: Array<string> = []
const localValues: Array<string> = []
for (let i = 0, l = locals.length; i < l; ++i) {
localNames.push(locals[i].alias)
localValues.push(locals[i].value)
}
el.localsProcessed = true
return `((function(${localNames.join(',')}){` +
`return ${genElement(el, state)}` +
`})(${localValues.join(',')}))`
}

export function genFor (
el: any,
state: CodegenState,
Expand Down Expand Up @@ -351,7 +367,9 @@ function genScopedSlot (
`return ${el.tag === 'template'
? el.if
? `${el.if}?${genChildren(el, state) || 'undefined'}:undefined`
: genChildren(el, state) || 'undefined'
: el.locals && !el.localsProcessed
? genLocalVariableScopeSlot(el, state)
: genChildren(el, state) || 'undefined'
: genElement(el, state)
}}`
return `{key:${key},fn:${fn}}`
Expand All @@ -373,6 +391,24 @@ function genForScopedSlot (
'})'
}

function genLocalVariableScopeSlot (
el: ASTElement,
state: CodegenState
): string {
const locals = el.locals || []
const localNames: Array<string> = []
const localValues: Array<string> = []
for (let i = 0, l = locals.length; i < l; ++i) {
localNames.push(locals[i].alias)
localValues.push(locals[i].value)
}

el.localsProcessed = true
return `((function(${localNames.join(',')}){` +
`return ${genChildren(el, state) || 'undefined'}` +
`})(${localValues.join(',')}))`
}

export function genChildren (
el: ASTElement,
state: CodegenState,
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function addRawAttr (el: ASTElement, name: string, value: any) {
el.attrsList.push({ name, value })
}

export function addLocalVariable (el: ASTElement, alias: string, value: string) {
(el.locals || (el.locals = [])).push({ alias, value })
}

export function addDirective (
el: ASTElement,
name: string,
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import {
addDirective,
getBindingAttr,
getAndRemoveAttr,
pluckModuleFunction
pluckModuleFunction,
addLocalVariable
} from '../helpers'

export const onRE = /^@|^v-on:/
export const localRE = /^v-local:/
export const dirRE = /^v-|^@|^:/
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
Expand Down Expand Up @@ -552,6 +554,9 @@ function processAttrs (el) {
} else {
addAttr(el, name, value)
}
} else if (localRE.test(name)) {
name = name.replace(localRE, '')
addLocalVariable(el, name, value)
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn)
Expand Down
166 changes: 166 additions & 0 deletions test/unit/features/directives/local.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import Vue from 'vue'

describe('Directive v-local', () => {
it('should work', () => {
const vm = new Vue({
template: '<div><span v-local:v="foo.bar">{{v.baz}}</span></div>',
data: {
foo: {
bar: {
baz: 1
}
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>1</span>')
})

it('should update', done => {
const vm = new Vue({
template: '<div><span v-local:v="foo.bar">{{v.baz}}</span></div>',
data: {
foo: {
bar: {
baz: 1
}
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>1</span>')
vm.foo.bar.baz = 2
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('<span>2</span>')
vm.foo.bar.baz = 'a'
}).then(() => {
expect(vm.$el.innerHTML).toBe('<span>a</span>')
}).then(done)
})

it('should work with v-if', done => {
const vm = new Vue({
template: '<div><span v-if="v.show" v-local:v="foo.bar">{{v.baz}}</span></div>',
data: {
foo: {
bar: {
baz: 1,
show: true
}
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>1</span>')
vm.foo.bar.show = false
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('<!---->')
vm.foo.bar.baz = 2
vm.foo.bar.show = true
}).then(() => {
expect(vm.$el.innerHTML).toBe('<span>2</span>')
}).then(done)
})

it('should work with v-for', done => {
const vm = new Vue({
template: '<div><span v-for="item in items" v-local:v="item.foo.bar">{{v.baz}}</span></div>',
data: {
items: [
{
foo: {
bar: {
baz: 1
}
}
}
]
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>1</span>')
vm.items.push({
foo: {
bar: {
baz: 2
}
}
})
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('<span>1</span><span>2</span>')
vm.items.shift()
}).then(() => {
expect(vm.$el.innerHTML).toBe('<span>2</span>')
vm.items = []
}).then(() => {
expect(vm.$el.innerHTML).toBe('')
}).then(done)
})

it('should been called once', () => {
const spy = jasmine.createSpy()
const vm = new Vue({
template: '<div><span v-local:v="getValue()">{{v.foo}}-{{v.bar}}</span></div>',
data: {
foo: {
bar: {
baz: 1
}
}
},
methods: {
getValue () {
spy()
return {
foo: this.foo.bar.baz + 1,
bar: this.foo.bar.baz + 2
}
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>2-3</span>')
expect(spy).toHaveBeenCalledTimes(1)
})

it('should work with scoped-slot', done => {
const vm = new Vue({
template: `
<test ref="test">
<template slot="item" slot-scope="props" v-local:v="props.text.foo.bar.baz">
<span>{{ v }}</span>
</template>
</test>
`,
components: {
test: {
data () {
return {
items: [{
foo: {
bar: {
baz: 1
}
}
}]
}
},
template: `
<div>
<slot v-for="item in items" name="item" :text="item"></slot>
</div>
`
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>1</span>')
vm.$refs.test.items.push({
foo: {
bar: {
baz: 2
}
}
})
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('<span>1</span><span>2</span>')
vm.$refs.test.items = []
}).then(() => {
expect(vm.$el.innerHTML).toBe('')
}).then(done)
})
})
56 changes: 56 additions & 0 deletions test/unit/modules/compiler/codegen.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,62 @@ describe('codegen', () => {
)
})

it('generate v-local directive', () => {
assertCodegen(
'<div v-local:value="some.deep.prop">{{value}}</div>',
`with(this){return ((function(value){return _c('div',{},[_v(_s(value))])})(some.deep.prop))}`
)
})

it('generate multi v-local directive', () => {
assertCodegen(
'<div v-local:foo="some.deep.prop" v-local:bar="other.deep.prop">{{foo}}{{bar}}</div>',
`with(this){return ((function(foo,bar){return _c('div',{},[_v(_s(foo)+_s(bar))])})(some.deep.prop,other.deep.prop))}`
)
})

it('generate v-local directive with v-if', () => {
assertCodegen(
'<div v-local:foo="some.deep.prop" v-if="foo.show">{{foo.value}}</div>',
`with(this){return ((function(foo){return (foo.show)?_c('div',{},[_v(_s(foo.value))]):_e()})(some.deep.prop))}`
)
})

it('generate v-local directive with v-for', () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need another test for combining v-for, scoped slot and v-local together as scoped vFor is handled separately IIRC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated,

  1. add some test for scoped slot
  2. fix invalid v-local in scoped-slot with template tag

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some deeper thoughts, v-local might be harder than I originally estimated. The root problem is it adds a new way to introduce new variable in template. After this, we will have three way to introduce new variables:

  1. v-for
  2. slot-scope
  3. v-local

With 1 and 2 we already have some ordering issue, #6817. I would say more variable introduction isn't scalable in current implementation. For example, <div v-local:items="foo.deep.prop" v-for="item in items"> is still missing in test. And supporting such usage will require yet another patch in genFor.

assertCodegen(
'<div><span v-for="item in items" v-local:foo="item.deep.prop">{{foo}}</span></div>',
`with(this){return _c('div',_l((items),function(item){return ((function(foo){return _c('span',{},[_v(_s(foo))])})(item.deep.prop))}))}`
)
})

it('generate v-local directive with other v-local', () => {
assertCodegen(
'<div v-local:foo="some.deep.prop"><span v-local:bar="foo.deep.prop">{{bar}}</span></div>',
`with(this){return ((function(foo){return _c('div',{},[((function(bar){return _c('span',{},[_v(_s(bar))])})(foo.deep.prop))])})(some.deep.prop))}`
)
})

it('generate v-local directive with scoped-slot', () => {
assertCodegen(
'<foo><div v-local:baz="bar.deep.prop" slot-scope="bar">{{baz}}</div></foo>',
`with(this){return _c('foo',{scopedSlots:_u([{key:"default",fn:function(bar){return ((function(baz){return _c('div',{},[_v(_s(baz))])})(bar.deep.prop))}}])})}`
)
})

it('generate v-local directive with template tag', () => {
assertCodegen(
'<div><template v-local:v="some.deep.prop"><span>{{ v }}</span></template></div>',
`with(this){return _c('div',[((function(v){return [_c('span',[_v(_s(v))])]})(some.deep.prop))],2)}`
)
})

it('generate v-local directive with scoped-slot and template tag', () => {
assertCodegen(
'<test><template slot="item" slot-scope="props" v-local:v="props.text"><span>{{ v }}</span></template></test>',
`with(this){return _c('test',{scopedSlots:_u([{key:"item",fn:function(props){return ((function(v){return [_c('span',[_v(_s(v))])]})(props.text))}}])})}`
)
})

it('generate v-if directive', () => {
assertCodegen(
'<p v-if="show">hello</p>',
Expand Down