Skip to content

Commit 7ef293a

Browse files
committed
fix: prevent reusing projection if is marked as deleted
1 parent ea02b0f commit 7ef293a

File tree

4 files changed

+117
-25
lines changed

4 files changed

+117
-25
lines changed

.changeset/tasty-penguins-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: prevent reusing projection if is marked as deleted

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,8 @@ export const vnode_diff = (
458458
slotName,
459459
(id) => vnode_locate(container.rootVNode, id)
460460
);
461+
// if projection is marked as deleted then we need to create a new one
462+
vCurrent = vCurrent && vCurrent[VNodeProps.flags] & VNodeFlags.Deleted ? null : vCurrent;
461463
if (vCurrent == null) {
462464
vNewNode = vnode_newVirtual();
463465
// you may be tempted to add the projection into the current parent, but
@@ -566,8 +568,8 @@ export const vnode_diff = (
566568
while (vCurrent) {
567569
const toRemove = vCurrent;
568570
advanceToNextSibling();
569-
cleanup(container, toRemove);
570571
if (vParent === vnode_getParent(toRemove)) {
572+
cleanup(container, toRemove);
571573
// If we are diffing projection than the parent is not the parent of the node.
572574
// If that is the case we don't want to remove the node from the parent.
573575
vnode_remove(journal, vParent as ElementVNode | VirtualVNode, toRemove, true);
@@ -1290,6 +1292,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
12901292
let vCursor: VNode | null = vNode;
12911293
// Depth first traversal
12921294
if (vnode_isTextVNode(vNode)) {
1295+
markVNodeAsDeleted(vCursor);
12931296
// Text nodes don't have subscriptions or children;
12941297
return;
12951298
}
@@ -1368,6 +1371,8 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
13681371
return;
13691372
}
13701373
}
1374+
} else if (type & VNodeFlags.Text) {
1375+
markVNodeAsDeleted(vCursor);
13711376
}
13721377
// Out of children
13731378
if (vCursor === vNode) {

packages/qwik/src/core/client/vnode.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -774,53 +774,71 @@ export const vnode_journalToString = (journal: VNodeJournal): string => {
774774

775775
function stringify(...args: any[]) {
776776
lines.push(
777-
' ' +
778-
args
779-
.map((arg) => {
780-
if (typeof arg === 'string') {
781-
return arg;
782-
} else if (arg && isHtmlElement(arg)) {
783-
const html = arg.outerHTML;
784-
const idx = html.indexOf('>');
785-
return '\n ' + (idx > 0 ? html.substring(0, idx + 1) : html);
786-
} else if (arg && isText(arg)) {
787-
return JSON.stringify(arg.nodeValue);
788-
} else {
789-
return String(arg);
790-
}
791-
})
792-
.join(' ')
777+
args
778+
.map((arg) => {
779+
if (typeof arg === 'string') {
780+
return arg;
781+
} else if (arg && isHtmlElement(arg)) {
782+
const html = arg.outerHTML;
783+
const hasChildNodes = !!arg.firstElementChild;
784+
const idx = html.indexOf('>');
785+
const lastIdx = html.lastIndexOf('<');
786+
return idx > 0 && hasChildNodes
787+
? html.substring(0, idx + 1) + '...' + html.substring(lastIdx)
788+
: html;
789+
} else if (arg && isText(arg)) {
790+
return JSON.stringify(arg.nodeValue);
791+
} else {
792+
return String(arg);
793+
}
794+
})
795+
.join(' ')
793796
);
794797
}
795798

796799
while (idx < length) {
797800
const op = journal[idx++] as VNodeJournalOpCode;
798801
switch (op) {
799802
case VNodeJournalOpCode.SetText:
800-
stringify('SetText', journal[idx++], journal[idx++]);
803+
stringify('SetText');
804+
stringify(' ', journal[idx++]);
805+
stringify(' -->', journal[idx++]);
801806
break;
802807
case VNodeJournalOpCode.SetAttribute:
803-
stringify('SetAttribute', journal[idx++], journal[idx++], journal[idx++]);
808+
stringify('SetAttribute');
809+
stringify(' ', journal[idx++]);
810+
stringify(' key', journal[idx++]);
811+
stringify(' val', journal[idx++]);
804812
break;
805813
case VNodeJournalOpCode.HoistStyles:
806814
stringify('HoistStyles');
807815
break;
808-
case VNodeJournalOpCode.Remove:
809-
stringify('Remove', journal[idx++]);
816+
case VNodeJournalOpCode.Remove: {
817+
stringify('Remove');
818+
const parent = journal[idx++];
819+
stringify(' ', parent);
810820
let nodeToRemove: any;
811821
while (idx < length && typeof (nodeToRemove = journal[idx]) !== 'number') {
812-
stringify(' ', nodeToRemove);
822+
stringify(' -->', nodeToRemove);
813823
idx++;
814824
}
815825
break;
816-
case VNodeJournalOpCode.Insert:
817-
stringify('Insert', journal[idx++], journal[idx++]);
826+
}
827+
case VNodeJournalOpCode.Insert: {
828+
stringify('Insert');
829+
const parent = journal[idx++];
830+
const insertBefore = journal[idx++];
831+
stringify(' ', parent);
818832
let newChild: any;
819833
while (idx < length && typeof (newChild = journal[idx]) !== 'number') {
820-
stringify(' ', newChild);
834+
stringify(' -->', newChild);
821835
idx++;
822836
}
837+
if (insertBefore) {
838+
stringify(' ', insertBefore);
839+
}
823840
break;
841+
}
824842
}
825843
}
826844
lines.push('END JOURNAL');
@@ -969,6 +987,7 @@ export const vnode_insertBefore = (
969987
// : insertBefore;
970988
const domParentVNode = vnode_getDomParentVNode(parent);
971989
const parentNode = domParentVNode && domParentVNode[ElementVNodeProps.element];
990+
972991
if (parentNode) {
973992
const domChildren = vnode_getDomChildrenWithCorrectNamespacesToInsert(
974993
journal,
@@ -1037,6 +1056,7 @@ export const vnode_remove = (
10371056
if (vnode_isTextVNode(vToRemove)) {
10381057
vnode_ensureTextInflated(journal, vToRemove);
10391058
}
1059+
10401060
const vPrevious = vToRemove[VNodeProps.previousSibling];
10411061
const vNext = vToRemove[VNodeProps.nextSibling];
10421062
if (vPrevious) {

packages/qwik/src/core/tests/projection.spec.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,68 @@ describe.each([
11271127
);
11281128
});
11291129

1130+
it('should cleanup removed projection and remove it from parent component', async () => {
1131+
const SomeCmp = component$((props: { toggle: boolean }) => {
1132+
return <>{props.toggle && <Slot />}</>;
1133+
});
1134+
1135+
const Cmp = component$(() => {
1136+
const toggle = useSignal(true);
1137+
1138+
return (
1139+
<>
1140+
<button onClick$={() => (toggle.value = !toggle.value)}></button>
1141+
<SomeCmp toggle={toggle.value}>
1142+
{toggle.value && <h1>Title 1</h1>}
1143+
{!toggle.value && <h1>Title 2</h1>}
1144+
</SomeCmp>
1145+
</>
1146+
);
1147+
});
1148+
1149+
const { document, vNode } = await render(<Cmp />, { debug: DEBUG });
1150+
expect(vNode).toMatchVDOM(
1151+
<Component ssr-required>
1152+
<Fragment ssr-required>
1153+
<button></button>
1154+
<Component ssr-required>
1155+
<Fragment ssr-required>
1156+
<Projection ssr-required>
1157+
<h1>Title 1</h1>
1158+
</Projection>
1159+
</Fragment>
1160+
</Component>
1161+
</Fragment>
1162+
</Component>
1163+
);
1164+
await trigger(document.body, 'button', 'click');
1165+
expect(vNode).toMatchVDOM(
1166+
<Component ssr-required>
1167+
<Fragment ssr-required>
1168+
<button></button>
1169+
<Component ssr-required>
1170+
<Fragment ssr-required></Fragment>
1171+
</Component>
1172+
</Fragment>
1173+
</Component>
1174+
);
1175+
await trigger(document.body, 'button', 'click');
1176+
expect(vNode).toMatchVDOM(
1177+
<Component ssr-required>
1178+
<Fragment ssr-required>
1179+
<button></button>
1180+
<Component ssr-required>
1181+
<Fragment ssr-required>
1182+
<Projection ssr-required>
1183+
<h1>Title 1</h1>
1184+
</Projection>
1185+
</Fragment>
1186+
</Component>
1187+
</Fragment>
1188+
</Component>
1189+
);
1190+
});
1191+
11301192
describe('ensureProjectionResolved', () => {
11311193
(globalThis as any).log = [] as string[];
11321194
beforeEach(() => {

0 commit comments

Comments
 (0)