diff --git a/flow/compiler.js b/flow/compiler.js index 3cf1714c66b..977df14856c 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -41,6 +41,7 @@ declare type ModuleOptions = { } declare type ASTModifiers = { [key: string]: boolean } +declare type ASTIfConditions = Array<{ exp: ?string; block: ASTElement }> declare type ASTElementHandler = { value: string; @@ -95,8 +96,9 @@ declare type ASTElement = { if?: string; ifProcessed?: boolean; + elseif?: string; else?: true; - elseBlock?: ASTElement; + conditions?: ASTIfConditions; for?: string; forProcessed?: boolean; diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index fe57ee5f80d..572634fa801 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -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 { diff --git a/src/compiler/optimizer.js b/src/compiler/optimizer.js index 60c61d9096e..34029ee6ec9 100644 --- a/src/compiler/optimizer.js +++ b/src/compiler/optimizer.js @@ -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 diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index 3b60e99d2e2..6e8a4b57266 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -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( @@ -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' @@ -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) { diff --git a/test/unit/features/directives/if.spec.js b/test/unit/features/directives/if.spec.js index 893c83d35a3..7146ec0e048 100644 --- a/test/unit/features/directives/if.spec.js +++ b/test/unit/features/directives/if.spec.js @@ -88,6 +88,55 @@ describe('Directive v-if', () => { }).then(done) }) + it('should work well with v-elseif', done => { + const vm = new Vue({ + template: ` +
+ hello + elseif + bye +
+ `, + data: { foo: true, bar: false } + }).$mount() + expect(vm.$el.innerHTML.trim()).toBe('hello') + vm.foo = false + waitForUpdate(() => { + expect(vm.$el.innerHTML.trim()).toBe('bye') + vm.bar = true + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('elseif') + vm.bar = false + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('bye') + vm.foo = true + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('hello') + vm.foo = false + vm.bar = {} + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('elseif') + vm.bar = 0 + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('bye') + vm.bar = [] + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('elseif') + vm.bar = null + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('bye') + vm.bar = '0' + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('elseif') + vm.bar = undefined + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('bye') + vm.bar = 1 + }).then(() => { + expect(vm.$el.innerHTML.trim()).toBe('elseif') + }).then(done) + }) + it('should work well with v-for', done => { const vm = new Vue({ template: ` diff --git a/test/unit/features/directives/once.spec.js b/test/unit/features/directives/once.spec.js index 21da307980b..01bb83ca9a4 100644 --- a/test/unit/features/directives/once.spec.js +++ b/test/unit/features/directives/once.spec.js @@ -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: ` +
+
+
+ {{ i.truthy }} + {{ i.text }}elseif + {{ i.text }} +
+
+
+
+ {{ i.truthy }} + {{ i.text }}elseif + {{ i.text }} +
+
+ ` + }).$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: { diff --git a/test/unit/features/directives/style.spec.js b/test/unit/features/directives/style.spec.js index 8a480602a76..beca442de76 100644 --- a/test/unit/features/directives/style.spec.js +++ b/test/unit/features/directives/style.spec.js @@ -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: + '
' + + '
' + + '
' + + '
' + + '
' + + '
', + 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) + }) }) diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js index 989a71f22bd..4a8be0b0b47 100644 --- a/test/unit/modules/compiler/codegen.spec.js +++ b/test/unit/modules/compiler/codegen.spec.js @@ -74,6 +74,27 @@ describe('codegen', () => { ) }) + it('generate v-elseif directive', () => { + assertCodegen( + '

hello

world

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

hello

world

bye

', + `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( + '

hello

world

elseif

bye

', + `with(this){return _h('div',[(show)?_h('p',["hello"]):(hide)?_h('p',["world"]):(3)?_h('p',["elseif"]):_h('p',["bye"])])}` + ) + }) + it('generate ref', () => { assertCodegen( '

', diff --git a/test/unit/modules/compiler/optimizer.spec.js b/test/unit/modules/compiler/optimizer.spec.js index f7b3e08c21b..5fc38251430 100644 --- a/test/unit/modules/compiler/optimizer.spec.js +++ b/test/unit/modules/compiler/optimizer.spec.js @@ -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', () => { @@ -213,14 +213,28 @@ describe('optimizer', () => { it('mark static trees inside v-for with nested v-else and v-once', () => { const ast = parse(`
+
+
+
{{ i }}
+
{{ i }}
+
{{ i }}
+
+
{{ i }}
{{ i }}
-
`, baseOptions) +
+ `, 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) }) }) diff --git a/test/unit/modules/compiler/parser.spec.js b/test/unit/modules/compiler/parser.spec.js index 21a7f26ad46..127e8528ea8 100644 --- a/test/unit/modules/compiler/parser.spec.js +++ b/test/unit/modules/compiler/parser.spec.js @@ -88,6 +88,12 @@ describe('parser', () => { .not.toHaveBeenWarned() }) + it('not warn 3 root elements with v-if, v-elseif and v-else', () => { + parse('
', baseOptions) + expect('Component template should contain exactly one root element') + .not.toHaveBeenWarned() + }) + it('not warn 2 root elements with v-if and v-else on separate lines', () => { parse(`
@@ -97,13 +103,59 @@ describe('parser', () => { .not.toHaveBeenWarned() }) + it('not warn 3 or more root elements with v-if, v-elseif and v-else on separate lines', () => { + parse(` +
+
+
+ `, baseOptions) + expect('Component template should contain exactly one root element') + .not.toHaveBeenWarned() + + parse(` +
+
+
+
+
+ `, baseOptions) + expect('Component template should contain exactly one root element') + .not.toHaveBeenWarned() + }) + it('generate correct ast for 2 root elements with v-if and v-else on separate lines', () => { const ast = parse(`

`, baseOptions) expect(ast.tag).toBe('div') - expect(ast.elseBlock.tag).toBe('p') + expect(ast.conditions[1].block.tag).toBe('p') + }) + + it('generate correct ast for 3 or more root elements with v-if and v-else on separate lines', () => { + const ast = parse(` +
+ +

+ `, baseOptions) + expect(ast.tag).toBe('div') + expect(ast.conditions[0].block.tag).toBe('div') + expect(ast.conditions[1].block.tag).toBe('span') + expect(ast.conditions[2].block.tag).toBe('p') + + const astMore = parse(` +
+ +
+ +

+ `, baseOptions) + expect(astMore.tag).toBe('div') + expect(astMore.conditions[0].block.tag).toBe('div') + expect(astMore.conditions[1].block.tag).toBe('span') + expect(astMore.conditions[2].block.tag).toBe('div') + expect(astMore.conditions[3].block.tag).toBe('span') + expect(astMore.conditions[4].block.tag).toBe('p') }) it('warn 2 root elements with v-if', () => { @@ -118,12 +170,34 @@ describe('parser', () => { .toHaveBeenWarned() }) + it('warn 3 root elements with v-if and v-elseif on first 2', () => { + parse('
', baseOptions) + expect('Component template should contain exactly one root element:\n\n' + + '
') + .toHaveBeenWarned() + }) + + it('warn 4 root elements with v-if, v-elseif and v-else on first 2', () => { + parse('
', baseOptions) + expect('Component template should contain exactly one root element:\n\n' + + '
') + .toHaveBeenWarned() + }) + it('warn 2 root elements with v-if and v-else with v-for on 2nd', () => { parse('
', baseOptions) - expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n
') + expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n' + + '
') .toHaveBeenWarned() }) + it('warn 2 root elements with v-if and v-elseif with v-for on 2nd', () => { + parse('
', baseOptions) + expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n' + + '
') + .toHaveBeenWarned() + }) + it('warn