diff --git a/src/core/api.md b/src/core/api.md index d2500b80527..ba5f7dba30d 100644 --- a/src/core/api.md +++ b/src/core/api.md @@ -329,6 +329,9 @@ export function useEvent(expectEventType?: QEvent | string): E // @public (undocumented) export function useHostElement(): Element; +// @public (undocumented) +export function useTransient(obj: OBJ, factory: (this: OBJ, ...args: ARGS) => RET, ...args: ARGS): RET; + // @public (undocumented) export function useURL(): URL; diff --git a/src/core/component/q-component-ctx.ts b/src/core/component/q-component-ctx.ts index 13f2412a7a4..ab38ac4a2ec 100644 --- a/src/core/component/q-component-ctx.ts +++ b/src/core/component/q-component-ctx.ts @@ -5,7 +5,7 @@ import { ComponentRenderQueue, visitJsxNode } from '../render/q-render'; import { AttributeMarker } from '../util/markers'; import { flattenPromiseTree } from '../util/promises'; import { QrlStyles, styleContent, styleHost } from './qrl-styles'; -import { _qObject } from '../object/q-object'; +import { _stateQObject } from '../object/q-object'; import { qProps } from '../props/q-props.public'; // TODO(misko): Can we get rid of this whole file, and instead teach qProps to know how to render @@ -41,7 +41,7 @@ export class QComponentCtx { if (hook) { const values: OnHookReturn[] = await hook('qMount'); values.forEach((v) => { - props['state:' + v.state] = _qObject(v.value, v.state); + props['state:' + v.state] = _stateQObject(v.value, v.state); }); } } catch (e) { diff --git a/src/core/component/qrl-hook.public.ts b/src/core/component/qrl-hook.public.ts index 466dfa7b31d..2a40108035b 100644 --- a/src/core/component/qrl-hook.public.ts +++ b/src/core/component/qrl-hook.public.ts @@ -28,9 +28,16 @@ export function qHook( - hook: (props: PropsOf, state: StateOf, args: ARGS) => ValueOrPromise -): QHook, StateOf, ARGS, RET> { +export function qHook(hook: any, symbol?: string): any { + if (typeof hook === 'string') return hook; + if (typeof symbol === 'string') { + const match = String(hook).match(EXTRACT_IMPORT_PATH); + if (match && match[2]) { + return (match[2] + '#' + symbol) as any; + } else { + throw new Error('dynamic import not found: ' + String(hook)); + } + } const qrlFn = async (element: HTMLElement, event: Event, url: URL) => { const isQwikInternalHook = typeof event == 'string'; // isQwikInternalHook && console.log('HOOK', event, element, url); @@ -50,9 +57,9 @@ export function qHook; } + +// https://regexr.com/68v72 +const EXTRACT_IMPORT_PATH = /import\(\s*(['"])([^\1]+)\1\s*\)/; diff --git a/src/core/index.ts b/src/core/index.ts index 95dba76ed15..001d45fbcd8 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -54,6 +54,7 @@ export { QwikDOMAttributes, QwikJSX } from './render/jsx/types/jsx-qwik'; export type { QwikIntrinsicElements } from './render/jsx/types/jsx-qwik-elements'; export { qRender } from './render/q-render.public'; export { useEvent, useHostElement, useURL } from './use/use-core.public'; +export { useTransient } from './use/use-transient.public'; ////////////////////////////////////////////////////////////////////////////////////////// // Developer Low-Level API ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/core/object/q-object.ts b/src/core/object/q-object.ts index ee4cd9c5b97..e545e50112b 100644 --- a/src/core/object/q-object.ts +++ b/src/core/object/q-object.ts @@ -4,16 +4,27 @@ import { safeQSubscribe } from '../use/use-core.public'; import type { QObject as IQObject } from './q-object.public'; export const Q_OBJECT_PREFIX_SEP = ':'; -export function _qObject(obj: T, prefix?: string, isId: boolean = false): T { +export function _qObject(obj: T): T { assertEqual(unwrapProxy(obj), obj, 'Unexpected proxy at this location'); - const id = isId - ? (prefix as string) - : (prefix == null ? '' : prefix + Q_OBJECT_PREFIX_SEP) + generateId(); - const proxy = readWriteProxy(obj as any as IQObject, id); + const proxy = readWriteProxy(obj as any as IQObject, generateId()); Object.assign(proxy, obj); return proxy; } +export function _stateQObject(obj: T, prefix: string): T { + const id = getQObjectId(obj); + if (id) { + (obj as any)[QObjectIdSymbol] = prefix + Q_OBJECT_PREFIX_SEP + id; + return obj; + } else { + return readWriteProxy(obj as any as IQObject, prefix + Q_OBJECT_PREFIX_SEP + generateId()); + } +} + +export function _restoreQObject(obj: T, id: string): T { + return readWriteProxy(obj as any as IQObject, id); +} + function QObject_notifyWrite(id: string, doc: Document | null) { if (doc) { doc.querySelectorAll(idToComponentSelector(id)).forEach(qNotifyRender); @@ -35,6 +46,17 @@ export function getQObjectId(obj: any): string | null { return (obj && typeof obj === 'object' && obj[QObjectIdSymbol]) || null; } +export function getTransient(obj: any, key: any): T | null { + assertDefined(getQObjectId(obj)); + return obj[QOjectTransientsSymbol].get(key); +} + +export function setTransient(obj: any, key: any, value: T): T { + assertDefined(getQObjectId(obj)); + obj[QOjectTransientsSymbol].set(key, value); + return value; +} + function idToComponentSelector(id: string): any { id = id.replace(/([^\w\d])/g, (_, v) => '\\' + v); return '[q\\:obj*=' + (isStateObj(id) ? '' : '\\!') + id + ']'; @@ -61,6 +83,7 @@ export function readWriteProxy(target: T, id: string): T { } const QOjectTargetSymbol = ':target:'; +const QOjectTransientsSymbol = ':transients:'; const QObjectIdSymbol = ':id:'; const QObjectDocumentSymbol = ':doc:'; @@ -90,6 +113,7 @@ export function wrap(value: T): T { class ReadWriteProxyHandler implements ProxyHandler { private id: string; private doc: Document | null = null; + private transients: WeakMap | null = null; constructor(id: string) { this.id = id; } @@ -97,6 +121,9 @@ class ReadWriteProxyHandler implements ProxyHandler { get(target: T, prop: string): any { if (prop === QOjectTargetSymbol) return target; if (prop === QObjectIdSymbol) return this.id; + if (prop === QOjectTransientsSymbol) { + return this.transients || (this.transients = new WeakMap()); + } const value = (target as any)[prop]; QObject_notifyRead(target); return wrap(value); @@ -105,6 +132,8 @@ class ReadWriteProxyHandler implements ProxyHandler { set(target: T, prop: string, newValue: any): boolean { if (prop === QObjectDocumentSymbol) { this.doc = newValue; + } else if (prop == QObjectIdSymbol) { + this.id = newValue; } else { const unwrappedNewValue = unwrapProxy(newValue); const oldValue = (target as any)[prop]; diff --git a/src/core/object/q-store.ts b/src/core/object/q-store.ts index b775e0d22fb..0ae7b407d02 100644 --- a/src/core/object/q-store.ts +++ b/src/core/object/q-store.ts @@ -2,7 +2,7 @@ import { assertDefined } from '../assert/assert'; import { JSON_OBJ_PREFIX } from '../json/q-json'; import { qDev } from '../util/qdev'; import { clearQProps, clearQPropsMap, QPropsContext } from '../props/q-props'; -import { getQObjectId, _qObject } from './q-object'; +import { getQObjectId, _restoreQObject } from './q-object'; import { qProps } from '../props/q-props.public'; export interface Store { @@ -55,7 +55,7 @@ function reviveQObjects(map: Record | null) { for (const key in map) { if (Object.prototype.hasOwnProperty.call(map, key)) { const value = map[key]; - map[key] = _qObject(value, key, true); + map[key] = _restoreQObject(value, key); } } } diff --git a/src/core/object/q-store.unit.tsx b/src/core/object/q-store.unit.tsx index 77aae655146..dc6228f8773 100644 --- a/src/core/object/q-store.unit.tsx +++ b/src/core/object/q-store.unit.tsx @@ -1,7 +1,7 @@ import { createDocument } from '../../testing/document'; import { qProps, QProps } from '../props/q-props.public'; import { qObject, qDehydrate } from '@builder.io/qwik'; -import { _qObject } from './q-object'; +import { _stateQObject } from './q-object'; describe('q-element', () => { let document: Document; @@ -16,7 +16,7 @@ describe('q-element', () => { it('should serialize content', () => { const shared = qObject({ mark: 'CHILD' }); - qDiv['state:'] = _qObject({ mark: 'WORKS', child: shared, child2: shared }, ''); + qDiv['state:'] = _stateQObject({ mark: 'WORKS', child: shared, child2: shared }, ''); qDehydrate(document); @@ -25,7 +25,7 @@ describe('q-element', () => { }); it('should serialize same objects multiple times', () => { - const foo = _qObject({ mark: 'CHILD' }, 'foo'); + const foo = _stateQObject({ mark: 'CHILD' }, 'foo'); qDiv['state:foo'] = foo; qDiv.foo = foo; @@ -36,8 +36,8 @@ describe('q-element', () => { expect(qDiv.foo).toEqual(foo); }); it('should serialize cyclic graphs', () => { - const foo = _qObject({ mark: 'foo', bar: {} }, 'foo'); - const bar = _qObject({ mark: 'bar', foo: foo }, 'bar'); + const foo = _stateQObject({ mark: 'foo', bar: {} }, 'foo'); + const bar = _stateQObject({ mark: 'bar', foo: foo }, 'bar'); foo.bar = bar; qDiv.foo = foo; diff --git a/src/core/props/q-props.unit.tsx b/src/core/props/q-props.unit.tsx index ec060ac6e52..e6afa325cd7 100644 --- a/src/core/props/q-props.unit.tsx +++ b/src/core/props/q-props.unit.tsx @@ -4,7 +4,7 @@ import { ParsedQRL } from '../import/qrl'; import { diff, test_clearqPropsCache as test_clearQPropsCache } from './q-props'; import type { QComponent } from '../component/q-component.public'; import { qObject } from '../object/q-object.public'; -import { getQObjectId, _qObject } from '../object/q-object'; +import { getQObjectId, _stateQObject } from '../object/q-object'; import { qDehydrate } from '../object/q-store.public'; import { qProps, QProps } from './q-props.public'; @@ -120,8 +120,8 @@ describe('q-element', () => { describe('state', () => { it('should retrieve state by name', () => { - const state1 = _qObject({ mark: 1 }, ''); - const state2 = _qObject({ mark: 2 }, 'foo'); + const state1 = _stateQObject({ mark: 1 }, ''); + const state2 = _stateQObject({ mark: 2 }, 'foo'); qDiv['state:'] = state1; qDiv['state:foo'] = state2; @@ -167,8 +167,8 @@ describe('q-element', () => { it('should read qrl as single function', async () => { qDiv['on:qRender'] = 'markAsHost'; - qDiv['state:'] = _qObject({ mark: 'implicit' }, ''); - qDiv['state:explicit'] = _qObject({ mark: 'explicit' }, 'explicit'); + qDiv['state:'] = _stateQObject({ mark: 'implicit' }, ''); + qDiv['state:explicit'] = _stateQObject({ mark: 'explicit' }, 'explicit'); qDiv.isHost = 'YES'; const child = document.createElement('child'); diff --git a/src/core/use/use-transient.public.ts b/src/core/use/use-transient.public.ts new file mode 100644 index 00000000000..8e22865b872 --- /dev/null +++ b/src/core/use/use-transient.public.ts @@ -0,0 +1,13 @@ +import { getTransient, setTransient } from '../object/q-object'; + +/** + * @public + */ +export function useTransient( + obj: OBJ, + factory: (this: OBJ, ...args: ARGS) => RET, + ...args: ARGS +): RET { + const existing = getTransient(obj, factory); + return existing || setTransient(obj, factory, factory.apply(obj, args)); +}