Skip to content

Commit 7db44f7

Browse files
committed
integrate context controller into core
1 parent 1e8f21e commit 7db44f7

7 files changed

+135
-191
lines changed

index.js

+102-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* @version 0.7.0
44
*/
55

6+
/* === Constants === */
7+
8+
const CONTEXT_REQUEST = 'context-request';
9+
610
/* === Internal variables and functions to the module === */
711

812
/**
@@ -147,12 +151,37 @@ const asNumber = value => parseFloat(value);
147151
*/
148152
const asString = value => value;
149153

154+
/**
155+
* Class for context-request events
156+
*
157+
* @class ContextRequestEvent
158+
* @extends {Event}
159+
*
160+
* @property {PropertyKey} context - context key
161+
* @property {import("../types").ContextCallback<import('./types').FxState>} callback - callback function for value getter and unsubscribe function
162+
* @property {boolean} [subscribe=false] - whether to subscribe to context changes
163+
*/
164+
class ContextRequestEvent extends Event {
165+
166+
/**
167+
* @param {PropertyKey} context - context key
168+
* @param {import("./types").ContextCallback<import('./types').FxState>} callback - callback for value getter and unsubscribe function
169+
* @param {boolean} [subscribe=false] - whether to subscribe to context changes
170+
*/
171+
constructor(context, callback, subscribe = false) {
172+
super(CONTEXT_REQUEST, { bubbles: true, cancelable: true, composed: true });
173+
this.context = context;
174+
this.callback = callback;
175+
this.subscribe = subscribe;
176+
}
177+
}
178+
150179
/* === Default export === */
151180

152181
/**
153182
* Base class for reactive custom elements
154183
*
155-
* @class
184+
* @class UIElement
156185
* @extends HTMLElement
157186
* @type {import('./types').UIElement}
158187
*/
@@ -180,8 +209,21 @@ export default class UIElement extends HTMLElement {
180209
*/
181210
attributeMap = {};
182211

212+
/**
213+
* @since 0.7.0
214+
* @property
215+
* @type {import('./types').ContextMap}
216+
*/
217+
contextMap = {};
218+
183219
// @private hold states – use `has()`, `get()`, `set()` and `delete()` to access and modify
184-
#state = new Map();
220+
#states = new Map();
221+
222+
// @private hold map of published contexts to subscribers (context consumers)
223+
#publishedContexts = new Map();
224+
225+
// @private hold map of subscribed contexts to publishers (context providers)
226+
#subscribedContexts = new Map();
185227

186228
/**
187229
* Native callback function when an observed attribute of the custom element changes
@@ -199,6 +241,55 @@ export default class UIElement extends HTMLElement {
199241
}
200242
}
201243

244+
connectedCallback() {
245+
const proto = Object.getPrototypeOf(this);
246+
247+
// context provider
248+
const provided = proto.providedContexts || [];
249+
const published = this.#publishedContexts;
250+
if (provided.length) {
251+
252+
// listen to context request events and add subscribers
253+
this.addEventListener(CONTEXT_REQUEST, (/** @type {import('./types').ContextRequestEvent} */e) => {
254+
const { target, context, callback, subscribe } = e;
255+
if (!provided.includes(context) || !isFunction(callback)) return;
256+
e.stopPropagation();
257+
const value = this.#states.get(context);
258+
if (subscribe) {
259+
const subscribers = nestMap(published, context);
260+
!subscribers.has(target) && subscribers.set(target, callback);
261+
callback(value, () => subscribers.delete(target));
262+
} else {
263+
callback(value);
264+
}
265+
});
266+
267+
// context change effects
268+
provided.forEach(context => {
269+
effect(() => {
270+
const subscribers = published.get(context);
271+
const value = this.#states.get(context);
272+
for (const [target, callback] of subscribers) callback(value, () => subscribers.delete(target));
273+
});
274+
});
275+
}
276+
277+
// context consumer
278+
setTimeout(() => { // wait for all custom elements to be defined
279+
proto.consumedContexts?.forEach(context => {
280+
const callback = (/** @type {import('./types').FxState} */ value, /** @type {() => void} */ unsubscribe) => {
281+
this.#subscribedContexts.set(context, unsubscribe);
282+
const input = this.contextMap[context];
283+
const [key, fn] = Array.isArray(input) ? input : [context, input];
284+
// @ts-ignore
285+
this.#states.set(key || context, isFunction(fn) ? fn(value) : value);
286+
};
287+
const event = new ContextRequestEvent(context, callback, true);
288+
this.dispatchEvent(event);
289+
});
290+
});
291+
}
292+
202293
/**
203294
* Check whether a state is set
204295
*
@@ -207,7 +298,7 @@ export default class UIElement extends HTMLElement {
207298
* @returns {boolean} `true` if this element has state with the given key; `false` otherwise
208299
*/
209300
has(key) {
210-
return this.#state.has(key);
301+
return this.#states.has(key);
211302
}
212303

213304
/**
@@ -218,7 +309,7 @@ export default class UIElement extends HTMLElement {
218309
* @returns {any} current value of state; undefined if state does not exist
219310
*/
220311
get(key) {
221-
return unwrap(this.#state.get(key));
312+
return unwrap(this.#states.get(key));
222313
}
223314

224315
/**
@@ -230,12 +321,12 @@ export default class UIElement extends HTMLElement {
230321
* @param {boolean} [update=true] - if `true` (default), the state is updated; if `false`, just return existing value
231322
*/
232323
set(key, value, update = true) {
233-
if (this.#state.has(key)) {
234-
const state = this.#state.get(key);
324+
if (this.#states.has(key)) {
325+
const state = this.#states.get(key);
235326
update && isFunction(state.set) && state.set(value);
236327
} else {
237328
const state = isFunction(value) && isFunction(value.set) ? value : cause(value);
238-
this.#state.set(key, state);
329+
this.#states.set(key, state);
239330
}
240331
}
241332

@@ -247,7 +338,7 @@ export default class UIElement extends HTMLElement {
247338
* @returns {boolean} `true` if the state existed and was deleted; `false` if ignored
248339
*/
249340
delete(key) {
250-
return this.#state.delete(key);
341+
return this.#states.delete(key);
251342
}
252343

253344
/**
@@ -261,7 +352,7 @@ export default class UIElement extends HTMLElement {
261352
async pass(element, states, registry = customElements) {
262353
await registry.whenDefined(element.localName);
263354
for (const [key, source] of Object.entries(states)) {
264-
element.set(key, cause(isFunction(source) ? source : this.#state.get(source)));
355+
element.set(key, cause(isFunction(source) ? source : this.#states.get(source)));
265356
}
266357
}
267358

@@ -274,12 +365,12 @@ export default class UIElement extends HTMLElement {
274365
*/
275366
targets(key) {
276367
const targets = new Set();
277-
for (const effect of this.#state.get(key).effects) {
368+
for (const effect of this.#states.get(key).effects) {
278369
for (const target of effect.targets.keys()) targets.add(target);
279370
}
280371
return targets;
281372
}
282373

283374
}
284375

285-
export { effect, asBoolean, asInteger, asNumber, asString, unwrap };
376+
export { effect, unwrap, asBoolean, asInteger, asNumber, asString, ContextRequestEvent };

index.min.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/context-controller.js

-161
This file was deleted.

lib/context-controller.min.js

-1
This file was deleted.

lib/dom-utils.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,11 @@ const highlightTargets = (el, className = 'ui-effect') => {
216216
*
217217
* @since 0.5.0
218218
*
219-
* @class
219+
* @class DebugElement
220220
* @extends {UIElement}
221221
* @type {import("../types.js").DebugElement}
222222
*/
223-
export default class DebugElement extends UIElement {
223+
class DebugElement extends UIElement {
224224

225225
/**
226226
* Wrap connectedCallback to log to the console
@@ -335,7 +335,7 @@ const component = (tag, attributeMap = {}, connect, disconnect) => {
335335
attributeMap = attributeMap;
336336

337337
connectedCallback() {
338-
DEV_MODE && super.connectedCallback();
338+
super.connectedCallback();
339339
connect && connect(this);
340340
autoEffects(this);
341341
highlightTargets(this);

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"scripts": {
88
"build": "terser index.js -c -m -o index.min.js --module",
99
"build-cause-effect": "terser lib/cause-effect.js -c -m -o lib/cause-effect.min.js --module",
10-
"build-context-controller": "terser lib/context-controller.js -c -m -o lib/context-controller.min.js --module",
1110
"build-dom-utils": "terser lib/dom-utils.js -c -m -o lib/dom-utils.min.js --module",
1211
"build-visibility-observer": "terser lib/visibility-observer.js -c -m -o lib/visibility-observer.min.js --module",
1312
"lint": "npx eslint index.js",

0 commit comments

Comments
 (0)