Skip to content

Commit dd133bc

Browse files
authored
Merge pull request #8185 from QwikDev/v2-fix-projections-resolving
fix: finding projections after client partial rerender
2 parents 76fdc14 + 12fee1f commit dd133bc

File tree

6 files changed

+83
-5
lines changed

6 files changed

+83
-5
lines changed

.changeset/sweet-candles-arrive.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: finding projections after client partial rerender

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1847,6 +1847,8 @@ function materializeFromVNodeData(
18471847
return !nodeIsElement || (nodeIsElement && shouldSkipElement(node));
18481848
};
18491849

1850+
let components: VirtualVNode[] | null = null;
1851+
18501852
processVNodeData(vData, (peek, consumeValue, consume, getChar, nextToConsumeIdx) => {
18511853
if (isNumber(peek())) {
18521854
// Element counts get encoded as numbers.
@@ -1871,6 +1873,7 @@ function materializeFromVNodeData(
18711873
} else if (peek() === VNodeDataChar.SCOPED_STYLE) {
18721874
vParent.setAttr(QScopedStyle, consumeValue(), null);
18731875
} else if (peek() === VNodeDataChar.RENDER_FN) {
1876+
(components ||= []).push(vParent as VirtualVNode);
18741877
vParent.setAttr(OnRenderProp, consumeValue(), null);
18751878
} else if (peek() === VNodeDataChar.ID) {
18761879
if (!container) {
@@ -1956,6 +1959,15 @@ function materializeFromVNodeData(
19561959
// Text nodes get encoded as alphanumeric characters.
19571960
}
19581961
});
1962+
if (components) {
1963+
if (!container) {
1964+
container = getDomContainer(element);
1965+
}
1966+
for (const component of components as VirtualVNode[]) {
1967+
container.ensureProjectionResolved(component);
1968+
}
1969+
components = null;
1970+
}
19591971
vParent.lastChild = vLast;
19601972
return vFirst!;
19611973
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
2222
import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture';
2323
import { beforeEach, describe, expect, it } from 'vitest';
2424
import { vnode_locate } from '../client/vnode';
25-
import { HTML_NS, QContainerAttr, SVG_NS } from '../shared/utils/markers';
25+
import { HTML_NS, QContainerAttr, QDefaultSlot, SVG_NS } from '../shared/utils/markers';
2626
import { QContainerValue } from '../shared/types';
27+
import { VNodeFlags } from '../client/types';
28+
import { VirtualVNode } from '../client/vnode-impl';
2729

2830
const DEBUG = false;
2931

@@ -1572,6 +1574,23 @@ describe.each([
15721574
</div>
15731575
);
15741576
});
1577+
1578+
it('should resolve projection when component is resumed', async () => {
1579+
const Child = component$(() => {
1580+
return <div></div>;
1581+
});
1582+
const Cmp = component$(() => {
1583+
return <Slot />;
1584+
});
1585+
const { vNode } = await render(
1586+
<Cmp>
1587+
<Child />
1588+
</Cmp>,
1589+
{ debug: DEBUG }
1590+
);
1591+
expect((vNode!.flags & VNodeFlags.Resolved) === VNodeFlags.Resolved).toBe(true);
1592+
expect(vNode?.getProp(QDefaultSlot, null)).toBeInstanceOf(VirtualVNode);
1593+
});
15751594
});
15761595

15771596
describe('q:template', () => {

packages/qwik/src/core/tests/use-visible-task.spec.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
1919
import { describe, expect, it } from 'vitest';
2020
import { ErrorProvider } from '../../testing/rendering.unit-util';
2121
import { delay } from '../shared/utils/promises';
22+
import { ELEMENT_SEQ } from '../../server/qwik-copy';
23+
import { Task, TaskFlags } from '../use/use-task';
24+
import { USE_ON_LOCAL } from '../shared/utils/markers';
2225

2326
const debug = false; //true;
2427
Error.stackTraceLimit = 100;
@@ -796,6 +799,43 @@ describe.each([
796799
});
797800

798801
describe('regression', () => {
802+
it('should not double-register events on component re-render', async () => {
803+
const Cmp = component$(() => {
804+
const count = useSignal(0);
805+
806+
useVisibleTask$(() => {});
807+
// component rerender
808+
count.value;
809+
810+
return (
811+
<div>
812+
<button onClick$={() => count.value++}>Click</button>
813+
</div>
814+
);
815+
});
816+
817+
const { document, vNode, container } = await render(<Cmp />, { debug });
818+
819+
if (render === ssrRenderToDom) {
820+
await trigger(document.body, 'div', 'qvisible');
821+
}
822+
const seq = vNode!.getProp<any[]>(ELEMENT_SEQ, container.$getObjectById$)!;
823+
const task = seq.find((task) => task instanceof Task)!;
824+
expect((task.$flags$ & TaskFlags.EVENTS_REGISTERED) === TaskFlags.EVENTS_REGISTERED).toBe(
825+
false
826+
);
827+
if (render === ssrRenderToDom) {
828+
// only on SSR after resuming we have no useOn props
829+
expect(vNode!.getProp(USE_ON_LOCAL, null)).toBeNull();
830+
}
831+
832+
await trigger(document.body, 'button', 'click');
833+
expect((task.$flags$ & TaskFlags.EVENTS_REGISTERED) === TaskFlags.EVENTS_REGISTERED).toBe(
834+
true
835+
);
836+
expect(vNode!.getProp(USE_ON_LOCAL, null)).not.toBeNull();
837+
});
838+
799839
it('#1717 - custom hooks should work', async () => {
800840
const Issue1717 = component$(() => {
801841
const val1 = useDelay('valueA');

packages/qwik/src/core/use/use-task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const enum TaskFlags {
2323
RESOURCE = 1 << 2,
2424
DIRTY = 1 << 3,
2525
RENDER_BLOCKING = 1 << 4,
26+
EVENTS_REGISTERED = 1 << 5,
2627
}
2728

2829
// <docs markdown="../readme.md#Tracker">

packages/qwik/src/core/use/use-visible-task.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,24 @@ export const useVisibleTaskQrl = (qrl: QRL<TaskFn>, opts?: OnVisibleTaskOptions)
3131
const { val, set, i, iCtx } = useSequentialScope<Task<TaskFn>>();
3232
const eagerness = opts?.strategy ?? 'intersection-observer';
3333
if (val) {
34-
if (isServerPlatform()) {
35-
useRunTask(val, eagerness);
34+
if (!(val.$flags$ & TaskFlags.EVENTS_REGISTERED) && !isServerPlatform()) {
35+
val.$flags$ |= TaskFlags.EVENTS_REGISTERED;
36+
useRegisterTaskEvents(val, eagerness);
3637
}
3738
return;
3839
}
3940
assertQrl(qrl);
4041

4142
const task = new Task(TaskFlags.VISIBLE_TASK, i, iCtx.$hostElement$, qrl, undefined, null);
4243
set(task);
43-
useRunTask(task, eagerness);
44+
useRegisterTaskEvents(task, eagerness);
4445
if (!isServerPlatform()) {
4546
(qrl as QRLInternal).resolve(iCtx.$element$);
4647
iCtx.$container$.$scheduler$(ChoreType.VISIBLE, task);
4748
}
4849
};
4950

50-
export const useRunTask = (task: Task, eagerness: VisibleTaskStrategy | undefined) => {
51+
export const useRegisterTaskEvents = (task: Task, eagerness: VisibleTaskStrategy | undefined) => {
5152
if (eagerness === 'intersection-observer') {
5253
useOn('qvisible', getTaskHandlerQrl(task));
5354
} else if (eagerness === 'document-ready') {

0 commit comments

Comments
 (0)