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

Support v-if multiple conditions #4271

Merged
merged 15 commits into from
Nov 21, 2016
4 changes: 3 additions & 1 deletion flow/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ declare type ModuleOptions = {
}

declare type ASTModifiers = { [key: string]: boolean }
declare type ASTIfConditions = Array<{ exp: ?string; block: ASTElement }>

Choose a reason for hiding this comment

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

what's this doing


declare type ASTElementHandler = {
value: string;
Expand Down Expand Up @@ -95,8 +96,9 @@ declare type ASTElement = {

if?: string;
ifProcessed?: boolean;
elseif?: string;
else?: true;
elseBlock?: ASTElement;
conditions?: ASTIfConditions;

Choose a reason for hiding this comment

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

and this


for?: string;
forProcessed?: boolean;
Expand Down
24 changes: 17 additions & 7 deletions src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,27 @@ function genOnce (el: ASTElement): string {
}
}

// v-if with v-once shuold generate code like (a)?_m(0):_m(1)
function genIf (el: any): string {
const exp = el.if
el.ifProcessed = true // avoid recursion
return `(${exp})?${el.once ? genOnce(el) : genElement(el)}:${genElse(el)}`
return genIfConditions(el.conditions)
}

function genElse (el: ASTElement): string {
return el.elseBlock
? genElement(el.elseBlock)
: '_e()'
function genIfConditions (conditions: ASTIfConditions): string {
if (!conditions.length) {
return '_e()'
}

var condition = conditions.shift()
if (condition.exp) {
return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}`
} else {
return `${genTernaryExp(condition.block)}`
}

// v-if with v-once shuold generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return el.once ? genOnce(el) : genElement(el)
}
}

function genFor (el: any): string {
Expand Down
10 changes: 8 additions & 2 deletions src/compiler/optimizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ function markStaticRoots (node: ASTNode, isInFor: boolean) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.elseBlock) {
markStaticRoots(node.elseBlock, isInFor)
if (node.conditions) {
walkThroughConditionsBlocks(node.conditions, isInFor)
}
}
}

function walkThroughConditionsBlocks (conditionBlocks: ASTIfConditions, isInFor: boolean): void {
for (let i = 1, len = conditionBlocks.length; i < len; i++) {
markStaticRoots(conditionBlocks[i].block, isInFor)
}
}

function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
Expand Down
45 changes: 34 additions & 11 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,13 @@ export function parse (
root = element
checkRootConstraints(root)
} else if (!stack.length) {
// allow 2 root elements with v-if and v-else
if (root.if && element.else) {
// allow root elements with v-if, v-elseif and v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element)
root.elseBlock = element
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production' && !warned) {
warned = true
warn(
Expand All @@ -166,8 +169,8 @@ export function parse (
}
}
if (currentParent && !element.forbidden) {
if (element.else) { // else block
processElse(element, currentParent)
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || 'default'
Expand Down Expand Up @@ -316,23 +319,43 @@ function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
}
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-elseif')
if (elseif) {
el.elseif = elseif
}
}
}

function processElse (el, parent) {
function processIfConditions (el, parent) {
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
prev.elseBlock = el
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if (process.env.NODE_ENV !== 'production') {
warn(
`v-else used on element <${el.tag}> without corresponding v-if.`
`v-${el.elseif ? ('elseif="' + el.elseif + '"') : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`
)
}
}

function addIfCondition (el, condition) {
if (!el.conditions) {
el.conditions = []
}
el.conditions.push(condition)
}

function processOnce (el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
Expand Down
49 changes: 49 additions & 0 deletions test/unit/features/directives/if.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,55 @@ describe('Directive v-if', () => {
}).then(done)
})

it('should work well with v-elseif', done => {
const vm = new Vue({
template: `
<div>
<span v-if="foo">hello</span>
<span v-elseif="bar">elseif</span>
<span v-else>bye</span>
</div>
`,
data: { foo: true, bar: false }
}).$mount()
expect(vm.$el.innerHTML.trim()).toBe('<span>hello</span>')
vm.foo = false
waitForUpdate(() => {

Choose a reason for hiding this comment

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

what's the waitForUpdate magic

expect(vm.$el.innerHTML.trim()).toBe('<span>bye</span>')
vm.bar = true
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
vm.bar = false

Choose a reason for hiding this comment

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

if it wants a then chaining,

+      expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
+      vm.bar = false

should return a promise,
but I see no promise T.T

Choose a reason for hiding this comment

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

@fnlctrl help wanted XD

}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>bye</span>')
vm.foo = true
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>hello</span>')
vm.foo = false
vm.bar = {}
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
vm.bar = 0
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>bye</span>')
vm.bar = []

Choose a reason for hiding this comment

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

why would it be necessary for so many kind of vm.bar @fnlctrl

}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
vm.bar = null
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>bye</span>')
vm.bar = '0'
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
vm.bar = undefined
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>bye</span>')
vm.bar = 1
}).then(() => {
expect(vm.$el.innerHTML.trim()).toBe('<span>elseif</span>')
}).then(done)
})

it('should work well with v-for', done => {
const vm = new Vue({
template: `
Expand Down
50 changes: 50 additions & 0 deletions test/unit/features/directives/once.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,56 @@ describe('Directive v-once', () => {
}).then(done)
})

it('should work inside v-for with nested v-elseif and v-else', done => {
const vm = new Vue({
data: {
tester: false,
list: [{ id: 0, text: 'a', tester: true, truthy: 'y' }]
},
template: `
<div v-if="0"></div>
<div v-elseif="tester">
<div v-for="i in list" :key="i.id">
<span v-if="i.tester" v-once>{{ i.truthy }}</span>
<span v-elseif="tester" v-once>{{ i.text }}elseif</span>
<span v-else v-once>{{ i.text }}</span>
</div>
</div>
<div v-else>
<div v-for="i in list" :key="i.id">
<span v-if="i.tester" v-once>{{ i.truthy }}</span>
<span v-elseif="tester">{{ i.text }}elseif</span>
<span v-else v-once>{{ i.text }}</span>
</div>
</div>
`
}).$mount()

expectTextContent(vm, 'y')
vm.list[0].truthy = 'yy'
waitForUpdate(() => {
expectTextContent(vm, 'y')
vm.list[0].tester = false
}).then(() => {
expectTextContent(vm, 'a')
vm.list[0].text = 'nn'
}).then(() => {
expectTextContent(vm, 'a')
vm.tester = true
}).then(() => {
expectTextContent(vm, 'nnelseif')
vm.list[0].text = 'xx'
}).then(() => {
expectTextContent(vm, 'nnelseif')
vm.list[0].tester = true
}).then(() => {
expectTextContent(vm, 'yy')
vm.list[0].truthy = 'nn'
}).then(() => {
expectTextContent(vm, 'yy')
}).then(done)
})

it('should warn inside non-keyed v-for', () => {
const vm = new Vue({
data: {
Expand Down
35 changes: 35 additions & 0 deletions test/unit/features/directives/style.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,39 @@ describe('Directive v-bind:style', () => {
expect(style.marginTop).toBe('12px')
}).then(done)
})

it('should not merge for v-if, v-elseif and v-else elements', (done) => {
const vm = new Vue({
template:
'<div>' +
'<section style="color: blue" :style="style" v-if="foo"></section>' +
'<section style="margin-top: 12px" v-elseif="bar"></section>' +
'<section style="margin-bottom: 24px" v-else></section>' +
'<div></div>' +
'</div>',
data: {
foo: true,
bar: false,
style: {
fontSize: '12px'
}
}
}).$mount()
const style = vm.$el.children[0].style
expect(style.fontSize).toBe('12px')
expect(style.color).toBe('blue')
waitForUpdate(() => {
vm.foo = false
}).then(() => {
expect(style.color).toBe('')
expect(style.fontSize).toBe('')
expect(style.marginBottom).toBe('24px')
vm.bar = true
}).then(() => {
expect(style.color).toBe('')
expect(style.fontSize).toBe('')
expect(style.marginBottom).toBe('')
expect(style.marginTop).toBe('12px')
}).then(done)
})
})
21 changes: 21 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,27 @@ describe('codegen', () => {
)
})

it('generate v-elseif directive', () => {
assertCodegen(
'<div><p v-if="show">hello</p><p v-elseif="hide">world</p></div>',
`with(this){return _h('div',[(show)?_h('p',["hello"]):(hide)?_h('p',["world"]):_e()])}`
)
})

it('generate v-elseif with v-else directive', () => {
assertCodegen(
'<div><p v-if="show">hello</p><p v-elseif="hide">world</p><p v-else>bye</p></div>',
`with(this){return _h('div',[(show)?_h('p',["hello"]):(hide)?_h('p',["world"]):_h('p',["bye"])])}`
)
})

it('generate mutli v-elseif with v-else directive', () => {
assertCodegen(
'<div><p v-if="show">hello</p><p v-elseif="hide">world</p><p v-elseif="3">elseif</p><p v-else>bye</p></div>',
`with(this){return _h('div',[(show)?_h('p',["hello"]):(hide)?_h('p',["world"]):(3)?_h('p',["elseif"]):_h('p',["bye"])])}`
)
})

it('generate ref', () => {
assertCodegen(
'<p ref="component1"></p>',
Expand Down
22 changes: 18 additions & 4 deletions test/unit/modules/compiler/optimizer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('optimizer', () => {
optimize(ast, baseOptions)
expect(ast.static).toBe(false)
expect(ast.children[0].static).toBe(false)
expect(ast.children[0].elseBlock.static).toBeUndefined()
expect(ast.children[0].conditions[1].block.static).toBeUndefined()
})

it('v-pre directive', () => {
Expand Down Expand Up @@ -213,14 +213,28 @@ describe('optimizer', () => {
it('mark static trees inside v-for with nested v-else and v-once', () => {
const ast = parse(`
<div v-if="1"></div>
<div v-elseif="2">
<div v-for="i in 10" :key="i">
<div v-if="1">{{ i }}</div>
<div v-elseif="2" v-once>{{ i }}</div>
<div v-else v-once>{{ i }}</div>
</div>
</div>
<div v-else>
<div v-for="i in 10" :key="i">
<div v-if="1">{{ i }}</div>
<div v-else v-once>{{ i }}</div>
</div>
<div>`, baseOptions)
</div>
`, baseOptions)
optimize(ast, baseOptions)
expect(ast.elseBlock.children[0].children[0].elseBlock.staticRoot).toBe(false)
expect(ast.elseBlock.children[0].children[0].elseBlock.staticInFor).toBe(true)
expect(ast.conditions[1].block.children[0].children[0].conditions[1].block.staticRoot).toBe(false)
expect(ast.conditions[1].block.children[0].children[0].conditions[1].block.staticInFor).toBe(true)

expect(ast.conditions[1].block.children[0].children[0].conditions[2].block.staticRoot).toBe(false)
expect(ast.conditions[1].block.children[0].children[0].conditions[2].block.staticInFor).toBe(true)

expect(ast.conditions[2].block.children[0].children[0].conditions[1].block.staticRoot).toBe(false)
expect(ast.conditions[2].block.children[0].children[0].conditions[1].block.staticInFor).toBe(true)
})
})