]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): handle initial selected state for select with v-model + v-for/v-if option...
authoredison <daiwei521@126.com>
Wed, 18 Jun 2025 12:54:32 +0000 (20:54 +0800)
committerGitHub <noreply@github.com>
Wed, 18 Jun 2025 12:54:32 +0000 (20:54 +0800)
close #13486

packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/src/transforms/ssrVModel.ts

index 0bf7673d00dc1c3bc3a6daabe80a9b079020719f..8a439dbf4b5cc6063d0a74d7f1859539b1d8082a 100644 (file)
@@ -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">', () => {
index 80e3839318b1540396ed8db2e0b8a9b6e5d49924..cbe5b2b42a30ce8897aa82ec681f826c8e58f491 100644 (file)
@@ -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(