-
Notifications
You must be signed in to change notification settings - Fork 398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: bake context into lwc framework #4995
base: master
Are you sure you want to change the base?
Changes from 9 commits
d5fa241
9da9f19
37a5598
f45935c
18a5c2f
a51be92
081a413
3d0a90e
4f95a2b
1b6fc7b
89a322c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
import { ContextEventName, getContextKeys } from '@lwc/shared'; | ||
import type { Signal } from '@lwc/signals'; | ||
|
||
export type ContextProvidedCallback = (contextSignal: Signal<unknown>) => void; | ||
export class ContextRequestEvent extends CustomEvent<{ | ||
key: symbol; | ||
contextVariety: unknown; | ||
callback: ContextProvidedCallback; | ||
}> { | ||
constructor(detail: { contextVariety: unknown; callback: ContextProvidedCallback }) { | ||
super(ContextEventName, { | ||
bubbles: true, | ||
composed: true, | ||
detail: { ...detail, key: getContextKeys().contextEventKey }, | ||
}); | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,9 @@ import { | |
assert, | ||
create, | ||
defineProperty, | ||
getPrototypeOf, | ||
getOwnPropertyNames, | ||
keys, | ||
isArray, | ||
isFalse, | ||
isFunction, | ||
|
@@ -20,6 +22,8 @@ import { | |
isTrue, | ||
isUndefined, | ||
flattenStylesheets, | ||
getContextKeys, | ||
ContextEventName, | ||
} from '@lwc/shared'; | ||
|
||
import { addErrorComponentStack } from '../shared/error'; | ||
|
@@ -49,6 +53,8 @@ import { flushMutationLogsForVM, getAndFlushMutationLogs } from './mutation-logg | |
import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; | ||
import { VNodeType, isVFragment } from './vnodes'; | ||
import { isReportingEnabled, report, ReportingEventId } from './reporting'; | ||
import { type ContextProvidedCallback, ContextRequestEvent } from './context'; | ||
|
||
import type { VNodes, VCustomElement, VNode, VBaseElement, VStaticPartElement } from './vnodes'; | ||
import type { ReactiveObserver } from './mutation-tracker'; | ||
import type { | ||
|
@@ -60,6 +66,7 @@ import type { ComponentDef } from './def'; | |
import type { Template } from './template'; | ||
import type { HostNode, HostElement, RendererAPI } from './renderer'; | ||
import type { Stylesheet, Stylesheets, APIVersion } from '@lwc/shared'; | ||
import type { Signal } from '@lwc/signals'; | ||
|
||
type ShadowRootMode = 'open' | 'closed'; | ||
|
||
|
@@ -699,6 +706,9 @@ export function runConnectedCallback(vm: VM) { | |
if (hasWireAdapters(vm)) { | ||
connectWireAdapters(vm); | ||
} | ||
// Setup context before connected callback is executed | ||
setupContext(vm); | ||
|
||
const { connectedCallback } = vm.def; | ||
if (!isUndefined(connectedCallback)) { | ||
logOperationStart(OperationId.ConnectedCallback, vm); | ||
|
@@ -740,10 +750,109 @@ export function runConnectedCallback(vm: VM) { | |
} | ||
} | ||
|
||
function setupContext(vm: VM) { | ||
const contextKeys = getContextKeys(); | ||
|
||
if (!contextKeys) { | ||
rax-it marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
const { connectContext, contextEventKey } = contextKeys; | ||
const { component } = vm; | ||
const enumerableKeys = keys(getPrototypeOf(component)); | ||
const contextfulFieldsOrProps = enumerableKeys.filter( | ||
rax-it marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(propName) => (component as any)[propName]?.[connectContext] | ||
); | ||
|
||
if (contextfulFieldsOrProps.length === 0) { | ||
return; | ||
} | ||
|
||
let isProvidingContext = false; | ||
const providedContextVarieties = new Map<unknown, Signal<unknown>>(); | ||
const contextRuntimeAdapter = { | ||
isServerSide: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
component, | ||
provideContext<T extends object>( | ||
contextVariety: T, | ||
providedContextSignal: Signal<unknown> | ||
): void { | ||
if (!isProvidingContext) { | ||
isProvidingContext = true; | ||
|
||
component.addEventListener(ContextEventName, (event: any) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We typically use the |
||
if ( | ||
event.detail.key === contextEventKey && | ||
providedContextVarieties.has(event.detail.contextVariety) | ||
) { | ||
event.stopImmediatePropagation(); | ||
const providedContextSignal = providedContextVarieties.get( | ||
event.detail.contextVariety | ||
); | ||
event.detail.callback(providedContextSignal); | ||
} | ||
}); | ||
} | ||
|
||
let multipleContextWarningShown = false; | ||
|
||
if (providedContextVarieties.has(contextVariety)) { | ||
if (!multipleContextWarningShown) { | ||
multipleContextWarningShown = true; | ||
logError( | ||
'Multiple contexts of the same variety were provided. Only the first context will be used.' | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is not covered in the Karma tests. You can download the coverage report and see that actually quite a bit is uncovered. |
||
return; | ||
} | ||
|
||
providedContextVarieties.set(contextVariety, providedContextSignal); | ||
}, | ||
consumeContext<T extends object>( | ||
contextVariety: T, | ||
contextProvidedCallback: ContextProvidedCallback | ||
): void { | ||
const event = new ContextRequestEvent({ | ||
contextVariety, | ||
callback: contextProvidedCallback, | ||
}); | ||
|
||
component.dispatchEvent(event); | ||
}, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may perform better in JS engines if it's an explicit |
||
|
||
for (const contextfulFieldsOrProp of contextfulFieldsOrProps) { | ||
(component as any)[contextfulFieldsOrProp][connectContext](contextRuntimeAdapter); | ||
} | ||
} | ||
|
||
function hasWireAdapters(vm: VM): boolean { | ||
return getOwnPropertyNames(vm.def.wire).length > 0; | ||
} | ||
|
||
function cleanupContext(vm: VM) { | ||
const contextKeys = getContextKeys(); | ||
|
||
if (!contextKeys) { | ||
return; | ||
} | ||
|
||
const { disconnectContext } = contextKeys; | ||
const { component } = vm; | ||
const enumerableKeys = keys(getPrototypeOf(component)); | ||
const contextfulFieldsOrProps = enumerableKeys.filter( | ||
(propName) => (component as any)[propName]?.[disconnectContext] | ||
); | ||
|
||
if (contextfulFieldsOrProps.length === 0) { | ||
return; | ||
} | ||
|
||
for (const contextfulField of contextfulFieldsOrProps) { | ||
(component as any)[contextfulField][disconnectContext](component); | ||
} | ||
} | ||
|
||
function runDisconnectedCallback(vm: VM) { | ||
if (process.env.NODE_ENV !== 'production') { | ||
assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`); | ||
|
@@ -767,6 +876,7 @@ function runDisconnectedCallback(vm: VM) { | |
|
||
logOperationEnd(OperationId.DisconnectedCallback, vm); | ||
} | ||
cleanupContext(vm); | ||
} | ||
|
||
function runChildNodesDisconnectedCallback(vm: VM) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
<p>Test Child: {name}</p> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { LightningElement } from 'lwc'; | ||
import { consumeStateFactory } from 'x/state'; | ||
|
||
export default class TestChildSymbol extends LightningElement { | ||
randomChild = consumeStateFactory(); | ||
|
||
get name() { | ||
return this.randomChild.value.context?.value?.name ?? 'not available'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<template> | ||
<x-context-child> | ||
</x-context-child> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { LightningElement } from 'lwc'; | ||
import { nameStateFactory } from 'x/state'; | ||
|
||
export default class ContextParent extends LightningElement { | ||
random = nameStateFactory(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you looked at the Web Components Community Group proposal for a "context" event? This is an attempt to standardize the concept of passing context between ancestor/descendant components across web component frameworks.
The advantage of using this protocol is that, hypothetically, a non-LWC framework like Lit could use the same protocol and be interoperable with LWC in the same DOM tree.