Bläddra i källkod

fix(ssr): handle initial selected state for select with v-model + v-for/v-if option (#13487)

close #13486
edison 10 månader sedan
förälder
incheckning
15520954f9

+ 126 - 0
packages/compiler-ssr/__tests__/ssrVModel.spec.ts

@@ -166,6 +166,132 @@ describe('ssr: v-model', () => {
         _push(\`</optgroup></select></div>\`)
       }"
     `)
+
+    expect(
+      compileWithWrapper(`
+        <select multiple v-model="model">
+          <optgroup>
+            <option v-for="item in items" :value="item">{{item}}</option>
+          </optgroup>
+        </select>`).code,
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
+        _ssrRenderList(_ctx.items, (item) => {
+          _push(\`<option\${
+            _ssrRenderAttr("value", item)
+          }\${
+            (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+              ? _ssrLooseContain(_ctx.model, item)
+              : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+          }>\${
+            _ssrInterpolate(item)
+          }</option>\`)
+        })
+        _push(\`<!--]--></optgroup></select></div>\`)
+      }"
+    `)
+
+    expect(
+      compileWithWrapper(`
+        <select multiple v-model="model">
+          <optgroup>
+            <option v-if="true" :value="item">{{item}}</option>
+          </optgroup>
+        </select>`).code,
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
+        if (true) {
+          _push(\`<option\${
+            _ssrRenderAttr("value", _ctx.item)
+          }\${
+            (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+              ? _ssrLooseContain(_ctx.model, _ctx.item)
+              : _ssrLooseEqual(_ctx.model, _ctx.item))) ? " selected" : ""
+          }>\${
+            _ssrInterpolate(_ctx.item)
+          }</option>\`)
+        } else {
+          _push(\`<!---->\`)
+        }
+        _push(\`</optgroup></select></div>\`)
+      }"
+    `)
+
+    expect(
+      compileWithWrapper(`
+        <select multiple v-model="model">
+          <optgroup>
+            <template v-if="ok">
+              <option v-for="item in items" :value="item">{{item}}</option>
+            </template>
+          </optgroup>
+        </select>`).code,
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup>\`)
+        if (_ctx.ok) {
+          _push(\`<!--[-->\`)
+          _ssrRenderList(_ctx.items, (item) => {
+            _push(\`<option\${
+              _ssrRenderAttr("value", item)
+            }\${
+              (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+                ? _ssrLooseContain(_ctx.model, item)
+                : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+            }>\${
+              _ssrInterpolate(item)
+            }</option>\`)
+          })
+          _push(\`<!--]-->\`)
+        } else {
+          _push(\`<!---->\`)
+        }
+        _push(\`</optgroup></select></div>\`)
+      }"
+    `)
+
+    expect(
+      compileWithWrapper(`
+        <select multiple v-model="model">
+          <optgroup>
+            <template v-for="item in items" :value="item">
+              <option v-if="item===1" :value="item">{{item}}</option>
+            </template>
+          </optgroup>
+        </select>`).code,
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _push(\`<div\${_ssrRenderAttrs(_attrs)}><select multiple><optgroup><!--[-->\`)
+        _ssrRenderList(_ctx.items, (item) => {
+          _push(\`<!--[-->\`)
+          if (item===1) {
+            _push(\`<option\${
+              _ssrRenderAttr("value", item)
+            }\${
+              (_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
+                ? _ssrLooseContain(_ctx.model, item)
+                : _ssrLooseEqual(_ctx.model, item))) ? " selected" : ""
+            }>\${
+              _ssrInterpolate(item)
+            }</option>\`)
+          } else {
+            _push(\`<!---->\`)
+          }
+          _push(\`<!--]-->\`)
+        })
+        _push(\`<!--]--></optgroup></select></div>\`)
+      }"
+    `)
   })
 
   test('<input type="radio">', () => {

+ 14 - 15
packages/compiler-ssr/src/transforms/ssrVModel.ts

@@ -39,6 +39,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
     }
   }
 
+  const processSelectChildren = (children: TemplateChildNode[]) => {
+    children.forEach(child => {
+      if (child.type === NodeTypes.ELEMENT) {
+        processOption(child as PlainElementNode)
+      } else if (child.type === NodeTypes.FOR) {
+        processSelectChildren(child.children)
+      } else if (child.type === NodeTypes.IF) {
+        child.branches.forEach(b => processSelectChildren(b.children))
+      }
+    })
+  }
+
   function processOption(plainNode: PlainElementNode) {
     if (plainNode.tag === 'option') {
       if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
@@ -65,9 +77,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
         )
       }
     } else if (plainNode.tag === 'optgroup') {
-      plainNode.children.forEach(option =>
-        processOption(option as PlainElementNode),
-      )
+      processSelectChildren(plainNode.children)
     }
   }
 
@@ -163,18 +173,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
       checkDuplicatedValue()
       node.children = [createInterpolation(model, model.loc)]
     } else if (node.tag === 'select') {
-      const processChildren = (children: TemplateChildNode[]) => {
-        children.forEach(child => {
-          if (child.type === NodeTypes.ELEMENT) {
-            processOption(child as PlainElementNode)
-          } else if (child.type === NodeTypes.FOR) {
-            processChildren(child.children)
-          } else if (child.type === NodeTypes.IF) {
-            child.branches.forEach(b => processChildren(b.children))
-          }
-        })
-      }
-      processChildren(node.children)
+      processSelectChildren(node.children)
     } else {
       context.onError(
         createDOMCompilerError(