From: edison Date: Mon, 20 Oct 2025 05:50:34 +0000 (+0800) Subject: feat(runtime-vapor): v-html and v-text work with component (#13496) X-Git-Tag: v3.6.0-alpha.3~36 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7870fc08a148c4a1e7ac60e7bdc759b56e589de5;p=thirdparty%2Fvuejs%2Fcore.git feat(runtime-vapor): v-html and v-text work with component (#13496) --- diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap index ecf886d7cb..4d65996f75 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vHtml.spec.ts.snap @@ -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 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap index 9a3b88acba..cd77f5e130 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap @@ -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 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts index 0de0b6abca..1b3b496381 100644 --- a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts @@ -54,6 +54,18 @@ describe('v-html', () => { expect(code).matchSnapshot() }) + test('work with dynamic component', () => { + const { code } = compileWithVHtml(``) + expect(code).matchSnapshot() + expect(code).contains('setBlockHtml(n0, _ctx.foo))') + }) + + test('work with component', () => { + const { code } = compileWithVHtml(``) + 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( diff --git a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts index 4f074fee87..d1cfd18f96 100644 --- a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts @@ -58,6 +58,18 @@ describe('v-text', () => { expect(code).matchSnapshot() }) + test('work with dynamic component', () => { + const { code } = compileWithVText(``) + expect(code).matchSnapshot() + expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))') + }) + + test('work with component', () => { + const { code } = compileWithVText(``) + 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(`
hello
`, { diff --git a/packages/compiler-vapor/src/generators/html.ts b/packages/compiler-vapor/src/generators/html.ts index 72af699dd0..711ee421d8 100644 --- a/packages/compiler-vapor/src/generators/html.ts +++ b/packages/compiler-vapor/src/generators/html.ts @@ -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), + ), ] } diff --git a/packages/compiler-vapor/src/generators/text.ts b/packages/compiler-vapor/src/generators/text.ts index 89e3167c66..ea3b041e6f 100644 --- a/packages/compiler-vapor/src/generators/text.ts +++ b/packages/compiler-vapor/src/generators/text.ts @@ -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, + ), ] } diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index a8130be389..69b6f25846 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -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 { diff --git a/packages/compiler-vapor/src/transforms/vHtml.ts b/packages/compiler-vapor/src/transforms/vHtml.ts index 6b9a269e49..1b98afe478 100644 --- a/packages/compiler-vapor/src/transforms/vHtml.ts +++ b/packages/compiler-vapor/src/transforms/vHtml.ts @@ -22,5 +22,6 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => { type: IRNodeTypes.SET_HTML, element: context.reference(), value: exp, + isComponent: node.tagType === 1, }) } diff --git a/packages/compiler-vapor/src/transforms/vText.ts b/packages/compiler-vapor/src/transforms/vText.ts index 0832398e12..9e46fa2e00 100644 --- a/packages/compiler-vapor/src/transforms/vText.ts +++ b/packages/compiler-vapor/src/transforms/vText.ts @@ -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, }) } } diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts index 9d07b41354..eedbde13e0 100644 --- a/packages/runtime-vapor/__tests__/dom/prop.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts @@ -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('

bar

') }) }) + + describe('setBlockText', () => { + test('with dynamic component', async () => { + const Comp = defineVaporComponent({ + setup() { + return template('
child
', 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('
foo
') + }) + + 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('') + }) + + test('with component', async () => { + const Comp = defineVaporComponent({ + setup() { + return template('
child
', 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('
foo
') + }) + + test('with component renders multiple roots nodes', async () => { + const Comp = defineVaporComponent({ + setup() { + return [ + template('
child
')(), + 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
child
') + 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('
child
', true)() + }, + }) + const value = ref('

foo

') + const { html } = define({ + setup() { + const n1 = createDynamicComponent(() => Comp, null, null, true) + renderEffect(() => setBlockHtml(n1, value.value)) + return n1 + }, + }).render() + + expect(html()).toBe('

foo

') + }) + + test('with dynamic component with fallback', async () => { + const value = ref('

foo

') + const { html } = define({ + setup() { + const n1 = createDynamicComponent(() => 'button', null, null, true) + renderEffect(() => setBlockHtml(n1, value.value)) + return n1 + }, + }).render() + + expect(html()).toBe('') + }) + + test('with component', async () => { + const Comp = defineVaporComponent({ + setup() { + return template('
child
', true)() + }, + }) + const value = ref('

foo

') + const { html } = define({ + setup() { + const n1 = createComponent(Comp, null, null, true) + renderEffect(() => setBlockHtml(n1, value.value)) + return n1 + }, + }).render() + + expect(html()).toBe('

foo

') + }) + + test('with component renders multiple roots', async () => { + const Comp = defineVaporComponent({ + setup() { + return [ + template('
child
')(), + template('
child
')(), + ] + }, + }) + const value = ref('

foo

') + const { html } = define({ + setup() { + const n1 = createComponent(Comp, null, null, true) + renderEffect(() => setBlockHtml(n1, value.value)) + return n1 + }, + }).render() + + expect(html()).toBe('
child
child
') + expect('Extraneous non-props attributes (innerHTML)').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(() => setBlockHtml(n1, value.value)) + return n1 + }, + }).render() + + expect(html()).toBe('child') + expect('Extraneous non-props attributes (innerHTML)').toHaveBeenWarned() + }) + }) }) diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index 71aaaf2853..2d60cf6b28 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -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 ? '$' : ''}` diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index bad4b4f343..54051cc195 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -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'