@webjsdev/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +14 -9
- package/index.d.ts +22 -10
- package/index.js +17 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-reader.js +197 -23
- package/src/binding-prefixes.js +0 -12
- package/src/component/lifecycle.js +760 -209
- package/src/component/reactive.js +2 -10
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +151 -41
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +38 -1
- package/src/directives.js +158 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +4 -0
- package/src/html.js +35 -3
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +8 -3
- package/src/render-client/parts.js +745 -31
- package/src/render-client/reconciler.js +56 -11
- package/src/render-client/template-compiler.js +139 -44
- package/src/render-client.d.ts +5 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +34 -0
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +61 -8
- package/src/render-server/served-attrs.js +1 -2
- package/src/render-server/walk.js +135 -3
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +86 -5
- package/src/router-client/navigator.js +45 -9
- package/src/router-client/prefetch.js +3 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/swap.js +4 -1
- package/src/routes.d.ts +75 -18
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +1 -8
package/src/context.js
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Portions derived from Lit (`@lit/context`), Copyright Google LLC, BSD-3-Clause.
|
|
4
|
+
* https://github.com/lit/lit/blob/main/packages/context/LICENSE
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Context Protocol: cross-component data sharing without prop drilling.
|
|
3
9
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
10
|
+
* A port of Lit's `@lit/context`, which implements the Web Components
|
|
11
|
+
* Community Group Context Protocol, so any web component (not only a WebJs
|
|
12
|
+
* one) can take part, and a component written from Lit memory behaves the
|
|
13
|
+
* same under the `@webjsdev/core/context` import.
|
|
6
14
|
*
|
|
7
15
|
* ## When to use
|
|
8
16
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Prefer Context over module-level globals when the shared data is **scoped
|
|
14
|
-
* to a subtree**: e.g. a specific page, panel, or dialog: and different
|
|
15
|
-
* subtrees may hold different values for the same context key.
|
|
16
|
-
*
|
|
17
|
-
* ## When NOT to use
|
|
18
|
-
*
|
|
19
|
-
* - For page-level data loading, use async page functions or server actions
|
|
20
|
-
* instead. Context is a client-side primitive.
|
|
21
|
-
* - If only a parent and its direct child need to communicate, plain
|
|
22
|
-
* properties/attributes are simpler.
|
|
23
|
-
* - If every component in the app needs the same single value (e.g. a
|
|
24
|
-
* singleton API client), a module-level export may be simpler than
|
|
25
|
-
* context.
|
|
17
|
+
* Share data (theme, auth state, locale, config) with deeply nested
|
|
18
|
+
* components without passing it through every level. Prefer it over a
|
|
19
|
+
* module-level signal when the value is SCOPED to a subtree, so different
|
|
20
|
+
* subtrees can hold different values for the same context.
|
|
26
21
|
*
|
|
27
22
|
* ## Quick example
|
|
28
23
|
*
|
|
@@ -30,15 +25,17 @@
|
|
|
30
25
|
* import { WebComponent, html } from '@webjsdev/core';
|
|
31
26
|
* import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context';
|
|
32
27
|
*
|
|
33
|
-
*
|
|
28
|
+
* // The KEY is the identity: the same string (or the same Symbol) in two
|
|
29
|
+
* // modules is the same context.
|
|
30
|
+
* export const themeContext = createContext('theme');
|
|
34
31
|
*
|
|
35
32
|
* class MyApp extends WebComponent {
|
|
36
|
-
* _theme = new ContextProvider(this, { context:
|
|
33
|
+
* _theme = new ContextProvider(this, { context: themeContext, initialValue: 'light' });
|
|
37
34
|
* render() {
|
|
38
35
|
* return html`
|
|
39
|
-
* <button @click=${() => this._theme.setValue(
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* <button @click=${() => this._theme.setValue(this._theme.value === 'light' ? 'dark' : 'light')}>
|
|
37
|
+
* Toggle
|
|
38
|
+
* </button>
|
|
42
39
|
* <slot></slot>
|
|
43
40
|
* `;
|
|
44
41
|
* }
|
|
@@ -46,7 +43,7 @@
|
|
|
46
43
|
* MyApp.register('my-app');
|
|
47
44
|
*
|
|
48
45
|
* class ThemedCard extends WebComponent {
|
|
49
|
-
* _theme = new ContextConsumer(this, { context:
|
|
46
|
+
* _theme = new ContextConsumer(this, { context: themeContext, subscribe: true });
|
|
50
47
|
* render() {
|
|
51
48
|
* return html`<div class=${this._theme.value}>Card content</div>`;
|
|
52
49
|
* }
|
|
@@ -54,176 +51,237 @@
|
|
|
54
51
|
* ThemedCard.register('themed-card');
|
|
55
52
|
* ```
|
|
56
53
|
*
|
|
57
|
-
* `<themed-card>` can live at any depth under `<my-app>`: it finds the
|
|
58
|
-
* provider automatically via a bubbling `context-request` event that
|
|
59
|
-
* crosses shadow-DOM boundaries (`composed: true`).
|
|
60
|
-
*
|
|
61
54
|
* @module
|
|
62
55
|
*/
|
|
63
56
|
|
|
64
57
|
// ---------------------------------------------------------------------------
|
|
65
|
-
//
|
|
58
|
+
// createContext
|
|
66
59
|
// ---------------------------------------------------------------------------
|
|
67
60
|
|
|
68
61
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const CONTEXT_KEY = Symbol.for('webjs.context');
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Create a typed context key.
|
|
77
|
-
*
|
|
78
|
-
* The returned object is used as an identity token: two calls to
|
|
79
|
-
* `createContext('theme')` produce **different** contexts even though
|
|
80
|
-
* the debug name is the same. Store the key in a shared module and
|
|
81
|
-
* import it from both provider and consumer.
|
|
62
|
+
* Create a context key. It returns `key` itself, typed with the value type,
|
|
63
|
+
* exactly as Lit does: the key IS the identity, so `createContext('theme')`
|
|
64
|
+
* in two modules produces the same context. Use a `Symbol('theme')` exported
|
|
65
|
+
* from one module when the context must not collide with any other.
|
|
82
66
|
*
|
|
83
|
-
* @template
|
|
84
|
-
* @
|
|
85
|
-
*
|
|
86
|
-
* @returns {
|
|
67
|
+
* @template ValueType
|
|
68
|
+
* @template [K=unknown]
|
|
69
|
+
* @param {K} key
|
|
70
|
+
* @returns {K}
|
|
87
71
|
*/
|
|
88
|
-
export function createContext(
|
|
89
|
-
return
|
|
72
|
+
export function createContext(key) {
|
|
73
|
+
return key;
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
// ---------------------------------------------------------------------------
|
|
93
|
-
//
|
|
77
|
+
// Events
|
|
94
78
|
// ---------------------------------------------------------------------------
|
|
95
79
|
|
|
96
80
|
/**
|
|
97
|
-
* Fired by a consumer to
|
|
98
|
-
*
|
|
99
|
-
* - `bubbles: true` so it walks up the DOM tree.
|
|
100
|
-
* - `composed: true` so it crosses shadow-DOM boundaries.
|
|
101
|
-
*
|
|
102
|
-
* Providers listen for this event on their host element and respond by
|
|
103
|
-
* calling the event's `callback` with the current value.
|
|
104
|
-
*
|
|
105
|
-
* @template T
|
|
81
|
+
* Fired by a consumer to ask for a value. It bubbles and is composed, so it
|
|
82
|
+
* crosses shadow roots on its way to the nearest provider.
|
|
106
83
|
*/
|
|
107
84
|
export class ContextRequestEvent extends Event {
|
|
108
85
|
/**
|
|
109
|
-
* @param {
|
|
110
|
-
* @param {
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @param {boolean} subscribe
|
|
86
|
+
* @param {unknown} context The context key.
|
|
87
|
+
* @param {Element} contextTarget The element asking (the consumer's host).
|
|
88
|
+
* @param {(value: any, unsubscribe?: () => void) => void} callback
|
|
89
|
+
* Called by the provider with the value. A subscribing request also gets
|
|
90
|
+
* an `unsubscribe` function, and is called again on every change.
|
|
91
|
+
* @param {boolean} [subscribe=false] Whether to keep receiving changes.
|
|
115
92
|
*/
|
|
116
|
-
constructor(context, callback, subscribe
|
|
93
|
+
constructor(context, contextTarget, callback, subscribe) {
|
|
117
94
|
super('context-request', { bubbles: true, composed: true });
|
|
118
|
-
/** @
|
|
95
|
+
/** @readonly */
|
|
119
96
|
this.context = context;
|
|
120
|
-
/** @
|
|
97
|
+
/** @readonly */
|
|
98
|
+
this.contextTarget = contextTarget;
|
|
99
|
+
/** @readonly */
|
|
121
100
|
this.callback = callback;
|
|
122
|
-
/** @
|
|
123
|
-
this.subscribe = subscribe;
|
|
101
|
+
/** @readonly */
|
|
102
|
+
this.subscribe = subscribe ?? false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fired by a provider when it connects, so a provider ABOVE it can hand over
|
|
108
|
+
* the subscriptions that now belong to the nearer provider, and so a
|
|
109
|
+
* `ContextRoot` can replay requests that arrived before any provider existed.
|
|
110
|
+
*/
|
|
111
|
+
export class ContextProviderEvent extends Event {
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} context The context key.
|
|
114
|
+
* @param {Element} contextTarget The provider's host.
|
|
115
|
+
*/
|
|
116
|
+
constructor(context, contextTarget) {
|
|
117
|
+
super('context-provider', { bubbles: true, composed: true });
|
|
118
|
+
/** @readonly */
|
|
119
|
+
this.context = context;
|
|
120
|
+
/** @readonly */
|
|
121
|
+
this.contextTarget = contextTarget;
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
|
-
//
|
|
126
|
+
// ValueNotifier (the provider's value + subscriber list)
|
|
129
127
|
// ---------------------------------------------------------------------------
|
|
130
128
|
|
|
131
129
|
/**
|
|
130
|
+
* Holds a value and the callbacks subscribed to it. The base of
|
|
131
|
+
* {@link ContextProvider}, as in Lit.
|
|
132
132
|
* @template T
|
|
133
|
-
* @typedef {{ callback: (value: T, unsubscribe?: () => void) => void }} Subscription
|
|
134
133
|
*/
|
|
134
|
+
export class ValueNotifier {
|
|
135
|
+
/** @param {T} [defaultValue] */
|
|
136
|
+
constructor(defaultValue) {
|
|
137
|
+
/**
|
|
138
|
+
* Subscribed callbacks, each with its disposer and the consumer host it
|
|
139
|
+
* came from (used to re-dispatch when a nearer provider appears).
|
|
140
|
+
* @type {Map<Function, { disposer: () => void, consumerHost: Element }>}
|
|
141
|
+
*/
|
|
142
|
+
this.subscriptions = new Map();
|
|
143
|
+
/** @type {T} */
|
|
144
|
+
this._value = /** @type {T} */ (undefined);
|
|
145
|
+
if (defaultValue !== undefined) this.value = defaultValue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @returns {T} */
|
|
149
|
+
get value() {
|
|
150
|
+
return this._value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @param {T} v */
|
|
154
|
+
set value(v) {
|
|
155
|
+
this.setValue(v);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set the value and, when it changed (or `force` is true), call every
|
|
160
|
+
* subscriber with it.
|
|
161
|
+
* @param {T} v
|
|
162
|
+
* @param {boolean} [force=false]
|
|
163
|
+
*/
|
|
164
|
+
setValue(v, force = false) {
|
|
165
|
+
const update = force || !Object.is(v, this._value);
|
|
166
|
+
this._value = v;
|
|
167
|
+
if (update) this.updateObservers();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Call every subscriber with the current value. */
|
|
171
|
+
updateObservers = () => {
|
|
172
|
+
for (const [callback, { disposer }] of this.subscriptions) {
|
|
173
|
+
callback(this._value, disposer);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Answer a request: call `callback` with the value, and keep it for later
|
|
179
|
+
* changes when `subscribe` is true.
|
|
180
|
+
* @param {(value: T, unsubscribe?: () => void) => void} callback
|
|
181
|
+
* @param {Element} consumerHost
|
|
182
|
+
* @param {boolean} [subscribe]
|
|
183
|
+
*/
|
|
184
|
+
addCallback(callback, consumerHost, subscribe) {
|
|
185
|
+
if (!subscribe) {
|
|
186
|
+
callback(this.value);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!this.subscriptions.has(callback)) {
|
|
190
|
+
this.subscriptions.set(callback, {
|
|
191
|
+
disposer: () => {
|
|
192
|
+
this.subscriptions.delete(callback);
|
|
193
|
+
},
|
|
194
|
+
consumerHost,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const { disposer } = /** @type {{ disposer: () => void }} */ (this.subscriptions.get(callback));
|
|
198
|
+
callback(this.value, disposer);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Drop every subscriber. */
|
|
202
|
+
clearCallbacks() {
|
|
203
|
+
this.subscriptions.clear();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// ContextProvider
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
135
210
|
|
|
136
211
|
/**
|
|
137
|
-
* A ReactiveController that provides a
|
|
138
|
-
*
|
|
212
|
+
* A ReactiveController that provides a value to descendants. It listens on
|
|
213
|
+
* its host from construction, answers matching `context-request` events from
|
|
214
|
+
* descendants (never its own host's), and on connect fires
|
|
215
|
+
* `context-provider` so an outer provider of the same context hands over the
|
|
216
|
+
* subscribers that are now nearer to this one.
|
|
139
217
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* for subscribing consumers, retains the callback for future updates).
|
|
143
|
-
*
|
|
144
|
-
* ## AI guidance
|
|
145
|
-
*
|
|
146
|
-
* - Create one `ContextProvider` per context key per component.
|
|
147
|
-
* - Call `setValue()` to push updates to all subscribing consumers.
|
|
148
|
-
* - The provider **must** be a DOM ancestor of its consumers (shadow-DOM
|
|
149
|
-
* depth does not matter thanks to `composed: true`).
|
|
218
|
+
* `new ContextProvider(host, { context, initialValue })` or
|
|
219
|
+
* `new ContextProvider(host, context, initialValue)`.
|
|
150
220
|
*
|
|
151
221
|
* @template T
|
|
222
|
+
* @extends {ValueNotifier<T>}
|
|
152
223
|
*/
|
|
153
|
-
export class ContextProvider {
|
|
224
|
+
export class ContextProvider extends ValueNotifier {
|
|
154
225
|
/**
|
|
155
|
-
* @param {
|
|
156
|
-
*
|
|
157
|
-
* @param {
|
|
226
|
+
* @param {any} host The host element (a WebComponent or any element).
|
|
227
|
+
* @param {any} contextOrOptions `{ context, initialValue }`, or the context.
|
|
228
|
+
* @param {T} [initialValue] The value, positional form only.
|
|
158
229
|
*/
|
|
159
|
-
constructor(host,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
/** @
|
|
163
|
-
this.
|
|
164
|
-
/** @
|
|
165
|
-
this.
|
|
166
|
-
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
/** @type {(e: Event) => void} */
|
|
170
|
-
this._onRequest = (e) => {
|
|
171
|
-
const evt = /** @type {ContextRequestEvent<T>} */ (e);
|
|
172
|
-
if (evt.context !== this._context) return;
|
|
173
|
-
|
|
174
|
-
// Prevent further providers higher in the tree from also responding.
|
|
175
|
-
e.stopPropagation();
|
|
176
|
-
|
|
177
|
-
if (evt.subscribe) {
|
|
178
|
-
/** @type {Subscription<T>} */
|
|
179
|
-
const sub = { callback: evt.callback };
|
|
180
|
-
const unsubscribe = () => { this._subscriptions.delete(sub); };
|
|
181
|
-
this._subscriptions.add(sub);
|
|
182
|
-
evt.callback(this._value, unsubscribe);
|
|
183
|
-
} else {
|
|
184
|
-
evt.callback(this._value);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Follow the ReactiveController protocol.
|
|
189
|
-
if (typeof host.addController === 'function') {
|
|
190
|
-
host.addController(this);
|
|
191
|
-
}
|
|
230
|
+
constructor(host, contextOrOptions, initialValue) {
|
|
231
|
+
const isOptions = contextOrOptions != null && contextOrOptions.context !== undefined;
|
|
232
|
+
super(isOptions ? contextOrOptions.initialValue : initialValue);
|
|
233
|
+
/** @protected */
|
|
234
|
+
this.host = host;
|
|
235
|
+
/** @private */
|
|
236
|
+
this.context = isOptions ? contextOrOptions.context : contextOrOptions;
|
|
237
|
+
this.attachListeners();
|
|
238
|
+
this.host.addController?.(this);
|
|
192
239
|
}
|
|
193
240
|
|
|
194
|
-
/** @
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
241
|
+
/** @param {Event} e */
|
|
242
|
+
onContextRequest = (e) => {
|
|
243
|
+
const ev = /** @type {ContextRequestEvent} */ (e);
|
|
244
|
+
if (ev.context !== this.context) return;
|
|
245
|
+
const consumerHost = ev.contextTarget ?? ev.composedPath()[0];
|
|
246
|
+
// A host never provides to itself: a component that both provides and
|
|
247
|
+
// consumes a context reads it from an ancestor.
|
|
248
|
+
if (consumerHost === this.host) return;
|
|
249
|
+
ev.stopPropagation();
|
|
250
|
+
this.addCallback(ev.callback, consumerHost, ev.subscribe);
|
|
251
|
+
};
|
|
198
252
|
|
|
199
253
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* re-renders via the microtask queue.
|
|
206
|
-
*
|
|
207
|
-
* @param {T} newValue
|
|
254
|
+
* A nearer provider of the same context connected below this one: re-send
|
|
255
|
+
* every subscriber's request from its consumer, so the nearest provider
|
|
256
|
+
* answers it. The consumer drops this provider's subscription when the new
|
|
257
|
+
* one calls back with a different disposer.
|
|
258
|
+
* @param {Event} e
|
|
208
259
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
260
|
+
onProviderRequest = (e) => {
|
|
261
|
+
const ev = /** @type {ContextProviderEvent} */ (e);
|
|
262
|
+
if (ev.context !== this.context) return;
|
|
263
|
+
const childProviderHost = ev.contextTarget ?? ev.composedPath()[0];
|
|
264
|
+
if (childProviderHost === this.host) return;
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
for (const [callback, { consumerHost }] of this.subscriptions) {
|
|
267
|
+
if (seen.has(callback)) continue;
|
|
268
|
+
seen.add(callback);
|
|
269
|
+
consumerHost.dispatchEvent(
|
|
270
|
+
new ContextRequestEvent(this.context, consumerHost, /** @type {any} */ (callback), true),
|
|
271
|
+
);
|
|
215
272
|
}
|
|
216
|
-
|
|
273
|
+
ev.stopPropagation();
|
|
274
|
+
};
|
|
217
275
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
this.
|
|
276
|
+
/** @private */
|
|
277
|
+
attachListeners() {
|
|
278
|
+
this.host.addEventListener('context-request', this.onContextRequest);
|
|
279
|
+
this.host.addEventListener('context-provider', this.onProviderRequest);
|
|
221
280
|
}
|
|
222
281
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
this._subscriptions.clear();
|
|
282
|
+
/** ReactiveController hook: announce this provider to the tree above. */
|
|
283
|
+
hostConnected() {
|
|
284
|
+
this.host.dispatchEvent(new ContextProviderEvent(this.context, this.host));
|
|
227
285
|
}
|
|
228
286
|
}
|
|
229
287
|
|
|
@@ -232,96 +290,162 @@ export class ContextProvider {
|
|
|
232
290
|
// ---------------------------------------------------------------------------
|
|
233
291
|
|
|
234
292
|
/**
|
|
235
|
-
* A ReactiveController that
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* for the matching context key exists higher in the DOM tree, the consumer
|
|
240
|
-
* receives the current value immediately (and, if `subscribe: true`, all
|
|
241
|
-
* future updates as well).
|
|
242
|
-
*
|
|
243
|
-
* ## AI guidance
|
|
293
|
+
* A ReactiveController that reads a context value from the nearest provider
|
|
294
|
+
* above its host. It requests the value on connect and re-renders the host
|
|
295
|
+
* on delivery. With `subscribe: true` it keeps receiving changes; the
|
|
296
|
+
* default, as in Lit, is a one-time read per connection.
|
|
244
297
|
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* the common case for reactive data like theme, auth, or locale.
|
|
248
|
-
* - Use `subscribe: false` for one-shot reads where you only need the
|
|
249
|
-
* value at connection time (e.g. reading a static config once).
|
|
250
|
-
* - Access the value via `consumer.value`. It is `undefined` until a
|
|
251
|
-
* provider responds.
|
|
252
|
-
* - If no provider exists in the ancestor chain, `value` stays
|
|
253
|
-
* `undefined`: design your render method to handle that case.
|
|
298
|
+
* `new ContextConsumer(host, { context, callback, subscribe })` or
|
|
299
|
+
* `new ContextConsumer(host, context, callback, subscribe)`.
|
|
254
300
|
*
|
|
255
301
|
* @template T
|
|
256
302
|
*/
|
|
257
303
|
export class ContextConsumer {
|
|
258
304
|
/**
|
|
259
|
-
* @param {
|
|
260
|
-
*
|
|
261
|
-
* @param {
|
|
262
|
-
*
|
|
305
|
+
* @param {any} host The host element.
|
|
306
|
+
* @param {any} contextOrOptions `{ context, callback, subscribe }`, or the context.
|
|
307
|
+
* @param {(value: T, dispose?: () => void) => void} [callback] Positional form only.
|
|
308
|
+
* @param {boolean} [subscribe] Positional form only.
|
|
263
309
|
*/
|
|
264
|
-
constructor(host,
|
|
265
|
-
/** @
|
|
266
|
-
this.
|
|
267
|
-
/** @
|
|
268
|
-
this.
|
|
269
|
-
/** @type {boolean} */
|
|
270
|
-
this._subscribe = subscribe;
|
|
310
|
+
constructor(host, contextOrOptions, callback, subscribe) {
|
|
311
|
+
/** @protected */
|
|
312
|
+
this.host = host;
|
|
313
|
+
/** @private */
|
|
314
|
+
this.provided = false;
|
|
271
315
|
/** @type {T | undefined} */
|
|
272
|
-
this.
|
|
273
|
-
/** @type {(() => void) | undefined} */
|
|
274
|
-
this.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
316
|
+
this.value = undefined;
|
|
317
|
+
/** @private @type {(() => void) | undefined} */
|
|
318
|
+
this.unsubscribe = undefined;
|
|
319
|
+
if (contextOrOptions != null && contextOrOptions.context !== undefined) {
|
|
320
|
+
/** @private */
|
|
321
|
+
this.context = contextOrOptions.context;
|
|
322
|
+
/** @private */
|
|
323
|
+
this.callback = contextOrOptions.callback;
|
|
324
|
+
/** @private */
|
|
325
|
+
this.subscribe = contextOrOptions.subscribe ?? false;
|
|
326
|
+
} else {
|
|
327
|
+
this.context = contextOrOptions;
|
|
328
|
+
this.callback = callback;
|
|
329
|
+
this.subscribe = subscribe ?? false;
|
|
278
330
|
}
|
|
331
|
+
this.host.addController(this);
|
|
279
332
|
}
|
|
280
333
|
|
|
281
|
-
/**
|
|
282
|
-
* The current context value. `undefined` if no provider has responded
|
|
283
|
-
* yet.
|
|
284
|
-
* @returns {T | undefined}
|
|
285
|
-
*/
|
|
286
|
-
get value() {
|
|
287
|
-
return this._value;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Called by the host's controller lifecycle when the element connects. */
|
|
334
|
+
/** ReactiveController hook: request the value. */
|
|
291
335
|
hostConnected() {
|
|
292
|
-
this.
|
|
336
|
+
this.dispatchRequest();
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
/**
|
|
339
|
+
/** ReactiveController hook: stop receiving changes. */
|
|
296
340
|
hostDisconnected() {
|
|
297
|
-
if (this.
|
|
298
|
-
this.
|
|
299
|
-
this.
|
|
341
|
+
if (this.unsubscribe) {
|
|
342
|
+
this.unsubscribe();
|
|
343
|
+
this.unsubscribe = undefined;
|
|
300
344
|
}
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
/** @private */
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.
|
|
307
|
-
(value, unsubscribe) => {
|
|
308
|
-
// Guard against duplicate updates with the same value.
|
|
309
|
-
const old = this._value;
|
|
310
|
-
this._value = value;
|
|
311
|
-
if (unsubscribe) {
|
|
312
|
-
this._unsubscribe = unsubscribe;
|
|
313
|
-
}
|
|
314
|
-
// Trigger a re-render of the consuming component when the value
|
|
315
|
-
// changes (skip on the very first delivery during connection,
|
|
316
|
-
// since the host will render after connectedCallback anyway -
|
|
317
|
-
// but we still update to be safe with various host lifecycles).
|
|
318
|
-
if (!Object.is(old, value) && typeof this._host.requestUpdate === 'function') {
|
|
319
|
-
this._host.requestUpdate();
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
this._subscribe,
|
|
348
|
+
dispatchRequest() {
|
|
349
|
+
this.host.dispatchEvent(
|
|
350
|
+
new ContextRequestEvent(this.context, this.host, this._callback, this.subscribe),
|
|
323
351
|
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* One stable callback per consumer, so a provider recognises a repeat
|
|
356
|
+
* request and a hand-over between providers.
|
|
357
|
+
* @private
|
|
358
|
+
* @param {T} value
|
|
359
|
+
* @param {() => void} [unsubscribe]
|
|
360
|
+
*/
|
|
361
|
+
_callback = (value, unsubscribe) => {
|
|
362
|
+
if (this.unsubscribe) {
|
|
363
|
+
// A different disposer means a different (nearer) provider answered:
|
|
364
|
+
// leave the old one.
|
|
365
|
+
if (this.unsubscribe !== unsubscribe) {
|
|
366
|
+
this.provided = false;
|
|
367
|
+
this.unsubscribe();
|
|
368
|
+
}
|
|
369
|
+
if (!this.subscribe) this.unsubscribe();
|
|
370
|
+
}
|
|
371
|
+
this.value = value;
|
|
372
|
+
this.host.requestUpdate();
|
|
373
|
+
if (!this.provided || this.subscribe) {
|
|
374
|
+
this.provided = true;
|
|
375
|
+
if (this.callback) this.callback(value, unsubscribe);
|
|
376
|
+
}
|
|
377
|
+
this.unsubscribe = unsubscribe;
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// ContextRoot
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Buffers SUBSCRIBING requests that reach it unanswered (no provider above
|
|
387
|
+
* the consumer yet, typically because the provider's element upgrades
|
|
388
|
+
* later), and replays them when a provider for that context connects.
|
|
389
|
+
* Attach one near the root, before components upgrade:
|
|
390
|
+
*
|
|
391
|
+
* ```js
|
|
392
|
+
* new ContextRoot().attach(document.body);
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
export class ContextRoot {
|
|
396
|
+
constructor() {
|
|
397
|
+
/**
|
|
398
|
+
* @private
|
|
399
|
+
* @type {Map<unknown, { callbacks: WeakMap<Element, WeakSet<Function>>, requests: Array<{ elementRef: WeakRef<Element>, callbackRef: WeakRef<Function> }> }>}
|
|
400
|
+
*/
|
|
401
|
+
this.pendingContextRequests = new Map();
|
|
402
|
+
}
|
|
324
403
|
|
|
325
|
-
|
|
404
|
+
/** @param {Element} element */
|
|
405
|
+
attach(element) {
|
|
406
|
+
element.addEventListener('context-request', this.onContextRequest);
|
|
407
|
+
element.addEventListener('context-provider', this.onContextProvider);
|
|
326
408
|
}
|
|
409
|
+
|
|
410
|
+
/** @param {Element} element */
|
|
411
|
+
detach(element) {
|
|
412
|
+
element.removeEventListener('context-request', this.onContextRequest);
|
|
413
|
+
element.removeEventListener('context-provider', this.onContextProvider);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** @private @param {Event} e */
|
|
417
|
+
onContextProvider = (e) => {
|
|
418
|
+
const event = /** @type {ContextProviderEvent} */ (e);
|
|
419
|
+
const pending = this.pendingContextRequests.get(event.context);
|
|
420
|
+
if (pending === undefined) return;
|
|
421
|
+
this.pendingContextRequests.delete(event.context);
|
|
422
|
+
for (const { elementRef, callbackRef } of pending.requests) {
|
|
423
|
+
const element = elementRef.deref();
|
|
424
|
+
const callback = callbackRef.deref();
|
|
425
|
+
if (element === undefined || callback === undefined) continue;
|
|
426
|
+
element.dispatchEvent(
|
|
427
|
+
new ContextRequestEvent(event.context, element, /** @type {any} */ (callback), true),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/** @private @param {Event} e */
|
|
433
|
+
onContextRequest = (e) => {
|
|
434
|
+
const event = /** @type {ContextRequestEvent} */ (e);
|
|
435
|
+
// Only a subscribing request can be answered later.
|
|
436
|
+
if (event.subscribe !== true) return;
|
|
437
|
+
const element = event.contextTarget ?? /** @type {Element} */ (event.composedPath()[0]);
|
|
438
|
+
const callback = event.callback;
|
|
439
|
+
let pending = this.pendingContextRequests.get(event.context);
|
|
440
|
+
if (pending === undefined) {
|
|
441
|
+
this.pendingContextRequests.set(event.context, (pending = { callbacks: new WeakMap(), requests: [] }));
|
|
442
|
+
}
|
|
443
|
+
let callbacks = pending.callbacks.get(element);
|
|
444
|
+
if (callbacks === undefined) {
|
|
445
|
+
pending.callbacks.set(element, (callbacks = new WeakSet()));
|
|
446
|
+
}
|
|
447
|
+
if (callbacks.has(callback)) return;
|
|
448
|
+
callbacks.add(callback);
|
|
449
|
+
pending.requests.push({ elementRef: new WeakRef(element), callbackRef: new WeakRef(callback) });
|
|
450
|
+
};
|
|
327
451
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Lit's default attribute converter, as `lit` exports it. */
|
|
2
|
+
export declare const defaultConverter: {
|
|
3
|
+
toAttribute(value: unknown, type?: unknown): unknown;
|
|
4
|
+
fromAttribute(value: string | null, type?: unknown): unknown;
|
|
5
|
+
};
|
|
6
|
+
/** Lit's default `hasChanged`: true when `value` and `old` differ by `Object.is`. */
|
|
7
|
+
export declare const notEqual: (value: unknown, old: unknown) => boolean;
|