From 6495c97f0d0a6e90b5329ddf9eb17687564ae561 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Mon, 11 Nov 2024 22:48:16 -0800 Subject: [PATCH 1/3] feat: implement standalone fromContext --- .../contextChildDetached.html | 4 ++ .../contextChildDetached.js | 12 ++++ .../contextParentDetached.html | 3 + .../contextParentDetached.js | 8 +++ .../src/modules/x/contextRoot/context.spec.js | 6 ++ .../modules/x/contextRoot/contextRoot.html | 1 + packages/@lwc/state/src/index.ts | 1 + packages/@lwc/state/src/standalone-context.ts | 55 +++++++++++++++++++ 8 files changed, 90 insertions(+) create mode 100644 example/src/modules/x/contextChildDetached/contextChildDetached.html create mode 100644 example/src/modules/x/contextChildDetached/contextChildDetached.js create mode 100644 example/src/modules/x/contextParentDetached/contextParentDetached.html create mode 100644 example/src/modules/x/contextParentDetached/contextParentDetached.js create mode 100644 packages/@lwc/state/src/standalone-context.ts diff --git a/example/src/modules/x/contextChildDetached/contextChildDetached.html b/example/src/modules/x/contextChildDetached/contextChildDetached.html new file mode 100644 index 0000000..4e49eda --- /dev/null +++ b/example/src/modules/x/contextChildDetached/contextChildDetached.html @@ -0,0 +1,4 @@ + diff --git a/example/src/modules/x/contextChildDetached/contextChildDetached.js b/example/src/modules/x/contextChildDetached/contextChildDetached.js new file mode 100644 index 0000000..2d98355 --- /dev/null +++ b/example/src/modules/x/contextChildDetached/contextChildDetached.js @@ -0,0 +1,12 @@ +import { ContextfulLightningElement } from '@lwc/state/context'; +import { fromContext } from '@lwc/state'; +import childStateFactory from 'x/childState'; +import parentStateFactory from 'x/parentState'; + +export default class ContextChild extends ContextfulLightningElement { + parentState = fromContext(parentStateFactory); + + get nameProvidedByParent() { + return this.parentState.value?.name ?? 'not available'; + } +} diff --git a/example/src/modules/x/contextParentDetached/contextParentDetached.html b/example/src/modules/x/contextParentDetached/contextParentDetached.html new file mode 100644 index 0000000..a4e3490 --- /dev/null +++ b/example/src/modules/x/contextParentDetached/contextParentDetached.html @@ -0,0 +1,3 @@ + diff --git a/example/src/modules/x/contextParentDetached/contextParentDetached.js b/example/src/modules/x/contextParentDetached/contextParentDetached.js new file mode 100644 index 0000000..938dba1 --- /dev/null +++ b/example/src/modules/x/contextParentDetached/contextParentDetached.js @@ -0,0 +1,8 @@ +import { api } from 'lwc'; +import { ContextfulLightningElement } from '@lwc/state/context'; +import parentStateFactory from 'x/parentState'; + +export default class ContextParent extends ContextfulLightningElement { + @api + parentState = parentStateFactory('parentFoo'); +} diff --git a/example/src/modules/x/contextRoot/context.spec.js b/example/src/modules/x/contextRoot/context.spec.js index 78b8419..34fc67d 100644 --- a/example/src/modules/x/contextRoot/context.spec.js +++ b/example/src/modules/x/contextRoot/context.spec.js @@ -96,4 +96,10 @@ describe('context', () => { expect(contextParent.parentState.subscribers.size).toBe(1); }); + + it('children can access context directly with detached fromContext', async () => { + const el = await clientSideRender(parentEl, componentPath, {}); + const childWithDetachedFromContext = querySelectorDeep('.child-content-detached', el); + expect(childWithDetachedFromContext.innerText).to.include('parentFoo'); + }); }); diff --git a/example/src/modules/x/contextRoot/contextRoot.html b/example/src/modules/x/contextRoot/contextRoot.html index e590340..6767925 100644 --- a/example/src/modules/x/contextRoot/contextRoot.html +++ b/example/src/modules/x/contextRoot/contextRoot.html @@ -1,4 +1,5 @@ diff --git a/packages/@lwc/state/src/index.ts b/packages/@lwc/state/src/index.ts index 7d3cc39..8c6a46a 100644 --- a/packages/@lwc/state/src/index.ts +++ b/packages/@lwc/state/src/index.ts @@ -12,6 +12,7 @@ import type { UnwrapSignal, } from './types.ts'; export { setTrustedSignalSet } from '@lwc/signals'; +export { fromContext } from './standalone-context.js'; const atomSetter = Symbol('atomSetter'); const contextID = Symbol('contextID'); diff --git a/packages/@lwc/state/src/standalone-context.ts b/packages/@lwc/state/src/standalone-context.ts new file mode 100644 index 0000000..eae80aa --- /dev/null +++ b/packages/@lwc/state/src/standalone-context.ts @@ -0,0 +1,55 @@ +import { connectContext } from './shared.js'; +import { SignalBaseClass, type Signal } from '@lwc/signals'; +import type { ExposedUpdater } from './types.js'; +import type { ContextRuntimeAdapter } from './runtime-interface.js'; +import type { ContextManager } from './index.js'; + +type ValidStateShape = Record | ExposedUpdater>; +type ValidStateDef = () => Signal; + +class ConsumedContextSignal + extends SignalBaseClass + implements ContextManager +{ + private desiredStateDef: ValidStateDef; + private _value: StateShape | null = null; + // Currently unused. Should be called once `disconnectContext` is implemented. + private unsubscribe: () => void = () => {}; + + constructor(stateDef: ValidStateDef) { + super(); + this.desiredStateDef = stateDef; + } + + get value(): StateShape | null { + return this._value; + } + + [connectContext](runtimeAdapter: ContextRuntimeAdapter) { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.', + ); + } + + runtimeAdapter.consumeContext( + this.desiredStateDef, + (providedContextSignal: Signal) => { + this._value = providedContextSignal.value; + this.notify(); + this.unsubscribe = providedContextSignal.subscribe(() => { + this._value = providedContextSignal.value; + }); + }, + ); + } +} + +export const fromContext = < + StateShape extends ValidStateShape, + StateDef extends ValidStateDef, +>( + stateDef: StateDef, +) => { + return new ConsumedContextSignal(stateDef); +}; From 751af82ad4f09aee2e1e7d2f424bc3469b9b3cb7 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Thu, 14 Nov 2024 15:17:28 -0800 Subject: [PATCH 2/3] fix: notify when providedContextSignal has an update Co-authored-by: rax-it --- packages/@lwc/state/src/standalone-context.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@lwc/state/src/standalone-context.ts b/packages/@lwc/state/src/standalone-context.ts index eae80aa..fa2ea46 100644 --- a/packages/@lwc/state/src/standalone-context.ts +++ b/packages/@lwc/state/src/standalone-context.ts @@ -39,6 +39,7 @@ class ConsumedContextSignal this.notify(); this.unsubscribe = providedContextSignal.subscribe(() => { this._value = providedContextSignal.value; + this.notify() }); }, ); From 646015fa4c6b99e3abea18cea018cf79b3a7363d Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Thu, 14 Nov 2024 15:25:59 -0800 Subject: [PATCH 3/3] chore: lint --- packages/@lwc/state/src/standalone-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/state/src/standalone-context.ts b/packages/@lwc/state/src/standalone-context.ts index fa2ea46..c09bd10 100644 --- a/packages/@lwc/state/src/standalone-context.ts +++ b/packages/@lwc/state/src/standalone-context.ts @@ -39,7 +39,7 @@ class ConsumedContextSignal this.notify(); this.unsubscribe = providedContextSignal.subscribe(() => { this._value = providedContextSignal.value; - this.notify() + this.notify(); }); }, );