Quellcode durchsuchen

fix(ssr): properly render <select v-model> initial state

fix #6986
Evan You vor 8 Jahren
Ursprung
Commit
e1657fd7ce

+ 3 - 1
src/platforms/web/server/directives/index.js

@@ -1,5 +1,7 @@
 import show from './show'
+import model from './model'
 
 export default {
-  show
+  show,
+  model
 }

+ 44 - 0
src/platforms/web/server/directives/model.js

@@ -0,0 +1,44 @@
+/* @flow */
+
+import { looseEqual, looseIndexOf } from 'shared/util'
+
+// this is only applied for <select v-model> because it is the only edge case
+// that must be done at runtime instead of compile time.
+export default function model (node: VNodeWithData, dir: VNodeDirective) {
+  if (!node.children) return
+  const value = dir.value
+  const isMultiple = node.data.attrs && node.data.attrs.multiple
+  for (let i = 0, l = node.children.length; i < l; i++) {
+    const option = node.children[i]
+    if (option.tag === 'option') {
+      if (isMultiple) {
+        const selected =
+          Array.isArray(value) &&
+          (looseIndexOf(value, getValue(option)) > -1)
+        if (selected) {
+          setSelected(option)
+        }
+      } else {
+        if (looseEqual(value, getValue(option))) {
+          setSelected(option)
+          return
+        }
+      }
+    }
+  }
+}
+
+function getValue (option) {
+  const data = option.data || {}
+  return (
+    (data.attrs && data.attrs.value) ||
+    (data.domProps && data.domProps.value) ||
+    (option.children && option.children[0] && option.children[0].text)
+  )
+}
+
+function setSelected (option) {
+  const data = option.data || (option.data = {})
+  const attrs = data.attrs || (data.attrs = {})
+  attrs.selected = ''
+}

+ 13 - 1
src/server/optimizing-compiler/optimizer.js

@@ -113,7 +113,8 @@ function isUnOptimizableTree (node: ASTNode): boolean {
   return (
     isBuiltInTag(node.tag) || // built-in (slot, component)
     !isPlatformReservedTag(node.tag) || // custom component
-    !!node.component // "is" component
+    !!node.component || // "is" component
+    isSelectWithModel(node) // <select v-model> requires runtime inspection
   )
 }
 
@@ -126,3 +127,14 @@ function hasCustomDirective (node: ASTNode): ?boolean {
     node.directives.some(d => !isBuiltInDir(d.name))
   )
 }
+
+// <select v-model> cannot be optimized because it requires a runtime check
+// to determine proper selected option
+function isSelectWithModel (node: ASTNode): ?boolean {
+  return (
+    node.type === 1 &&
+    node.tag === 'select' &&
+    node.directives &&
+    node.directives.some(d => d.name === 'model')
+  )
+}

+ 78 - 0
test/ssr/ssr-string.spec.js

@@ -1034,6 +1034,84 @@ describe('SSR: renderToString', () => {
       done()
     })
   })
+
+  it('render v-model with <select> (value binding)', done => {
+    renderVmWithOptions({
+      data: {
+        selected: 2,
+        options: [
+          { id: 1, label: 'one' },
+          { id: 2, label: 'two' }
+        ]
+      },
+      template: `
+      <div>
+        <select v-model="selected">
+          <option v-for="o in options" :value="o.id">{{ o.label }}</option>
+        </select>
+      </div>
+      `
+    }, result => {
+      expect(result).toContain(
+        '<select>' +
+          '<option value="1">one</option>' +
+          '<option selected="selected" value="2">two</option>' +
+        '</select>'
+      )
+      done()
+    })
+  })
+
+  it('render v-model with <select> (static value)', done => {
+    renderVmWithOptions({
+      data: {
+        selected: 2
+      },
+      template: `
+      <div>
+        <select v-model="selected">
+          <option value="1">one</option>
+          <option value="2">two</option>
+        </select>
+      </div>
+      `
+    }, result => {
+      expect(result).toContain(
+        '<select>' +
+          '<option value="1">one</option> ' +
+          '<option value="2" selected="selected">two</option>' +
+        '</select>'
+      )
+      done()
+    })
+  })
+
+  it('render v-model with <select> (text as value)', done => {
+    renderVmWithOptions({
+      data: {
+        selected: 2,
+        options: [
+          { id: 1, label: 'one' },
+          { id: 2, label: 'two' }
+        ]
+      },
+      template: `
+      <div>
+        <select v-model="selected">
+          <option v-for="o in options">{{ o.id }}</option>
+        </select>
+      </div>
+      `
+    }, result => {
+      expect(result).toContain(
+        '<select>' +
+          '<option>1</option>' +
+          '<option selected="selected">2</option>' +
+        '</select>'
+      )
+      done()
+    })
+  })
 })
 
 function renderVmWithOptions (options, cb) {