]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): v-html and v-text work with component (#13496)
authoredison <daiwei521@126.com>
Mon, 20 Oct 2025 05:50:34 +0000 (13:50 +0800)
committerGitHub <noreply@github.com>
Mon, 20 Oct 2025 05:50:34 +0000 (13:50 +0800)
12 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
packages/compiler-vapor/__tests__/transforms/vText.spec.ts
packages/compiler-vapor/src/generators/html.ts
packages/compiler-vapor/src/generators/text.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/vHtml.ts
packages/compiler-vapor/src/transforms/vText.ts
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/index.ts

index ecf886d7cbbdfa03d6b9cd631d002028a899bd47..4d65996f7571e4c969ff6c4960ce85eb42f039fd 100644 (file)
@@ -32,3 +32,24 @@ export function render(_ctx) {
   return n0
 }"
 `;
+
+exports[`v-html > work with component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, setBlockHtml as _setBlockHtml, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, null, null, true)
+  _renderEffect(() => _setBlockHtml(n0, _ctx.foo))
+  return n0
+}"
+`;
+
+exports[`v-html > work with dynamic component 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, setBlockHtml as _setBlockHtml, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
+  _renderEffect(() => _setBlockHtml(n0, _ctx.foo))
+  return n0
+}"
+`;
index 9a3b88acba308231b58f1d2eb1b31ed4de2adae4..cd77f5e13019180494f6cea1de76b950a6bef567 100644 (file)
@@ -33,3 +33,24 @@ export function render(_ctx) {
   return n0
 }"
 `;
+
+exports[`v-text > work with component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, null, null, true)
+  _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+  return n0
+}"
+`;
+
+exports[`v-text > work with dynamic component 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
+  _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+  return n0
+}"
+`;
index 0de0b6abca6a055ae1f9d4249148906107a108d2..1b3b4963812f67cc5998ee7421704f0189c626bd 100644 (file)
@@ -54,6 +54,18 @@ describe('v-html', () => {
     expect(code).matchSnapshot()
   })
 
+  test('work with dynamic component', () => {
+    const { code } = compileWithVHtml(`<component :is="Comp" v-html="foo"/>`)
+    expect(code).matchSnapshot()
+    expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+  })
+
+  test('work with component', () => {
+    const { code } = compileWithVHtml(`<Comp v-html="foo"/>`)
+    expect(code).matchSnapshot()
+    expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+  })
+
   test('should raise error and ignore children when v-html is present', () => {
     const onError = vi.fn()
     const { code, ir, helpers } = compileWithVHtml(
index 4f074fee87e16b8a2b1d15e468028d7893125dcd..d1cfd18f96e60fa081ab7ff2f183526d0ba4503d 100644 (file)
@@ -58,6 +58,18 @@ describe('v-text', () => {
     expect(code).matchSnapshot()
   })
 
+  test('work with dynamic component', () => {
+    const { code } = compileWithVText(`<component :is="Comp" v-text="foo"/>`)
+    expect(code).matchSnapshot()
+    expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+  })
+
+  test('work with component', () => {
+    const { code } = compileWithVText(`<Comp v-text="foo"/>`)
+    expect(code).matchSnapshot()
+    expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+  })
+
   test('should raise error and ignore children when v-text is present', () => {
     const onError = vi.fn()
     const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, {
index 72af699dd03d7bd04a927e725d0cfcc6639ebd06..711ee421d86aeae49882a1718fde22ce2e3d2b53 100644 (file)
@@ -8,9 +8,15 @@ export function genSetHtml(
   context: CodegenContext,
 ): CodeFragment[] {
   const { helper } = context
-  const { value, element } = oper
+
+  const { value, element, isComponent } = oper
   return [
     NEWLINE,
-    ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
+    ...genCall(
+      // use setBlockHtml for component
+      isComponent ? helper('setBlockHtml') : helper('setHtml'),
+      `n${element}`,
+      genExpression(value, context),
+    ),
   ]
 }
index 89e3167c664dedaf111b8b1f7e1f576cfe363375..ea3b041e6f63e6cb424fed0c6e899397d68a62e2 100644 (file)
@@ -10,11 +10,16 @@ export function genSetText(
   context: CodegenContext,
 ): CodeFragment[] {
   const { helper } = context
-  const { element, values, generated, jsx } = oper
+  const { element, values, generated, jsx, isComponent } = oper
   const texts = combineValues(values, context, jsx)
   return [
     NEWLINE,
-    ...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts),
+    ...genCall(
+      // use setBlockText for component
+      isComponent ? helper('setBlockText') : helper('setText'),
+      `${generated && !isComponent ? 'x' : 'n'}${element}`,
+      texts,
+    ),
   ]
 }
 
index a8130be3890982310973671630a58fd5ef986717..69b6f25846a8844e7072299caf25b8a2b3f148c4 100644 (file)
@@ -126,6 +126,7 @@ export interface SetTextIRNode extends BaseIRNode {
   values: SimpleExpressionNode[]
   generated?: boolean // whether this is a generated empty text node by `processTextLikeContainer`
   jsx?: boolean
+  isComponent?: boolean
 }
 
 export type KeyOverride = [find: string, replacement: string]
@@ -152,6 +153,7 @@ export interface SetHtmlIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_HTML
   element: number
   value: SimpleExpressionNode
+  isComponent?: boolean
 }
 
 export interface SetTemplateRefIRNode extends BaseIRNode {
index 6b9a269e495bf03161157a4ae059bf86388da630..1b98afe4788556c395700a8dc6c3140e75c1e81b 100644 (file)
@@ -22,5 +22,6 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => {
     type: IRNodeTypes.SET_HTML,
     element: context.reference(),
     value: exp,
+    isComponent: node.tagType === 1,
   })
 }
index 0832398e12a4870eb1fdbcab68ac9b723b6833f4..9e46fa2e0093073c84c9853d4a9a40314f79f2c6 100644 (file)
@@ -1,4 +1,8 @@
-import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
+import {
+  DOMErrorCodes,
+  ElementTypes,
+  createDOMCompilerError,
+} from '@vue/compiler-dom'
 import { IRNodeTypes } from '../ir'
 import { EMPTY_EXPRESSION } from './utils'
 import type { DirectiveTransform } from '../transform'
@@ -30,15 +34,19 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
     context.childrenTemplate = [String(literal)]
   } else {
     context.childrenTemplate = [' ']
-    context.registerOperation({
-      type: IRNodeTypes.GET_TEXT_CHILD,
-      parent: context.reference(),
-    })
+    const isComponent = node.tagType === ElementTypes.COMPONENT
+    if (!isComponent) {
+      context.registerOperation({
+        type: IRNodeTypes.GET_TEXT_CHILD,
+        parent: context.reference(),
+      })
+    }
     context.registerEffect([exp], {
       type: IRNodeTypes.SET_TEXT,
       element: context.reference(),
       values: [exp],
       generated: true,
+      isComponent,
     })
   }
 }
index 9d07b413541932f0f6c58ddc1a630138b15e866e..eedbde13e0ec20aff7e68b7fbc2bdd80614b96dd 100644 (file)
@@ -1,7 +1,9 @@
-import { NOOP } from '@vue/shared'
+import { NOOP, toDisplayString } from '@vue/shared'
 import {
   setDynamicProp as _setDynamicProp,
   setAttr,
+  setBlockHtml,
+  setBlockText,
   setClass,
   setDynamicProps,
   setElementText,
@@ -11,8 +13,15 @@ import {
   setValue,
 } from '../../src/dom/prop'
 import { setStyle } from '../../src/dom/prop'
-import { VaporComponentInstance } from '../../src/component'
+import { VaporComponentInstance, createComponent } from '../../src/component'
 import { ref, setCurrentInstance } from '@vue/runtime-dom'
+import { makeRender } from '../_utils'
+import {
+  createDynamicComponent,
+  defineVaporComponent,
+  renderEffect,
+  template,
+} from '../../src'
 
 let removeComponentInstance = NOOP
 beforeEach(() => {
@@ -24,6 +33,8 @@ afterEach(() => {
   removeComponentInstance()
 })
 
+const define = makeRender()
+
 describe('patchProp', () => {
   describe('setClass', () => {
     test('should set class', () => {
@@ -444,4 +455,188 @@ describe('patchProp', () => {
       expect(el.innerHTML).toBe('<p>bar</p>')
     })
   })
+
+  describe('setBlockText', () => {
+    test('with dynamic component', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('<div>child</div>', true)()
+        },
+      })
+      const value = ref('foo')
+      const { html } = define({
+        setup() {
+          const n1 = createDynamicComponent(() => Comp, null, null, true)
+          renderEffect(() => setBlockText(n1, toDisplayString(value)))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div>foo</div><!--dynamic-component-->')
+    })
+
+    test('with dynamic component with fallback', async () => {
+      const value = ref('foo')
+      const { html } = define({
+        setup() {
+          const n1 = createDynamicComponent(() => 'button', null, null, true)
+          renderEffect(() => setBlockText(n1, toDisplayString(value)))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<button>foo</button><!--dynamic-component-->')
+    })
+
+    test('with component', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('<div>child</div>', true)()
+        },
+      })
+      const value = ref('foo')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockText(n1, toDisplayString(value)))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div>foo</div>')
+    })
+
+    test('with component renders multiple roots nodes', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return [
+            template('<div>child</div>')(),
+            template('<div>child</div>')(),
+          ]
+        },
+      })
+      const value = ref('foo')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockText(n1, toDisplayString(value)))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div>child</div><div>child</div>')
+      expect('Extraneous non-props attributes (textContent)').toHaveBeenWarned()
+    })
+
+    test('with component renders text node', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('child')()
+        },
+      })
+      const value = ref('foo')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockText(n1, toDisplayString(value)))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('child')
+      expect('Extraneous non-props attributes (textContent)').toHaveBeenWarned()
+    })
+  })
+
+  describe('setBlockHtml', () => {
+    test('with dynamic component', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('<div>child</div>', true)()
+        },
+      })
+      const value = ref('<p>foo</p>')
+      const { html } = define({
+        setup() {
+          const n1 = createDynamicComponent(() => Comp, null, null, true)
+          renderEffect(() => setBlockHtml(n1, value.value))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div><p>foo</p></div><!--dynamic-component-->')
+    })
+
+    test('with dynamic component with fallback', async () => {
+      const value = ref('<p>foo</p>')
+      const { html } = define({
+        setup() {
+          const n1 = createDynamicComponent(() => 'button', null, null, true)
+          renderEffect(() => setBlockHtml(n1, value.value))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<button><p>foo</p></button><!--dynamic-component-->')
+    })
+
+    test('with component', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('<div>child</div>', true)()
+        },
+      })
+      const value = ref('<p>foo</p>')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockHtml(n1, value.value))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div><p>foo</p></div>')
+    })
+
+    test('with component renders multiple roots', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return [
+            template('<div>child</div>')(),
+            template('<div>child</div>')(),
+          ]
+        },
+      })
+      const value = ref('<p>foo</p>')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockHtml(n1, value.value))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('<div>child</div><div>child</div>')
+      expect('Extraneous non-props attributes (innerHTML)').toHaveBeenWarned()
+    })
+
+    test('with component renders text node', async () => {
+      const Comp = defineVaporComponent({
+        setup() {
+          return template('child')()
+        },
+      })
+      const value = ref('<p>foo</p>')
+      const { html } = define({
+        setup() {
+          const n1 = createComponent(Comp, null, null, true)
+          renderEffect(() => setBlockHtml(n1, value.value))
+          return n1
+        },
+      }).render()
+
+      expect(html()).toBe('child')
+      expect('Extraneous non-props attributes (innerHTML)').toHaveBeenWarned()
+    })
+  })
 })
index 71aaaf2853237b346328b94538e7f653034964e7..2d60cf6b283ae11095dbd5dd9c1ab6f2b13a68fc 100644 (file)
@@ -1,6 +1,7 @@
 import {
   type NormalizedStyle,
   canSetValueDirectly,
+  isArray,
   isOn,
   isString,
   normalizeClass,
@@ -20,7 +21,9 @@ import {
 import {
   type VaporComponentInstance,
   isApplyingFallthroughProps,
+  isVaporComponent,
 } from '../component'
+import type { Block } from '../block'
 
 type TargetElement = Element & {
   $root?: true
@@ -197,6 +200,46 @@ export function setElementText(
   }
 }
 
+export function setBlockText(
+  block: Block & { $txt?: string },
+  value: unknown,
+): void {
+  value = value == null ? '' : value
+  if (block.$txt !== value) {
+    setTextToBlock(block, (block.$txt = value as string))
+  }
+}
+
+/**
+ * dev only
+ */
+function warnCannotSetProp(prop: string): void {
+  warn(
+    `Extraneous non-props attributes (` +
+      `${prop}) ` +
+      `were passed to component but could not be automatically inherited ` +
+      `because component renders text or multiple root nodes.`,
+  )
+}
+
+function setTextToBlock(block: Block, value: any): void {
+  if (block instanceof Node) {
+    if (block instanceof Element) {
+      block.textContent = value
+    } else if (__DEV__) {
+      warnCannotSetProp('textContent')
+    }
+  } else if (isVaporComponent(block)) {
+    setTextToBlock(block.block, value)
+  } else if (isArray(block)) {
+    if (__DEV__) {
+      warnCannotSetProp('textContent')
+    }
+  } else {
+    setTextToBlock(block.nodes, value)
+  }
+}
+
 export function setHtml(el: TargetElement, value: any): void {
   value = value == null ? '' : unsafeToTrustedHTML(value)
   if (el.$html !== value) {
@@ -204,6 +247,34 @@ export function setHtml(el: TargetElement, value: any): void {
   }
 }
 
+export function setBlockHtml(
+  block: Block & { $html?: string },
+  value: any,
+): void {
+  value = value == null ? '' : value
+  if (block.$html !== value) {
+    setHtmlToBlock(block, (block.$html = value))
+  }
+}
+
+function setHtmlToBlock(block: Block, value: any): void {
+  if (block instanceof Node) {
+    if (block instanceof Element) {
+      block.innerHTML = value
+    } else if (__DEV__) {
+      warnCannotSetProp('innerHTML')
+    }
+  } else if (isVaporComponent(block)) {
+    setHtmlToBlock(block.block, value)
+  } else if (isArray(block)) {
+    if (__DEV__) {
+      warnCannotSetProp('innerHTML')
+    }
+  } else {
+    setHtmlToBlock(block.nodes, value)
+  }
+}
+
 export function setDynamicProps(el: any, args: any[]): void {
   const props = args.length > 1 ? mergeProps(...args) : args[0]
   const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}`
index bad4b4f343a190e8a097f88439b3b5fea7175a4c..54051cc1950173093d48d99e6654f54ea27f7ade 100644 (file)
@@ -18,7 +18,9 @@ export { template } from './dom/template'
 export { createTextNode, child, nthChild, next } from './dom/node'
 export {
   setText,
+  setBlockText,
   setHtml,
+  setBlockHtml,
   setClass,
   setStyle,
   setAttr,
@@ -26,6 +28,7 @@ export {
   setProp,
   setDOMProp,
   setDynamicProps,
+  setElementText,
 } from './dom/prop'
 export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
 export { createIf } from './apiCreateIf'