3
3
* @version 0.7.0
4
4
*/
5
5
6
+ /* === Constants === */
7
+
8
+ const CONTEXT_REQUEST = 'context-request' ;
9
+
6
10
/* === Internal variables and functions to the module === */
7
11
8
12
/**
@@ -147,12 +151,37 @@ const asNumber = value => parseFloat(value);
147
151
*/
148
152
const asString = value => value ;
149
153
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
+
150
179
/* === Default export === */
151
180
152
181
/**
153
182
* Base class for reactive custom elements
154
183
*
155
- * @class
184
+ * @class UIElement
156
185
* @extends HTMLElement
157
186
* @type {import('./types').UIElement }
158
187
*/
@@ -180,8 +209,21 @@ export default class UIElement extends HTMLElement {
180
209
*/
181
210
attributeMap = { } ;
182
211
212
+ /**
213
+ * @since 0.7.0
214
+ * @property
215
+ * @type {import('./types').ContextMap }
216
+ */
217
+ contextMap = { } ;
218
+
183
219
// @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 ( ) ;
185
227
186
228
/**
187
229
* Native callback function when an observed attribute of the custom element changes
@@ -199,6 +241,55 @@ export default class UIElement extends HTMLElement {
199
241
}
200
242
}
201
243
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
+
202
293
/**
203
294
* Check whether a state is set
204
295
*
@@ -207,7 +298,7 @@ export default class UIElement extends HTMLElement {
207
298
* @returns {boolean } `true` if this element has state with the given key; `false` otherwise
208
299
*/
209
300
has ( key ) {
210
- return this . #state . has ( key ) ;
301
+ return this . #states . has ( key ) ;
211
302
}
212
303
213
304
/**
@@ -218,7 +309,7 @@ export default class UIElement extends HTMLElement {
218
309
* @returns {any } current value of state; undefined if state does not exist
219
310
*/
220
311
get ( key ) {
221
- return unwrap ( this . #state . get ( key ) ) ;
312
+ return unwrap ( this . #states . get ( key ) ) ;
222
313
}
223
314
224
315
/**
@@ -230,12 +321,12 @@ export default class UIElement extends HTMLElement {
230
321
* @param {boolean } [update=true] - if `true` (default), the state is updated; if `false`, just return existing value
231
322
*/
232
323
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 ) ;
235
326
update && isFunction ( state . set ) && state . set ( value ) ;
236
327
} else {
237
328
const state = isFunction ( value ) && isFunction ( value . set ) ? value : cause ( value ) ;
238
- this . #state . set ( key , state ) ;
329
+ this . #states . set ( key , state ) ;
239
330
}
240
331
}
241
332
@@ -247,7 +338,7 @@ export default class UIElement extends HTMLElement {
247
338
* @returns {boolean } `true` if the state existed and was deleted; `false` if ignored
248
339
*/
249
340
delete ( key ) {
250
- return this . #state . delete ( key ) ;
341
+ return this . #states . delete ( key ) ;
251
342
}
252
343
253
344
/**
@@ -261,7 +352,7 @@ export default class UIElement extends HTMLElement {
261
352
async pass ( element , states , registry = customElements ) {
262
353
await registry . whenDefined ( element . localName ) ;
263
354
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 ) ) ) ;
265
356
}
266
357
}
267
358
@@ -274,12 +365,12 @@ export default class UIElement extends HTMLElement {
274
365
*/
275
366
targets ( key ) {
276
367
const targets = new Set ( ) ;
277
- for ( const effect of this . #state . get ( key ) . effects ) {
368
+ for ( const effect of this . #states . get ( key ) . effects ) {
278
369
for ( const target of effect . targets . keys ( ) ) targets . add ( target ) ;
279
370
}
280
371
return targets ;
281
372
}
282
373
283
374
}
284
375
285
- export { effect , asBoolean , asInteger , asNumber , asString , unwrap } ;
376
+ export { effect , unwrap , asBoolean , asInteger , asNumber , asString , ContextRequestEvent } ;
0 commit comments