Просмотр исходного кода

Support v-if multiple conditions (#4271)

* add if conditions

* update v-if conditional

* update test

* update test case

* add test case

* update if conditions

* update walkThroughConditionsBlocks

* update v-elseif

* update v-once with v-elseif test case

* update style with v-elseif

* update flow type
chengchao 9 лет назад
Родитель
Сommit
af78bcf916

+ 3 - 1
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;

+ 17 - 7
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 {

+ 8 - 2
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

+ 34 - 11
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) {

+ 49 - 0
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: `
+        <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(() => {
+      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
+    }).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 = []
+    }).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: `

+ 50 - 0
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: `
+        <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: {

+ 35 - 0
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:
+        '<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 - 0
test/unit/modules/compiler/codegen.spec.js

@@ -83,6 +83,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>',

+ 18 - 4
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(`
       <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)
   })
 })

+ 97 - 6
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('<div v-if="1"></div><div v-elseif="2"></div><div v-else></div>', 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(`
       <div v-if="1"></div>
@@ -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(`
+      <div v-if="1"></div>
+      <div v-elseif="2"></div>
+      <div v-else></div>
+    `, baseOptions)
+    expect('Component template should contain exactly one root element')
+        .not.toHaveBeenWarned()
+
+    parse(`
+      <div v-if="1"></div>
+      <div v-elseif="2"></div>
+      <div v-elseif="3"></div>
+      <div v-elseif="4"></div>
+      <div v-else></div>
+    `, 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(`
       <div v-if="1"></div>
       <p v-else></p>
     `, 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(`
+      <div v-if="1"></div>
+      <span v-elseif="2"></span>
+      <p v-else></p>
+    `, 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(`
+      <div v-if="1"></div>
+      <span v-elseif="2"></span>
+      <div v-elseif="3"></div>
+      <span v-elseif="4"></span>
+      <p v-else></p>
+    `, 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('<div v-if="1"></div><div v-elseif></div><div></div>', baseOptions)
+    expect('Component template should contain exactly one root element:\n\n' +
+        '<div v-if="1"></div><div v-elseif></div><div></div>')
+        .toHaveBeenWarned()
+  })
+
+  it('warn 4 root elements with v-if, v-elseif and v-else on first 2', () => {
+    parse('<div v-if="1"></div><div v-elseif></div><div v-else></div><div></div>', baseOptions)
+    expect('Component template should contain exactly one root element:\n\n' +
+        '<div v-if="1"></div><div v-elseif></div><div v-else></div><div></div>')
+        .toHaveBeenWarned()
+  })
+
   it('warn 2 root elements with v-if and v-else with v-for on 2nd', () => {
     parse('<div v-if="1"></div><div v-else v-for="i in [1]"></div>', baseOptions)
-    expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n<div v-if="1"></div><div v-else v-for="i in [1]"></div>')
+    expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n' +
+        '<div v-if="1"></div><div v-else v-for="i in [1]"></div>')
       .toHaveBeenWarned()
   })
 
+  it('warn 2 root elements with v-if and v-elseif with v-for on 2nd', () => {
+    parse('<div v-if="1"></div><div v-elseif="2" v-for="i in [1]"></div>', baseOptions)
+    expect('Cannot use v-for on stateful component root element because it renders multiple elements:\n' +
+        '<div v-if="1"></div><div v-elseif="2" v-for="i in [1]"></div>')
+        .toHaveBeenWarned()
+  })
+
   it('warn <template> as root element', () => {
     parse('<template></template>', baseOptions)
     expect('Cannot use <template> as component root element').toHaveBeenWarned()
@@ -193,15 +267,32 @@ describe('parser', () => {
   it('v-if directive syntax', () => {
     const ast = parse('<p v-if="show">hello world</p>', baseOptions)
     expect(ast.if).toBe('show')
+    expect(ast.conditions[0].exp).toBe('show')
+  })
+
+  it('v-elseif directive syntax', () => {
+    const ast = parse('<div><p v-if="show">hello</p><span v-elseif="2">elseif</span><p v-else>world</p></div>', baseOptions)
+    const ifAst = ast.children[0]
+    const conditionsAst = ifAst.conditions
+    expect(conditionsAst.length).toBe(3)
+    expect(conditionsAst[1].block.children[0].text).toBe('elseif')
+    expect(conditionsAst[1].block.parent).toBe(ast)
+    expect(conditionsAst[2].block.children[0].text).toBe('world')
+    expect(conditionsAst[2].block.parent).toBe(ast)
   })
 
   it('v-else directive syntax', () => {
     const ast = parse('<div><p v-if="show">hello</p><p v-else>world</p></div>', baseOptions)
     const ifAst = ast.children[0]
-    const elseAst = ifAst.elseBlock
-    expect(elseAst.else).toBe(true)
-    expect(elseAst.children[0].text).toBe('world')
-    expect(elseAst.parent).toBe(ast)
+    const conditionsAst = ifAst.conditions
+    expect(conditionsAst.length).toBe(2)
+    expect(conditionsAst[1].block.children[0].text).toBe('world')
+    expect(conditionsAst[1].block.parent).toBe(ast)
+  })
+
+  it('v-elseif directive invalid syntax', () => {
+    parse('<div><p v-elseif="1">world</p></div>', baseOptions)
+    expect('v-elseif="1" used on element').toHaveBeenWarned()
   })
 
   it('v-else directive invalid syntax', () => {