Skip to content

Commit 443029e

Browse files
committed
fix: event handling for headless components
1 parent bc6b37d commit 443029e

File tree

3 files changed

+81
-17
lines changed

3 files changed

+81
-17
lines changed

packages/qwik/src/core/shared/component-execution.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import {
2424
USE_ON_LOCAL_SEQ_IDX,
2525
} from './utils/markers';
2626
import { MAX_RETRY_ON_PROMISE_COUNT, isPromise, maybeThen, safeCall } from './utils/promises';
27-
import type { ValueOrPromise } from './utils/types';
27+
import { isArray, isPrimitive, type ValueOrPromise } from './utils/types';
2828
import { getSubscriber } from '../reactive-primitives/subscriber';
2929
import { EffectProperty } from '../reactive-primitives/types';
30+
import { EventNameJSXScope } from './utils/event-names';
3031

3132
/**
3233
* Use `executeComponent` to execute a component.
@@ -170,7 +171,10 @@ function addUseOnEvents(
170171
if (jsxElement) {
171172
addUseOnEvent(jsxElement, 'document:onQinit$', useOnEvents[key]);
172173
}
173-
} else if (key.startsWith('document:') || key.startsWith('window:')) {
174+
} else if (
175+
key.startsWith(EventNameJSXScope.document) ||
176+
key.startsWith(EventNameJSXScope.window)
177+
) {
174178
const [jsxElement, jsx] = addScriptNodeForInvisibleComponents(jsxResult);
175179
jsxResult = jsx;
176180
if (jsxElement) {
@@ -186,7 +190,11 @@ function addUseOnEvents(
186190
);
187191
}
188192
} else if (jsxElement) {
189-
addUseOnEvent(jsxElement, key, useOnEvents[key]);
193+
if (jsxElement.type === 'script' && key === 'onQvisible$') {
194+
addUseOnEvent(jsxElement, 'document:onQinit$', useOnEvents[key]);
195+
} else {
196+
addUseOnEvent(jsxElement, key, useOnEvents[key]);
197+
}
190198
}
191199
}
192200
}
@@ -222,7 +230,7 @@ function findFirstStringJSX(jsx: JSXOutput): ValueOrPromise<JSXNodeInternal<stri
222230
return jsx as JSXNodeInternal<string>;
223231
}
224232
queue.push(jsx.children);
225-
} else if (Array.isArray(jsx)) {
233+
} else if (isArray(jsx)) {
226234
queue.push(...jsx);
227235
} else if (isPromise(jsx)) {
228236
return maybeThen<JSXOutput, JSXNodeInternal<string> | null>(jsx, (jsx) =>
@@ -239,33 +247,40 @@ function addScriptNodeForInvisibleComponents(
239247
jsx: JSXOutput
240248
): [JSXNodeInternal<string> | null, JSXOutput | null] {
241249
if (isJSXNode(jsx)) {
242-
const jsxElement = new JSXNodeImpl(
243-
'script',
244-
{},
245-
{
246-
type: 'placeholder',
247-
hidden: '',
248-
},
249-
null,
250-
3
251-
);
250+
const jsxElement = createScriptNode();
252251
if (jsx.type === Slot) {
253252
return [jsxElement, _jsxSorted(Fragment, null, null, [jsx, jsxElement], 0, null)];
254253
}
255254

256255
if (jsx.children == null) {
257256
jsx.children = jsxElement;
258-
} else if (Array.isArray(jsx.children)) {
257+
} else if (isArray(jsx.children)) {
259258
jsx.children.push(jsxElement);
260259
} else {
261260
jsx.children = [jsx.children, jsxElement];
262261
}
263262
return [jsxElement, jsx];
264-
} else if (Array.isArray(jsx) && jsx.length) {
263+
} else if (isArray(jsx) && jsx.length) {
265264
// get first element
266265
const [jsxElement, _] = addScriptNodeForInvisibleComponents(jsx[0]);
267266
return [jsxElement, jsx];
267+
} else if (isPrimitive(jsx)) {
268+
const jsxElement = createScriptNode();
269+
return [jsxElement, _jsxSorted(Fragment, null, null, [jsx, jsxElement], 0, null)];
268270
}
269271

270-
return [null, null];
272+
return [null, jsx];
273+
}
274+
275+
function createScriptNode(): JSXNodeInternal<string> {
276+
return new JSXNodeImpl(
277+
'script',
278+
{},
279+
{
280+
type: 'placeholder',
281+
hidden: '',
282+
},
283+
null,
284+
3
285+
);
271286
}

packages/qwik/src/core/shared/utils/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ export const isFunction = <T extends (...args: any) => any>(v: unknown): v is T
2424
return typeof v === 'function';
2525
};
2626

27+
export const isPrimitive = (v: unknown): v is string | number | boolean | null | undefined => {
28+
return (
29+
v === null ||
30+
v === undefined ||
31+
typeof v === 'string' ||
32+
typeof v === 'number' ||
33+
typeof v === 'boolean' ||
34+
typeof v === 'symbol'
35+
);
36+
};
37+
2738
/**
2839
* Type representing a value which is either resolve or a promise.
2940
*

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,44 @@ describe.each([
367367
});
368368
});
369369

370+
it('should add q:visible event if only script tag is present', async () => {
371+
(globalThis as any).counter = 0;
372+
const Cmp = component$(() => {
373+
useVisibleTask$(() => {
374+
(globalThis as any).counter++;
375+
});
376+
return <script />;
377+
});
378+
379+
const { document } = await render(<Cmp />, { debug });
380+
if (render === ssrRenderToDom) {
381+
await trigger(document.body, 'script', ':document:qinit');
382+
}
383+
384+
expect((globalThis as any).counter).toBe(1);
385+
386+
(globalThis as any).counter = undefined;
387+
});
388+
389+
it('should add script tag for visible task if only primitive child is present', async () => {
390+
(globalThis as any).counter = 0;
391+
const Cmp = component$(() => {
392+
useVisibleTask$(() => {
393+
(globalThis as any).counter++;
394+
});
395+
return 123;
396+
});
397+
398+
const { document } = await render(<Cmp />, { debug });
399+
if (render === ssrRenderToDom) {
400+
await trigger(document.body, 'script[hidden]', ':document:qinit');
401+
}
402+
403+
expect((globalThis as any).counter).toBe(1);
404+
405+
(globalThis as any).counter = undefined;
406+
});
407+
370408
describe(render.name + ': queue', () => {
371409
it('should execute dependant visible tasks', async () => {
372410
(globalThis as any).log = [] as string[];

0 commit comments

Comments
 (0)