]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-ssr): textarea with v-text directive SSR (#13975)
authorAlex Snezhko <alexsnezhko89@gmail.com>
Wed, 5 Nov 2025 09:05:29 +0000 (01:05 -0800)
committerGitHub <noreply@github.com>
Wed, 5 Nov 2025 09:05:29 +0000 (17:05 +0800)
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/server-renderer/__tests__/ssrDirectives.spec.ts

index ee46894f9fcefb80e3180788f94c7e39c2df16d2..3fbedc1ae576a91cdf950ab9ace14c53ec2b7b18 100644 (file)
@@ -122,8 +122,13 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             | InterpolationNode
             | undefined
           // If interpolation, this is dynamic <textarea> content, potentially
-          // injected by v-model and takes higher priority than v-bind value
-          if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
+          // injected by v-model and takes higher priority than v-bind value.
+          // Additionally, directives with content overrides (v-text/v-html)
+          // have higher priority than the merged props.
+          if (
+            !hasContentOverrideDirective(node) &&
+            (!existingText || existingText.type !== NodeTypes.INTERPOLATION)
+          ) {
             // <textarea> with dynamic v-bind. We don't know if the final props
             // will contain .value, so we will have to do something special:
             // assign the merged props to a temp variable, and check whether
@@ -176,9 +181,8 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
             ]
           }
         } else if (directives.length && !node.children.length) {
-          // v-text directive has higher priority than the merged props
-          const vText = findDir(node, 'text')
-          if (!vText) {
+          // v-text/v-html have higher priority than the merged props
+          if (!hasContentOverrideDirective(node)) {
             const tempId = `_temp${context.temps++}`
             propsExp.arguments = [
               createAssignmentExpression(
@@ -449,6 +453,10 @@ function findVModel(node: PlainElementNode): DirectiveNode | undefined {
   ) as DirectiveNode | undefined
 }
 
+function hasContentOverrideDirective(node: PlainElementNode): boolean {
+  return !!findDir(node, 'text') || !!findDir(node, 'html')
+}
+
 export function ssrProcessElement(
   node: PlainElementNode,
   context: SSRTransformContext,
index dfdebe971f55888229ccf155148a17863ab93799..fdf16ba97cfb7d1c6601f471219030f0afdc988f 100644 (file)
@@ -263,6 +263,41 @@ describe('ssr: directives', () => {
     })
   })
 
+  describe('template with v-text / v-html', () => {
+    test('element with v-html', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<span v-html="foo"/>`,
+          }),
+        ),
+      ).toBe(`<span>hello</span>`)
+    })
+
+    test('textarea with v-text', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<textarea v-text="foo"/>`,
+          }),
+        ),
+      ).toBe(`<textarea>hello</textarea>`)
+    })
+
+    test('textarea with v-html', async () => {
+      expect(
+        await renderToString(
+          createApp({
+            data: () => ({ foo: 'hello' }),
+            template: `<textarea v-html="foo"/>`,
+          }),
+        ),
+      ).toBe(`<textarea>hello</textarea>`)
+    })
+  })
+
   describe('vnode v-show', () => {
     test('basic', async () => {
       expect(