feat(runtime-vapor): dynamic components work with v-html and v-text

This commit is contained in:
daiwei 2025-06-19 11:23:54 +08:00
parent 88ef97ffd3
commit a64867a500
11 changed files with 96 additions and 10 deletions

View File

@ -32,3 +32,13 @@ export function render(_ctx) {
return n0 return n0
}" }"
`; `;
exports[`v-html > work with dynamic component 1`] = `
"import { createDynamicComponent as _createDynamicComponent, setHtml as _setHtml, renderEffect as _renderEffect } from 'vue';
export function render(_ctx) {
const n0 = _createDynamicComponent(() => ('button'), null, null, true)
_renderEffect(() => _setHtml(n0.nodes, _ctx.foo))
return n0
}"
`;

View File

@ -33,3 +33,13 @@ export function render(_ctx) {
return n0 return n0
}" }"
`; `;
exports[`v-text > work with dynamic component 1`] = `
"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setElementText as _setElementText, renderEffect as _renderEffect } from 'vue';
export function render(_ctx) {
const n0 = _createDynamicComponent(() => ('button'), null, null, true)
_renderEffect(() => _setElementText(n0.nodes, _toDisplayString(_ctx.foo), true))
return n0
}"
`;

View File

@ -54,6 +54,14 @@ describe('v-html', () => {
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('work with dynamic component', () => {
const { code } = compileWithVHtml(
`<component :is="'button'" v-html="foo"/>`,
)
expect(code).matchSnapshot()
expect(code).contains('setHtml(n0.nodes, _ctx.foo))')
})
test('should raise error and ignore children when v-html is present', () => { test('should raise error and ignore children when v-html is present', () => {
const onError = vi.fn() const onError = vi.fn()
const { code, ir, helpers } = compileWithVHtml( const { code, ir, helpers } = compileWithVHtml(

View File

@ -58,6 +58,16 @@ describe('v-text', () => {
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('work with dynamic component', () => {
const { code } = compileWithVText(
`<component :is="'button'" v-text="foo"/>`,
)
expect(code).matchSnapshot()
expect(code).contains(
'setElementText(n0.nodes, _toDisplayString(_ctx.foo), true)',
)
})
test('should raise error and ignore children when v-text is present', () => { test('should raise error and ignore children when v-text is present', () => {
const onError = vi.fn() const onError = vi.fn()
const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, { const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, {

View File

@ -60,12 +60,15 @@ export function genCreateComponent(
[], [],
) )
const isDynamicComponent = operation.dynamic && !operation.dynamic.isStatic
if (isDynamicComponent) context.block.dynamicComponents.push(operation.id)
return [ return [
NEWLINE, NEWLINE,
...inlineHandlers, ...inlineHandlers,
`const n${operation.id} = `, `const n${operation.id} = `,
...genCall( ...genCall(
operation.dynamic && !operation.dynamic.isStatic isDynamicComponent
? helper('createDynamicComponent') ? helper('createDynamicComponent')
: operation.asset : operation.asset
? helper('createComponentWithFallback') ? helper('createComponentWithFallback')

View File

@ -7,10 +7,21 @@ export function genSetHtml(
oper: SetHtmlIRNode, oper: SetHtmlIRNode,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const {
helper,
block: { dynamicComponents },
} = context
const isDynamicComponent = dynamicComponents.includes(oper.element)
const { value, element } = oper const { value, element } = oper
return [ return [
NEWLINE, NEWLINE,
...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)), ...genCall(
helper('setHtml'),
// if the element is a dynamic component (VaporFragment)
// it should set html to the VaporFragment's nodes
`n${element}${isDynamicComponent ? '.nodes' : ''}`,
genExpression(value, context),
),
] ]
} }

View File

@ -9,13 +9,33 @@ export function genSetText(
oper: SetTextIRNode, oper: SetTextIRNode,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const {
helper,
block: { dynamicComponents },
} = context
const { element, values, generated, jsx } = oper const { element, values, generated, jsx } = oper
const texts = combineValues(values, context, jsx) const texts = combineValues(values, context, jsx)
return [
NEWLINE, // if the element is a dynamic component, we need to use `setElementText`
...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts), // to set the textContent of the VaporFragment's nodes.
] return dynamicComponents.includes(oper.element)
? [
NEWLINE,
...genCall(
helper('setElementText'),
`n${element}.nodes`,
texts,
'true', // isConverted
),
]
: [
NEWLINE,
...genCall(
helper('setText'),
`${generated ? 'x' : 'n'}${element}`,
texts,
),
]
} }
function combineValues( function combineValues(
@ -40,6 +60,14 @@ export function genGetTextChild(
oper: GetTextChildIRNode, oper: GetTextChildIRNode,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const {
block: { dynamicComponents },
} = context
// if the parent is a dynamic component, don't need to generate a child
// because it will use the `setElementText` helper directly.
if (dynamicComponents.includes(oper.parent)) return []
return [ return [
NEWLINE, NEWLINE,
`const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`, `const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`,

View File

@ -49,6 +49,7 @@ export interface BlockIRNode extends BaseIRNode {
type: IRNodeTypes.BLOCK type: IRNodeTypes.BLOCK
node: RootNode | TemplateChildNode node: RootNode | TemplateChildNode
dynamic: IRDynamicInfo dynamic: IRDynamicInfo
dynamicComponents: number[]
tempId: number tempId: number
effect: IREffect[] effect: IREffect[]
operation: OperationNode[] operation: OperationNode[]

View File

@ -26,6 +26,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
type: IRNodeTypes.BLOCK, type: IRNodeTypes.BLOCK,
node, node,
dynamic: newDynamic(), dynamic: newDynamic(),
dynamicComponents: [],
effect: [], effect: [],
operation: [], operation: [],
returns: [], returns: [],

View File

@ -185,13 +185,16 @@ export function setText(el: Text & { $txt?: string }, value: string): void {
} }
/** /**
* Used by setDynamicProps only, so need to guard with `toDisplayString` * Used by
* - setDynamicProps, need to guard with `toDisplayString`
* - v-text on dynamic component, value passed here is already converted
*/ */
export function setElementText( export function setElementText(
el: Node & { $txt?: string }, el: Node & { $txt?: string },
value: unknown, value: unknown,
isConverted: boolean = false,
): void { ): void {
if (el.$txt !== (value = toDisplayString(value))) { if (el.$txt !== (value = isConverted ? value : toDisplayString(value))) {
el.textContent = el.$txt = value as string el.textContent = el.$txt = value as string
} }
} }

View File

@ -22,6 +22,7 @@ export {
setProp, setProp,
setDOMProp, setDOMProp,
setDynamicProps, setDynamicProps,
setElementText,
} from './dom/prop' } from './dom/prop'
export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event' export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { createIf } from './apiCreateIf' export { createIf } from './apiCreateIf'