Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit 3e9e32e

Browse files
authored
fix(runtime-dom): properly handle innerHTML unmount into new children (#11159)
close #9135
1 parent b287aee commit 3e9e32e

File tree

5 files changed

+40
-92
lines changed

5 files changed

+40
-92
lines changed

packages/runtime-core/src/hydration.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -465,15 +465,7 @@ export function createHydrationFunctions(
465465
// force hydrate v-bind with .prop modifiers
466466
key[0] === '.'
467467
) {
468-
patchProp(
469-
el,
470-
key,
471-
null,
472-
props[key],
473-
undefined,
474-
undefined,
475-
parentComponent,
476-
)
468+
patchProp(el, key, null, props[key], undefined, parentComponent)
477469
}
478470
}
479471
} else if (props.onClick) {
@@ -485,7 +477,6 @@ export function createHydrationFunctions(
485477
null,
486478
props.onClick,
487479
undefined,
488-
undefined,
489480
parentComponent,
490481
)
491482
} else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {

packages/runtime-core/src/renderer.ts

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,7 @@ export interface RendererOptions<
107107
prevValue: any,
108108
nextValue: any,
109109
namespace?: ElementNamespace,
110-
prevChildren?: VNode<HostNode, HostElement>[],
111110
parentComponent?: ComponentInternalInstance | null,
112-
parentSuspense?: SuspenseBoundary | null,
113-
unmountChildren?: UnmountChildrenFn,
114111
): void
115112
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
116113
remove(el: HostNode): void
@@ -670,17 +667,7 @@ function baseCreateRenderer(
670667
if (props) {
671668
for (const key in props) {
672669
if (key !== 'value' && !isReservedProp(key)) {
673-
hostPatchProp(
674-
el,
675-
key,
676-
null,
677-
props[key],
678-
namespace,
679-
vnode.children as VNode[],
680-
parentComponent,
681-
parentSuspense,
682-
unmountChildren,
683-
)
670+
hostPatchProp(el, key, null, props[key], namespace, parentComponent)
684671
}
685672
}
686673
/**
@@ -833,6 +820,15 @@ function baseCreateRenderer(
833820
dynamicChildren = null
834821
}
835822

823+
// #9135 innerHTML / textContent unset needs to happen before possible
824+
// new children mount
825+
if (
826+
(oldProps.innerHTML && newProps.innerHTML == null) ||
827+
(oldProps.textContent && newProps.textContent == null)
828+
) {
829+
hostSetElementText(el, '')
830+
}
831+
836832
if (dynamicChildren) {
837833
patchBlockChildren(
838834
n1.dynamicChildren!,
@@ -869,15 +865,7 @@ function baseCreateRenderer(
869865
// (i.e. at the exact same position in the source template)
870866
if (patchFlag & PatchFlags.FULL_PROPS) {
871867
// element props contain dynamic keys, full diff needed
872-
patchProps(
873-
el,
874-
n2,
875-
oldProps,
876-
newProps,
877-
parentComponent,
878-
parentSuspense,
879-
namespace,
880-
)
868+
patchProps(el, oldProps, newProps, parentComponent, namespace)
881869
} else {
882870
// class
883871
// this flag is matched when the element has dynamic class bindings.
@@ -908,17 +896,7 @@ function baseCreateRenderer(
908896
const next = newProps[key]
909897
// #1471 force patch value
910898
if (next !== prev || key === 'value') {
911-
hostPatchProp(
912-
el,
913-
key,
914-
prev,
915-
next,
916-
namespace,
917-
n1.children as VNode[],
918-
parentComponent,
919-
parentSuspense,
920-
unmountChildren,
921-
)
899+
hostPatchProp(el, key, prev, next, namespace, parentComponent)
922900
}
923901
}
924902
}
@@ -933,15 +911,7 @@ function baseCreateRenderer(
933911
}
934912
} else if (!optimized && dynamicChildren == null) {
935913
// unoptimized, full diff
936-
patchProps(
937-
el,
938-
n2,
939-
oldProps,
940-
newProps,
941-
parentComponent,
942-
parentSuspense,
943-
namespace,
944-
)
914+
patchProps(el, oldProps, newProps, parentComponent, namespace)
945915
}
946916

947917
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
@@ -998,11 +968,9 @@ function baseCreateRenderer(
998968

999969
const patchProps = (
1000970
el: RendererElement,
1001-
vnode: VNode,
1002971
oldProps: Data,
1003972
newProps: Data,
1004973
parentComponent: ComponentInternalInstance | null,
1005-
parentSuspense: SuspenseBoundary | null,
1006974
namespace: ElementNamespace,
1007975
) => {
1008976
if (oldProps !== newProps) {
@@ -1015,10 +983,7 @@ function baseCreateRenderer(
1015983
oldProps[key],
1016984
null,
1017985
namespace,
1018-
vnode.children as VNode[],
1019986
parentComponent,
1020-
parentSuspense,
1021-
unmountChildren,
1022987
)
1023988
}
1024989
}
@@ -1030,17 +995,7 @@ function baseCreateRenderer(
1030995
const prev = oldProps[key]
1031996
// defer patching value
1032997
if (next !== prev && key !== 'value') {
1033-
hostPatchProp(
1034-
el,
1035-
key,
1036-
prev,
1037-
next,
1038-
namespace,
1039-
vnode.children as VNode[],
1040-
parentComponent,
1041-
parentSuspense,
1042-
unmountChildren,
1043-
)
998+
hostPatchProp(el, key, prev, next, namespace, parentComponent)
1044999
}
10451000
}
10461001
if ('value' in newProps) {

packages/runtime-dom/__tests__/patchProps.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { patchProp } from '../src/patchProp'
2-
import { h, render } from '../src'
2+
import { h, nextTick, ref, render } from '../src'
33

44
describe('runtime-dom: props patching', () => {
55
test('basic', () => {
@@ -133,6 +133,25 @@ describe('runtime-dom: props patching', () => {
133133
expect(fn).toHaveBeenCalled()
134134
})
135135

136+
test('patch innerHTML porp', async () => {
137+
const root = document.createElement('div')
138+
const state = ref(false)
139+
const Comp = {
140+
render: () => {
141+
if (state.value) {
142+
return h('div', [h('del', null, 'baz')])
143+
} else {
144+
return h('div', { innerHTML: 'baz' })
145+
}
146+
},
147+
}
148+
render(h(Comp), root)
149+
expect(root.innerHTML).toBe(`<div>baz</div>`)
150+
state.value = true
151+
await nextTick()
152+
expect(root.innerHTML).toBe(`<div><del>baz</del></div>`)
153+
})
154+
136155
test('textContent unmount prev children', () => {
137156
const fn = vi.fn()
138157
const comp = {

packages/runtime-dom/src/modules/props.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@ export function patchDOMProp(
1010
el: any,
1111
key: string,
1212
value: any,
13-
// the following args are passed only due to potential innerHTML/textContent
14-
// overriding existing VNodes, in which case the old tree must be properly
15-
// unmounted.
16-
prevChildren: any,
1713
parentComponent: any,
18-
parentSuspense: any,
19-
unmountChildren: any,
2014
) {
2115
if (key === 'innerHTML' || key === 'textContent') {
22-
if (prevChildren) {
23-
unmountChildren(prevChildren, parentComponent, parentSuspense)
24-
}
25-
el[key] = value == null ? '' : value
16+
// null value case is handled in renderer patchElement before patching
17+
// children
18+
if (value === null) return
19+
el[key] = value
2620
return
2721
}
2822

packages/runtime-dom/src/patchProp.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
2121
prevValue,
2222
nextValue,
2323
namespace,
24-
prevChildren,
2524
parentComponent,
26-
parentSuspense,
27-
unmountChildren,
2825
) => {
2926
const isSVG = namespace === 'svg'
3027
if (key === 'class') {
@@ -43,15 +40,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
4340
? ((key = key.slice(1)), false)
4441
: shouldSetAsProp(el, key, nextValue, isSVG)
4542
) {
46-
patchDOMProp(
47-
el,
48-
key,
49-
nextValue,
50-
prevChildren,
51-
parentComponent,
52-
parentSuspense,
53-
unmountChildren,
54-
)
43+
patchDOMProp(el, key, nextValue, parentComponent)
5544
// #6007 also set form state as attributes so they work with
5645
// <input type="reset"> or libs / extensions that expect attributes
5746
// #11163 custom elements may use value as an prop and set it as object

0 commit comments

Comments
 (0)