Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sweet-candles-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: finding projections after client partial rerender
12 changes: 12 additions & 0 deletions packages/qwik/src/core/client/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,8 @@ function materializeFromVNodeData(
return !nodeIsElement || (nodeIsElement && shouldSkipElement(node));
};

let components: VirtualVNode[] | null = null;

processVNodeData(vData, (peek, consumeValue, consume, getChar, nextToConsumeIdx) => {
if (isNumber(peek())) {
// Element counts get encoded as numbers.
Expand All @@ -1871,6 +1873,7 @@ function materializeFromVNodeData(
} else if (peek() === VNodeDataChar.SCOPED_STYLE) {
vParent.setAttr(QScopedStyle, consumeValue(), null);
} else if (peek() === VNodeDataChar.RENDER_FN) {
(components ||= []).push(vParent as VirtualVNode);
vParent.setAttr(OnRenderProp, consumeValue(), null);
} else if (peek() === VNodeDataChar.ID) {
if (!container) {
Expand Down Expand Up @@ -1956,6 +1959,15 @@ function materializeFromVNodeData(
// Text nodes get encoded as alphanumeric characters.
}
});
if (components) {
if (!container) {
container = getDomContainer(element);
}
for (const component of components as VirtualVNode[]) {
container.ensureProjectionResolved(component);
}
components = null;
}
vParent.lastChild = vLast;
return vFirst!;
}
Expand Down
21 changes: 20 additions & 1 deletion packages/qwik/src/core/tests/projection.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture';
import { beforeEach, describe, expect, it } from 'vitest';
import { vnode_locate } from '../client/vnode';
import { HTML_NS, QContainerAttr, SVG_NS } from '../shared/utils/markers';
import { HTML_NS, QContainerAttr, QDefaultSlot, SVG_NS } from '../shared/utils/markers';
import { QContainerValue } from '../shared/types';
import { VNodeFlags } from '../client/types';
import { VirtualVNode } from '../client/vnode-impl';

const DEBUG = false;

Expand Down Expand Up @@ -1572,6 +1574,23 @@ describe.each([
</div>
);
});

it('should resolve projection when component is resumed', async () => {
const Child = component$(() => {
return <div></div>;
});
const Cmp = component$(() => {
return <Slot />;
});
const { vNode } = await render(
<Cmp>
<Child />
</Cmp>,
{ debug: DEBUG }
);
expect((vNode!.flags & VNodeFlags.Resolved) === VNodeFlags.Resolved).toBe(true);
expect(vNode?.getProp(QDefaultSlot, null)).toBeInstanceOf(VirtualVNode);
});
});

describe('q:template', () => {
Expand Down
40 changes: 40 additions & 0 deletions packages/qwik/src/core/tests/use-visible-task.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
import { describe, expect, it } from 'vitest';
import { ErrorProvider } from '../../testing/rendering.unit-util';
import { delay } from '../shared/utils/promises';
import { ELEMENT_SEQ } from '../../server/qwik-copy';
import { Task, TaskFlags } from '../use/use-task';
import { USE_ON_LOCAL } from '../shared/utils/markers';

const debug = false; //true;
Error.stackTraceLimit = 100;
Expand Down Expand Up @@ -796,6 +799,43 @@ describe.each([
});

describe('regression', () => {
it('should not double-register events on component re-render', async () => {
const Cmp = component$(() => {
const count = useSignal(0);

useVisibleTask$(() => {});
// component rerender
count.value;

return (
<div>
<button onClick$={() => count.value++}>Click</button>
</div>
);
});

const { document, vNode, container } = await render(<Cmp />, { debug });

if (render === ssrRenderToDom) {
await trigger(document.body, 'div', 'qvisible');
}
const seq = vNode!.getProp<any[]>(ELEMENT_SEQ, container.$getObjectById$)!;
const task = seq.find((task) => task instanceof Task)!;
expect((task.$flags$ & TaskFlags.EVENTS_REGISTERED) === TaskFlags.EVENTS_REGISTERED).toBe(
false
);
if (render === ssrRenderToDom) {
// only on SSR after resuming we have no useOn props
expect(vNode!.getProp(USE_ON_LOCAL, null)).toBeNull();
}

await trigger(document.body, 'button', 'click');
expect((task.$flags$ & TaskFlags.EVENTS_REGISTERED) === TaskFlags.EVENTS_REGISTERED).toBe(
true
);
expect(vNode!.getProp(USE_ON_LOCAL, null)).not.toBeNull();
});

it('#1717 - custom hooks should work', async () => {
const Issue1717 = component$(() => {
const val1 = useDelay('valueA');
Expand Down
1 change: 1 addition & 0 deletions packages/qwik/src/core/use/use-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const enum TaskFlags {
RESOURCE = 1 << 2,
DIRTY = 1 << 3,
RENDER_BLOCKING = 1 << 4,
EVENTS_REGISTERED = 1 << 5,
}

// <docs markdown="../readme.md#Tracker">
Expand Down
9 changes: 5 additions & 4 deletions packages/qwik/src/core/use/use-visible-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,24 @@ export const useVisibleTaskQrl = (qrl: QRL<TaskFn>, opts?: OnVisibleTaskOptions)
const { val, set, i, iCtx } = useSequentialScope<Task<TaskFn>>();
const eagerness = opts?.strategy ?? 'intersection-observer';
if (val) {
if (isServerPlatform()) {
useRunTask(val, eagerness);
if (!(val.$flags$ & TaskFlags.EVENTS_REGISTERED) && !isServerPlatform()) {
val.$flags$ |= TaskFlags.EVENTS_REGISTERED;
useRegisterTaskEvents(val, eagerness);
}
return;
}
assertQrl(qrl);

const task = new Task(TaskFlags.VISIBLE_TASK, i, iCtx.$hostElement$, qrl, undefined, null);
set(task);
useRunTask(task, eagerness);
useRegisterTaskEvents(task, eagerness);
if (!isServerPlatform()) {
(qrl as QRLInternal).resolve(iCtx.$element$);
iCtx.$container$.$scheduler$(ChoreType.VISIBLE, task);
}
};

export const useRunTask = (task: Task, eagerness: VisibleTaskStrategy | undefined) => {
export const useRegisterTaskEvents = (task: Task, eagerness: VisibleTaskStrategy | undefined) => {
if (eagerness === 'intersection-observer') {
useOn('qvisible', getTaskHandlerQrl(task));
} else if (eagerness === 'document-ready') {
Expand Down
Loading