Skip to content

Commit 79f1f42

Browse files
authored
fix: rendering state relative to q:container (#322)
1 parent 0921889 commit 79f1f42

32 files changed

+268
-217
lines changed

src/bootloader-shared.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,11 @@
2424
* @param href
2525
* @returns
2626
*/
27-
export const qrlResolver = (
28-
doc: Document,
29-
element: Element | null,
30-
eventUrl?: string | null,
31-
_url?: string,
32-
_base?: string | URL
33-
): URL | undefined => {
34-
if (eventUrl === undefined) {
35-
// recursive call
36-
if (element) {
37-
_url = element.getAttribute('q:base')!;
38-
_base = qrlResolver(
39-
doc,
40-
element.parentNode && (element.parentNode as HTMLElement).closest('[q\\:base]')
41-
);
42-
} else {
43-
_url = doc.baseURI;
44-
}
45-
} else if (eventUrl) {
46-
_url = eventUrl;
47-
_base = qrlResolver(doc, element!.closest('[q\\:base]'));
48-
}
49-
return _url ? new URL(_url, _base) : undefined;
27+
export const qrlResolver = (element: Element, eventUrl: string): URL => {
28+
const doc = element.ownerDocument!;
29+
const containerEl = element.closest('[q\\:container]');
30+
const base = new URL(containerEl?.getAttribute('q:base') ?? doc.baseURI, doc.baseURI);
31+
return new URL(eventUrl, base);
5032
};
5133

5234
const error = (msg: string) => {
@@ -92,7 +74,7 @@ export const qwikLoader = (doc: Document, hasInitialized?: boolean | number) =>
9274
ev.preventDefault();
9375
}
9476
for (const qrl of attrValue.split('\n')) {
95-
const url = qrlResolver(doc, element, qrl);
77+
const url = qrlResolver(element, qrl);
9678
if (url) {
9779
const symbolName = getSymbolName(url);
9880
const module =
@@ -223,7 +205,7 @@ export const setupPrefetching = (
223205
const name = attr.name;
224206
const value = attr.value;
225207
if (name.startsWith('on:') && value) {
226-
const url = qrlResolver(doc, element, value)!;
208+
const url = qrlResolver(element, value)!;
227209
url.hash = url.search = '';
228210
const key = url.toString() + '.js';
229211
if (!qrlCache[key]) {

src/bootloader-shared.unit.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,36 +100,29 @@ describe('qwikloader', () => {
100100

101101
it('should resolve full URL', () => {
102102
const div = doc.createElement('div');
103-
expect(String(qrlResolver(doc, div, 'http://foo.bar/baz'))).toEqual('http://foo.bar/baz');
103+
expect(String(qrlResolver(div, 'http://foo.bar/baz'))).toEqual('http://foo.bar/baz');
104104
});
105105

106106
it('should resolve relative URL against base', () => {
107107
const div = doc.createElement('div');
108-
expect(String(qrlResolver(doc, div, './bar'))).toEqual('http://document.qwik.dev/bar');
108+
expect(String(qrlResolver(div, './bar'))).toEqual('http://document.qwik.dev/bar');
109109
});
110110

111111
it('should resolve relative URL against q:base', () => {
112112
const div = doc.createElement('div');
113-
div.setAttribute('q:base', '../baz/');
114-
expect(String(qrlResolver(doc, div, './bar'))).toEqual('http://document.qwik.dev/baz/bar');
113+
div.setAttribute('q:container', '');
114+
div.setAttribute('q:base', '/baz/');
115+
expect(String(qrlResolver(div, './bar'))).toEqual('http://document.qwik.dev/baz/bar');
115116
});
116117

117118
it('should resolve relative URL against nested q:base', () => {
118119
const div = doc.createElement('div');
119120
const parent = doc.createElement('parent');
120121
doc.body.appendChild(parent);
121122
parent.appendChild(div);
123+
parent.setAttribute('q:container', '');
122124
parent.setAttribute('q:base', './parent/');
123-
div.setAttribute('q:base', './child/');
124-
expect(String(qrlResolver(doc, div, './bar'))).toEqual(
125-
'http://document.qwik.dev/parent/child/bar'
126-
);
127-
});
128-
129-
it('do nothing for null/undefined/empty string', () => {
130-
const div = doc.createElement('div');
131-
expect(qrlResolver(doc, null, null)).toBeFalsy();
132-
expect(qrlResolver(doc, div, '')).toBeFalsy();
125+
expect(String(qrlResolver(div, './bar'))).toEqual('http://document.qwik.dev/parent/bar');
133126
});
134127
});
135128

src/core/component/component-ctx.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import { styleContent, styleHost } from './qrl-styles';
77
import { newInvokeContext, useInvoke } from '../use/use-core';
88
import type { QContext } from '../props/props';
99
import { processNode } from '../render/jsx/jsx-runtime';
10+
import type { QRLInternal } from '../import/qrl-class';
11+
12+
export interface RenderFactoryOutput {
13+
renderQRL: QRLInternal;
14+
waitOn: any[];
15+
}
1016

1117
export const firstRenderComponent = (rctx: RenderContext, ctx: QContext) => {
1218
ctx.element.setAttribute(QHostAttr, '');

src/core/component/component.public.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { toQrlOrError } from '../import/qrl';
22
import type { QRLInternal } from '../import/qrl-class';
33
import { $, implicit$FirstArg, QRL } from '../import/qrl.public';
4-
import { qPropWriteQRL, qrlFactory } from '../props/props-on';
4+
import { qPropWriteQRL } from '../props/props-on';
55
import type { JSXNode } from '../render/jsx/types/jsx-node';
6-
import { newInvokeContext, useInvoke, useWaitOn } from '../use/use-core';
6+
import { newInvokeContext, StyleAppend, useInvoke, useWaitOn } from '../use/use-core';
77
import { useHostElement } from '../use/use-host-element.public';
88
import { ComponentScopedStyles, OnRenderProp } from '../util/markers';
99
import { styleKey } from './qrl-styles';
@@ -14,6 +14,8 @@ import type { FunctionComponent } from '../index';
1414
import { jsx } from '../render/jsx/jsx-runtime';
1515

1616
import { getDocument } from '../util/dom';
17+
import { promiseAll } from '../util/promises';
18+
import type { RenderFactoryOutput } from './component-ctx';
1719

1820
// <docs markdown="https://hackmd.io/c_nNpiLZSYugTU0c5JATJA#onUnmount">
1921
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
@@ -323,14 +325,17 @@ export function componentQrl<PROPS extends {}>(
323325

324326
// Return a QComponent Factory function.
325327
return function QComponent(props, key): JSXNode<PROPS> {
326-
const onRenderFactory: qrlFactory = async (hostElement: Element): Promise<QRLInternal> => {
327-
// Turn function into QRL
328+
const onRenderFactory = async (hostElement: Element): Promise<RenderFactoryOutput> => {
328329
const onMountQrl = toQrlOrError(onMount);
329330
const onMountFn = await resolveQrl(hostElement, onMountQrl);
330331
const ctx = getContext(hostElement);
331332
const props = getProps(ctx) as any;
332333
const invokeCtx = newInvokeContext(getDocument(hostElement), hostElement, hostElement);
333-
return useInvoke(invokeCtx, onMountFn, props) as QRLInternal;
334+
const renderQRL = (await useInvoke(invokeCtx, onMountFn, props)) as QRLInternal;
335+
return {
336+
renderQRL,
337+
waitOn: await promiseAll(invokeCtx.waitOn || []),
338+
};
334339
};
335340
onRenderFactory.__brand__ = 'QRLFactory';
336341

@@ -433,14 +438,12 @@ function _useStyles(styles: QRL<string>, scoped: boolean) {
433438

434439
useWaitOn(
435440
styleQrl.resolve(hostElement).then((styleText) => {
436-
const document = getDocument(hostElement);
437-
const head = document.querySelector('head');
438-
if (head && !head.querySelector(`style[q\\:style="${styleId}"]`)) {
439-
const style = document.createElement('style');
440-
style.setAttribute('q:style', styleId);
441-
style.textContent = scoped ? styleText.replace(//g, styleId) : styleText;
442-
head.appendChild(style);
443-
}
441+
const task: StyleAppend = {
442+
type: 'style',
443+
scope: styleId,
444+
content: scoped ? styleText.replace(//g, styleId) : styleText,
445+
};
446+
return task;
444447
})
445448
);
446449
}

src/core/component/mock.unit.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
* Sample file demonstrating what the CSS may look like
33
*
4-
* - 📦: Is prefixed to host elements and is the replacement for the `:host` selector.
5-
* - 🏷️: Is prefixed to all other elements.
4+
* - 💎: Is prefixed to host elements and is the replacement for the `:host` selector.
5+
* - ️: Is prefixed to all other elements.
66
*/
77

8-
📦ABC123 {
8+
💎ABC123 {
99
}
1010

11-
🏷️ABC123 {
11+
️ABC123 {
1212
}

src/core/object/store.public.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { snapshotState } from './store';
99
* @public
1010
*/
1111
export function snapshot(elmOrDoc: Element | Document) {
12-
const doc = isDocument(elmOrDoc) ? elmOrDoc : getDocument(elmOrDoc);
12+
const doc = getDocument(elmOrDoc);
1313
const data = snapshotState(elmOrDoc);
1414
const parentJSON = isDocument(elmOrDoc) ? elmOrDoc.body : elmOrDoc;
1515
const script = doc.createElement('script');

src/core/object/store.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { getContext } from '../props/props';
66
import { getDocument } from '../util/dom';
77
import { isDocument, isElement } from '../util/element';
88
import { logError, logWarn } from '../util/log';
9-
import { ELEMENT_ID, ELEMENT_ID_PREFIX, QHostAttr, QObjAttr } from '../util/markers';
9+
import {
10+
ELEMENT_ID,
11+
ELEMENT_ID_PREFIX,
12+
QContainerAttr,
13+
QHostAttr,
14+
QObjAttr,
15+
} from '../util/markers';
1016
import { qDev } from '../util/qdev';
1117
import {
1218
getProxyMap,
@@ -34,7 +40,7 @@ export function resume(elmOrDoc: Element | Document) {
3440
// logWarn('Skipping hydration because parent element is not q:container');
3541
return;
3642
}
37-
const doc = isDocument(elmOrDoc) ? elmOrDoc : getDocument(elmOrDoc);
43+
const doc = getDocument(elmOrDoc);
3844
const isDoc = isDocument(elmOrDoc) || elmOrDoc === doc.documentElement;
3945
const parentJSON = isDoc ? doc.body : parentElm;
4046
const script = getQwikJSON(parentJSON);
@@ -90,7 +96,7 @@ export function resume(elmOrDoc: Element | Document) {
9096
}
9197

9298
export function snapshotState(elmOrDoc: Element | Document) {
93-
const doc = isDocument(elmOrDoc) ? elmOrDoc : getDocument(elmOrDoc);
99+
const doc = getDocument(elmOrDoc);
94100
const parentElm = isDocument(elmOrDoc) ? elmOrDoc.documentElement : elmOrDoc;
95101
const proxyMap = getProxyMap(doc);
96102
const objSet = new Set<any>();
@@ -427,7 +433,7 @@ export function isProxy(obj: any): boolean {
427433
}
428434

429435
export function isContainer(el: Element) {
430-
return el.hasAttribute('q:container');
436+
return el.hasAttribute(QContainerAttr);
431437
}
432438

433439
function hasQObj(el: Element) {

src/core/platform/platform.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { getContainer } from '../use/use-core';
12
import { getDocument } from '../util/dom';
2-
import { isDocument } from '../util/element';
33
import type { CorePlatform } from './types';
44

55
export const createPlatform = (doc: Document): CorePlatform => {
@@ -52,27 +52,10 @@ export const createPlatform = (doc: Document): CorePlatform => {
5252
* @param url - relative URL
5353
* @returns fully qualified URL.
5454
*/
55-
export function toUrl(doc: Document, element: Element | null, url?: string | URL): URL {
56-
let _url: string | URL;
57-
let _base: string | URL | undefined = undefined;
58-
59-
if (url === undefined) {
60-
// recursive call
61-
if (element) {
62-
_url = element.getAttribute('q:base')!;
63-
_base = toUrl(
64-
doc,
65-
element.parentNode && (element.parentNode as HTMLElement).closest('[q\\:base]')
66-
);
67-
} else {
68-
_url = doc.baseURI;
69-
}
70-
} else if (url) {
71-
(_url = url), (_base = toUrl(doc, element!.closest('[q\\:base]')));
72-
} else {
73-
throw new Error('INTERNAL ERROR');
74-
}
75-
return new URL(String(_url), _base);
55+
export function toUrl(doc: Document, element: Element, url: string | URL): URL {
56+
const containerEl = getContainer(element);
57+
const base = new URL(containerEl?.getAttribute('q:base') ?? doc.baseURI, doc.baseURI);
58+
return new URL(url, base);
7659
}
7760

7861
/**
@@ -85,7 +68,7 @@ export const setPlatform = (doc: Document, plt: CorePlatform) =>
8568
* @public
8669
*/
8770
export const getPlatform = (docOrNode: Document | Node) => {
88-
const doc = (isDocument(docOrNode) ? docOrNode : getDocument(docOrNode)!) as PlatformDocument;
71+
const doc = getDocument(docOrNode) as PlatformDocument;
8972
return doc[DocumentPlatform] || (doc[DocumentPlatform] = createPlatform(doc));
9073
};
9174

src/core/props/props-on.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,6 @@ export function isOn$Prop(prop: string): boolean {
2525
return ON$_PROP_REGEX.test(prop);
2626
}
2727

28-
/**
29-
* In the case of a component, it is necessary to have `on:q-render` value.
30-
* However the `component` can run when parent component is rendering only to
31-
* realize that `on:q-render` already exists. This interface exists to solve that
32-
* problem.
33-
*
34-
* A parent component's `component` returns a `qrlFactory` for `on:q-render`. The
35-
* `getProps` than looks to see if it already has a resolved value, and if so the
36-
* `qrlFactory` is ignored, otherwise the `qrlFactory` is used to recover the `QRLInternal`.
37-
*/
38-
export interface qrlFactory {
39-
__brand__: `QRLFactory`;
40-
(element: Element): Promise<QRLInternal<any>>;
41-
}
42-
4328
export function qPropReadQRL(
4429
ctx: QContext,
4530
prop: string

src/core/props/props.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { getProxyMap, readWriteProxy } from '../object/q-object';
44
import { resume } from '../object/store';
55
import type { RenderContext } from '../render/cursor';
66
import { getDocument } from '../util/dom';
7-
import { isDocument } from '../util/element';
8-
import { logWarn } from '../util/log';
97
import { newQObjectMap, QObjectMap } from './props-obj-map';
108
import { qPropWriteQRL, qPropReadQRL } from './props-on';
119
import type { QRLInternal } from '../import/qrl-class';
@@ -15,17 +13,11 @@ Error.stackTraceLimit = 9999;
1513
const Q_IS_RESUMED = '__isResumed__';
1614
const Q_CTX = '__ctx__';
1715

18-
export function resumeIfNeeded(elm: Element | Document): void {
19-
const doc = isDocument(elm) ? elm : getDocument(elm);
20-
const root = isDocument(elm) ? elm : elm.closest('[q\\:container]') ?? doc;
21-
if (!root) {
22-
logWarn('cant find qwik app root');
23-
return;
24-
}
25-
const isHydrated = (root as any)[Q_IS_RESUMED];
16+
export function resumeIfNeeded(containerEl: Element): void {
17+
const isHydrated = (containerEl as any)[Q_IS_RESUMED];
2618
if (!isHydrated) {
27-
(root as any)[Q_IS_RESUMED] = true;
28-
resume(root);
19+
(containerEl as any)[Q_IS_RESUMED] = true;
20+
resume(containerEl);
2921
}
3022
}
3123

0 commit comments

Comments
 (0)