@brightspace-ui/labs 2.9.1 → 2.10.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/package.json CHANGED
@@ -39,7 +39,9 @@
39
39
  "./components/wizard-step.js": "./src/components/wizard/step.js",
40
40
  "./controllers/computed-value.js": "./src/controllers/computed-values/computed-value.js",
41
41
  "./controllers/computed-values.js": "./src/controllers/computed-values/computed-values.js",
42
- "./controllers/language-listener.js": "./src/controllers/language-listener/language-listener.js"
42
+ "./controllers/language-listener.js": "./src/controllers/language-listener/language-listener.js",
43
+ "./utilities/pub-sub.js": "./src/utilities/pub-sub/pub-sub.js",
44
+ "./utilities/reactive-store.js": "./src/utilities/reactive-store/reactive-store.js"
43
45
  },
44
46
  "scripts": {
45
47
  "langs:sync": "mfv add-missing && mfv remove-extraneous",
@@ -74,7 +76,8 @@
74
76
  },
75
77
  "dependencies": {
76
78
  "@brightspace-ui/core": "^3",
79
+ "@lit/context": "^1.1.3",
77
80
  "lit": "^3"
78
81
  },
79
- "version": "2.9.1"
82
+ "version": "2.10.0"
80
83
  }
@@ -0,0 +1,76 @@
1
+ # PubSub
2
+
3
+ A simple class implementation of the publish-subscribe model.
4
+
5
+ ## Simple Example
6
+
7
+ ```js
8
+ import PubSub from '@brightspace-ui/labs/utilites/pub-sub.js';
9
+
10
+ // Instantiate the PubSub class
11
+ const myPubSub = new PubSub();
12
+
13
+ // Register subscribers
14
+ const subscriber1 = (message) => console.log('Subscriber 1 received: ', message);
15
+ const subscriber2 = (message) => console.log('Subscriber 2 received: ', message);
16
+ myPubSub.subscribe(subscriber1);
17
+ myPubSub.subscribe(subscriber2);
18
+
19
+ // Publish messages to subscribers
20
+ myPubSub.publish('Hello!');
21
+ // Console: Subscriber 1 received: Hello!
22
+ // Console: Subscriber 2 received: Hello!
23
+
24
+ // Unsubscribe
25
+ myPubSub.unsubscribe(subscriber1);
26
+ myPubSub.unsubscribe(subscriber2);
27
+ ```
28
+
29
+ ## Instance Methods
30
+
31
+ ### Constructor
32
+
33
+ The constructor takes no arguments.
34
+
35
+ ### `clear()`
36
+
37
+ The `clear` method unsubscribes all subscribed callback functions. Subscriber callback functions are not called at this time, just removed.
38
+
39
+ This method accepts no arguments and returns no values.
40
+
41
+ ### `publish(...args)`
42
+
43
+ The `publish` method is used to publish messages/data to all subscribed callbacks. All subscribed callback functions are called in subscription order with the same arguments passed to `publish`.
44
+
45
+ | Parameter Name | Type | Description | Required |
46
+ |---|---|---|---|
47
+ | `...args` | Any | The arguments to be passed to subscriber callback functions when called. | No |
48
+
49
+ `publish` returns no values.
50
+
51
+ ### `subscribe(callback, initialize = false)`
52
+
53
+ The `subscribe` method is used to subscribe a callback function for future published messages.
54
+
55
+ If the callback being subscribed is already subscribed, nothing will change and it will still only be called once when messages are published.
56
+
57
+ By default, a subscribed callback function won't be called until a message is published, but if the `initialize` argument is set to `true` and the `PubSub` instance has published at least once, then the callback function will be immediately called after subscription with the last published arguments.
58
+
59
+ | Parameter Name | Type | Description | Required | Default Value |
60
+ |---|---|---|---|---|
61
+ | `callback` | Function | The callback function to be called when messages are published. | True | |
62
+ | `initialize` | Boolean | Whether or not to immedately call the callback function with the last published values. | False | `false` |
63
+
64
+ `subscribe` returns no values.
65
+
66
+ ### `unsubscribe(callback)`
67
+
68
+ The `unsubscribe` method is used to remove a callback function from the collection of subscribers so it no longer receives future published messages.
69
+
70
+ If the callback function passed in does not match a currently subscribed function, nothing happens.
71
+
72
+ | Parameter Name | Type | Description | Required |
73
+ |---|---|---|---|
74
+ | `callback` | Function | The callback function to remove from the subscribers. | True |
75
+
76
+ `unsubscribe` returns no values.
@@ -0,0 +1,33 @@
1
+ /*
2
+ A simple pub-sub implementation. It allows for subscribing to the class and publishing to all subscribers.
3
+ */
4
+ export default class PubSub {
5
+ constructor() {
6
+ this._subscribers = new Map();
7
+ this._hasTriggered = false;
8
+ this._previousArgs = [];
9
+ }
10
+
11
+ clear() {
12
+ this._subscribers.clear();
13
+ }
14
+
15
+ publish(...args) {
16
+ this._subscribers.forEach(callback => callback(...args));
17
+ this._hasTriggered = true;
18
+ this._previousArgs = args;
19
+ }
20
+
21
+ // If initialize is true and publish has been called at least once, the callback will be called
22
+ // immediately with the last published arguments.
23
+ subscribe(callback, initialize = false) {
24
+ this._subscribers.set(callback, callback);
25
+ if (this._hasTriggered && initialize) {
26
+ callback(...this._previousArgs);
27
+ }
28
+ }
29
+
30
+ unsubscribe(callback) {
31
+ this._subscribers.delete(callback);
32
+ }
33
+ }
@@ -0,0 +1,444 @@
1
+ # ReactiveStore
2
+
3
+ A simple data store that will automatically notify subscribers when any of its properties changes.
4
+
5
+ It's designed to work as a state store for Lit based apps and mimics Lit's component class API.
6
+
7
+ ## Usage Examples
8
+
9
+ ### Basic Usage
10
+
11
+ First, define and instantiate your own Reactive Store:
12
+
13
+ ```js
14
+ // my-store.js
15
+
16
+ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
17
+
18
+ // Define your store with its reactive properties
19
+ class MyStore extends ReactiveStore {
20
+ static get properties() {
21
+ return {
22
+ foo: { type: Number },
23
+ };
24
+ }
25
+
26
+ constructor() {
27
+ super();
28
+
29
+ this.foo = 0;
30
+ }
31
+ }
32
+
33
+ // Create an instance of your store
34
+ const myStore = new MyStore();
35
+
36
+ // Create and export a consumer class
37
+ export const MyStoreConsumer = myStore.createConsumer();
38
+ ```
39
+
40
+ Then, connect to your store from any Lit component using the consumer:
41
+
42
+ ```js
43
+ // my-component.js
44
+
45
+ import { MyStoreConsumer } from './my-store.js';
46
+
47
+ class MyComponent extends LitElement {
48
+ constructor() {
49
+ super();
50
+
51
+ // Connect to the store by instantiating the consumer.
52
+ // This will automatically notify your component of changes to the store properties.
53
+ this.myStoreConsumer = new MyStoreConsumer(this);
54
+ }
55
+
56
+ render() {
57
+ // The consumer will have all the same properties defined in your store.
58
+ return html`
59
+ <div>Foo: ${this.myStoreConsumer.foo}</div>
60
+ <button @click=${this._click}>Update foo</button>
61
+ `;
62
+ }
63
+
64
+ _click() {
65
+ // Updating the values from the consumer will update the store, which will then
66
+ // notify all consumers of the changes and trigger component updates.
67
+ this.myStoreConsumer.foo += 1;
68
+ }
69
+ }
70
+
71
+ customElements.define('my-component', MyComponent);
72
+ ```
73
+
74
+ ### Context Example
75
+
76
+ If you want to create a store that's tied to a branch of your DOM tree instead of being shared globally, you can generate a pair of Context Provider/Consumer reactive controllers.
77
+
78
+ Like the basic usage example, you'll first define and instantiate your own Reactive Store, but instead of instantiating the store and creating a consumer for it, you'll create a Context Provider/Consumer pair for it:
79
+
80
+ ```js
81
+ // my-store.js
82
+
83
+ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
84
+
85
+ // Define your store with its reactive properties
86
+ class MyStore extends ReactiveStore {
87
+ static get properties() {
88
+ return {
89
+ foo: { type: Number },
90
+ };
91
+ }
92
+
93
+ constructor() {
94
+ super();
95
+
96
+ this.foo = 0;
97
+ }
98
+ }
99
+
100
+ // Generate and export the Context Provider/Consumer pair for your store
101
+ const {
102
+ Provider: MyStoreContextProvider,
103
+ Consumer: MyStoreContextConsumer
104
+ } = MyStore.createContextControllers();
105
+ export { MyStoreContextProvider, MyStoreContextConsumer };
106
+ ```
107
+
108
+ Then, instantiate the provider within the component you want to provide the store from:
109
+
110
+ ```js
111
+ // my-component.js
112
+
113
+ import { MyStoreContextProvider } from './my-store.js';
114
+
115
+ class MyComponent extends LitElement {
116
+ constructor() {
117
+ super();
118
+
119
+ // Instantiate the provider here to provide an instance of your store to all descendants of
120
+ // this component.
121
+ // Note: This creates a new instance of your store by default, but it's possible to pass a
122
+ // pre-existing instance to the constructor instead.
123
+ this.myStoreProvider = new MyStoreContextProvider(this);
124
+ }
125
+
126
+ render() {
127
+ // The provider will have all the same properties defined in your store, so you can
128
+ // access your store data from the provider if you wish.
129
+ return html`
130
+ <div>Foo: ${this.myStoreProvider.foo}</div>
131
+ <button @click=${this._click}>Update foo</button>
132
+ <my-descendant-component></my-descendant-component>
133
+ `;
134
+ }
135
+
136
+ _click() {
137
+ // Updating the values from the provider will update the store, which will then
138
+ // notify all consumers of the changes and trigger component updates.
139
+ this.myStoreProvider.foo += 1;
140
+ }
141
+ }
142
+
143
+ customElements.define('my-component', MyComponent);
144
+ ```
145
+
146
+ Finally, any component that is descended from the component with the store provider can connect to the store by using your store's Context Consumer:
147
+
148
+ ```js
149
+ // my-descendant-component.js
150
+
151
+ import { MyStoreContextConsumer } from './my-store.js';
152
+
153
+ class MyDescendantComponent extends LitElement {
154
+ constructor() {
155
+ super();
156
+
157
+ // Connect to the store by instantiating the context consumer.
158
+ // This will automatically notify your component of changes to the store properties.
159
+ this.myStoreConsumer = new MyStoreContextConsumer(this);
160
+ }
161
+
162
+ render() {
163
+ // The consumer will have all the same properties defined in your store.
164
+ return html`
165
+ <div>Foo: ${this.myStoreConsumer.foo}</div>
166
+ <button @click=${this._click}>Update foo</button>
167
+ `;
168
+ }
169
+
170
+ _click() {
171
+ // Updating the values from the consumer will update the store, which will then
172
+ // notify all consumers of the changes and trigger component updates for all consumers and
173
+ // the provider as well.
174
+ this.myStoreConsumer.foo += 1;
175
+ }
176
+ }
177
+
178
+ customElements.define('my-descendant-component', MyDescendantComponent);
179
+ ```
180
+
181
+ ### Non-Lit Example
182
+
183
+ While the `ReactiveStore` was designed with Lit components in mind, there may be situations where you want to connect something other than a Lit component to a store. For such scenarios, the `ReactiveStore` provides a pair of `subscribe`/`unsubscribe` methods.
184
+
185
+ Similar to other uses, you'll first define and instantiate your own Reactive Store:
186
+
187
+ ```js
188
+ // my-store.js
189
+
190
+ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
191
+
192
+ // Define your store with its reactive properties
193
+ class MyStore extends ReactiveStore {
194
+ static get properties() {
195
+ return {
196
+ foo: { type: Number },
197
+ };
198
+ }
199
+
200
+ constructor() {
201
+ super();
202
+
203
+ this.foo = 0;
204
+ }
205
+ }
206
+
207
+ // Create and export an instance of your store
208
+ export const myStore = new MyStore();
209
+ ```
210
+
211
+ Then, you can subscribe a callback directly to the store from anywhere and that callback will be called whenever a store property changes, just don't forget to unsubscribe your callback when you no longer need it:
212
+
213
+ ```js
214
+ import { myStore } from './my-store.js';
215
+
216
+ // Define and subscribe a callback function
217
+ function handlePropertyChange({ property, value, prevValue }) {
218
+ console.log(`The "${property}" property changed from ${prevValue} to ${value}`);
219
+ }
220
+ myStore.subscribe(handlePropertyChange);
221
+
222
+ // When a store property is changed, any subscribed callback functions will be invoked synchronously
223
+ myStore.foo += 1; // console: The "foo" property changed from 0 to 1
224
+
225
+ // Unsubscribe your callback function when you no longer want to receive store updates
226
+ myStore.unsubscribe(handlePropertyChange);
227
+ ```
228
+
229
+ # API
230
+
231
+ ## The `ReactiveStore` class
232
+
233
+ The `ReactiveStore` class is an abstract class that can be extended to define your own store.
234
+
235
+ ### Static Properties
236
+
237
+ #### `properties`
238
+
239
+ This static propery must be created by the extending class. This property must be an object where each key represents a reactive property to be added to the extending store. The value for each of the property keys must be an object representing the property options.
240
+
241
+ The available property options are as follows:
242
+
243
+ | Option | Type | Description | Required | Default Value |
244
+ |---|---|---|---|---|
245
+ | `hasChanged(oldValue, newValue)` | Function | This comparison function is called when the store property is set. It is called with both the previous value and the new value to be set. If the function returns `true`, the store will notify all consumers of the value change. | False | `(oldValue, newValue) => oldValue !== newValue` |
246
+
247
+ ### Static Methods
248
+
249
+ #### `createContextControllers()`
250
+
251
+ This static method is used to generate a Provider/Consumer pair of Reactive Controllers. These controllers can be used to provide a store to Lit components through the use of [Context](https://lit.dev/docs/data/context/).
252
+
253
+ This method should never be called on the `ReactiveStore` class directly, instead it should be called on the extending class.
254
+
255
+ Calling this method on the extending class returns an object with the following properties:
256
+
257
+ | Property | Type | Description |
258
+ |---|---|---|
259
+ | `Provider` | class | A Reactive Controller responsible for providing the store instance to any descendant components that instantiate the Consumer class. |
260
+ | `Consumer` | class | A Reactive Controller that connects the hosting component to the store provided by the ancestor component that instantiated the Provider class. |
261
+
262
+ ### Instance Properties
263
+
264
+ #### The Reactive `properties`
265
+
266
+ Each of the reactive properties defined in the static `properties` object will have a getter and setter generated at object construction time.
267
+
268
+ Any time the property value is updated, the generated setter will check if the property has changed from its previous value. If the property has changed, it will update it and call all subscriber callback functions that have been registered with the store.
269
+
270
+ Note that since this is a setter, the value of the property itself must be changed for subscribers to be notified of the change. If you change a nested value of the property, the store will not detect that change. If you wish to manually trigger an update notification after changing a nested property, you can call the `forceUpdate()` instance method.
271
+
272
+ ### Instance Methods
273
+
274
+ #### `constructor()`
275
+
276
+ The store constructor. Make sure to call `super()` when overriding.
277
+
278
+ The reactive property accessors are dynamically generated in the constructor.
279
+
280
+ #### `createConsumer()`
281
+
282
+ This method can be used on any store instance (i.e. an instance of a class that extends the `ReactiveStore`). When called, it generates and returns a Reactive Controller consumer class that can be used to connect a Lit component to the store instance.
283
+
284
+ #### `forceUpdate()`
285
+
286
+ This method can be used on any store instance (i.e. an instance of a class that extends the `ReactiveStore`). When called, it notifies all subscribers that a value within the store has changed without specifying which one.
287
+
288
+ This method should not be needed in most scenarios since changes to the reactive properties should be the primary way to send update notifications to subscribers. However, this method can be used in cases where a deeply nested property of one the reactive properties has been changed and you wish to notify subscribers that the store has changed.
289
+
290
+ #### `subscribe(callback)`
291
+
292
+ This method can be used on any store instance (i.e. an instance of a class that extends the `ReactiveStore`). This method is used to subscribe a callback function for future store update notifications.
293
+
294
+ If the callback being subscribed is already subscribed, nothing will change and it will still only be called once when the store updates.
295
+
296
+ | Parameter Name | Type | Description | Required |
297
+ |---|---|---|---|
298
+ | `callback` | Function | The callback function to be called when the store is updated. | True |
299
+
300
+ The callback function itself is called with an object as it's first and only parameter. The object contains the following properties:
301
+
302
+ | Property Name | Type | Description |
303
+ |---|---|---|
304
+ | `property` | String | The name of the property that was changed. Will be `undefined` if this update was triggered by a `forceUpdate()` call. |
305
+ | `value` | Any | The new value of the changed property. Will be `undefined` if this update was triggered by a `forceUpdate()` call. |
306
+ | `prevValue` | Any | The previous value of the changed property. Will be `undefined` if this update was triggered by a `forceUpdate()` call. |
307
+ | `forceUpdate` | Boolean | Is `true` if this update was triggered by a `forceUpdate()` call. It will be `false` otherwise |
308
+
309
+ `subscribe` returns no values.
310
+
311
+ #### `unsubscribe(callback)`
312
+
313
+ This method can be used on any store instance (i.e. an instance of a class that extends the `ReactiveStore`). This method is used to remove a callback function from the collection of subscribers so it no longer receives future store update notifications.
314
+
315
+ If the callback function passed in does not match a currently subscribed function, nothing happens.
316
+
317
+ | Parameter Name | Type | Description | Required |
318
+ |---|---|---|---|
319
+ | `callback` | Function | The callback function to remove from the subscribers. | True |
320
+
321
+ `unsubscribe` returns no values.
322
+
323
+ ## The store Consumer class
324
+
325
+ This is the class that is returned by the `createConsumer()` instance method on an instance of the store.
326
+
327
+ This class is a [Lit Reactive Controller](https://lit.dev/docs/composition/controllers/) that when instantiated can be used by a Lit component to connect to the originating store instance.
328
+
329
+ Any Consumer class instances will have access to all the same properties that the originating store does and will automatically trigger the update cycle on the host component whenever a property of the store changes.
330
+
331
+ ### Instance Properties
332
+
333
+ #### The Reactive `properties`
334
+
335
+ Just like the store has a set of properties dynamically generated, the Consumer class will have the same property accessors generated at construction time. The Consumer's properties will be directly connected to the corresponding properties on the originating store instance, so they can be used as if connecting to the store directly.
336
+
337
+ Setting any of these properties will call the corresponding setter on the originating store, and since the store notifies all consumers of changes, all consumers will then trigger the update cycle for their respective host components.
338
+
339
+ #### `changedProperties`
340
+
341
+ Each Consumer instance has a `changedProperties` Map that keeps track of which reactive properties of the store have changed since the end of the host component's last update cycle.
342
+
343
+ Each key of the map represents a reactive property of the store that has changed since the last update cycle of the host Lit component and the corresponding value will be the previous value of that reactive property.
344
+
345
+ This map serves a similar function to the `changedProperties` map that [Lit provides](https://lit.dev/docs/components/lifecycle/#changed-properties) as an argument to lifecycle methods like `shouldUpdate` and `willUpdate`.
346
+
347
+ Note that `changedProperties` is unique for each instance of the Consumer class and is explicitly tied to the hosting component's update cycle.
348
+
349
+ ### Instance Methods
350
+
351
+ #### `constructor(host)`
352
+
353
+ The constructor for the Consumer class accepts the following parameters:
354
+
355
+ | Parameter Name | Type | Description | Required |
356
+ |---|---|---|---|
357
+ | `host` | LitElement | The host Lit element that the Consumer is to be connected to. | True |
358
+
359
+ #### `forceUpdate()`
360
+
361
+ This method can be used to call the originating store's own `forceUpdate()` method, which will trigger an update for all consumer host components. See the store's `forceUpdate()` definition for additional details.
362
+
363
+ ## The context `Provider` class
364
+
365
+ The context `Provider` class is one of the two [Lit Reactive Controllers](https://lit.dev/docs/composition/controllers/) returned by the `createContextControllers()` static method. The context `Consumer` class is the other controller returned and both of these act as a pair. Both of these controllers also have ther functionality tied to the specific store class you used to generate them.
366
+
367
+ The context `Provider` controller can be used to provide an instance of your store to a portion of your DOM tree. This is done by instantiating it and connecting it to a host Lit component. Once connected, all descendants of the host component will be able to connect to the provided store by using the corresponding `Consumer` controller.
368
+
369
+ This class is based on (and internally uses) the `ContextProvider` class from Lit's [Context](https://lit.dev/docs/data/context/) library.
370
+
371
+ ### Instance Properties
372
+
373
+ #### The Reactive `properties`
374
+
375
+ Just like the store has a set of properties dynamically generated, the context `Provider` class will have the same property accessors generated at construction time. The `Provider`'s properties will be directly connected to the corresponding properties on the store instance that it provides, so they can be used as if connecting to the store directly.
376
+
377
+ Setting any of these properties will call the corresponding setter on the provided store and the Provider will trigger a corresponding lifecycle update for its hosting component. Any context `Consumer` instances that are descendants of the `Provider` will also trigger the lifecycle update for their corresponding hosting components.
378
+
379
+ #### `changedProperties`
380
+
381
+ Each context `Provider` instance has a `changedProperties` Map that keeps track of which reactive properties of the provided store have changed since the end of the host component's last update cycle.
382
+
383
+ Each key of the map represents a reactive property of the provided store that has changed since the last update cycle of the host Lit component and the corresponding value will be the previous value of that reactive property.
384
+
385
+ This map serves a similar function to the `changedProperties` map that [Lit provides](https://lit.dev/docs/components/lifecycle/#changed-properties) as an argument to lifecycle methods like `shouldUpdate` and `willUpdate`.
386
+
387
+ Note that `changedProperties` is unique for each instance of the `Provider` class and is explicitly tied to the hosting component's update cycle.
388
+
389
+ ### Instance Methods
390
+
391
+ #### `constructor(host, store = new StoreClass())`
392
+
393
+ The constructor for the context `Provider` class accepts the following parameters:
394
+
395
+ | Parameter Name | Type | Description | Required | Default Value |
396
+ |---|---|---|---|---|
397
+ | `host` | LitElement instance | The host Lit element that the `Provider` is to be connected to. | True | |
398
+ | `store` | ReactiveStore instance | The instance of your store that you wish to provide to descendant context `Consumer` classes. Note that if no store instance is passed to this parameter, an instance of your store will be instantiated to be used. | True | `new StoreClass()` |
399
+
400
+ #### `forceUpdate()`
401
+
402
+ This method can be used to call the provided store's own `forceUpdate()` method, which will trigger an update for the `Provider`'s host component as well as all context `Consumer` host components. See the store's `forceUpdate()` definition for additional details.
403
+
404
+ ## The context `Consumer` class
405
+
406
+ The context `Consumer` class is one of the two [Lit Reactive Controllers](https://lit.dev/docs/composition/controllers/) returned by the `createContextControllers()` static method. The context `Provider` class is the other controller returned and both of these act as a pair. Both of these controllers also have ther functionality tied to the specific store class you used to generate them.
407
+
408
+ > Note: This class is separate from the store Consumer class provided by calling a store instance's `createConsumer()`.
409
+
410
+ The context `Consumer` controller can be used to connect to a context `Provider` class that has been instantiated and connected to an ancestor in the DOM tree. This is done by instantiating it and connecting it to a host Lit component.
411
+
412
+ This class is based on (and internally uses) the `ContextConsumer` class from Lit's [Context](https://lit.dev/docs/data/context/) library.
413
+
414
+ ### Instance Properties
415
+
416
+ #### The Reactive `properties`
417
+
418
+ Just like the store has a set of properties dynamically generated, the context `Consumer` class will have the same property accessors generated at construction time. The `Consumer`'s properties will be directly connected to the corresponding properties on the store instance that it receives from the context `Provider` ancestor, so they can be used as if connecting to the store directly.
419
+
420
+ Setting any of these properties will call the corresponding setter on the provided store and the `Provider` will trigger a corresponding lifecycle update for its hosting component. Any context `Consumer` instances (including this one) that are descendants of that `Provider` will also trigger the lifecycle update for their corresponding hosting components.
421
+
422
+ #### `changedProperties`
423
+
424
+ Each context `Consumer` instance has a `changedProperties` Map that keeps track of which reactive properties of the provided store have changed since the end of the host component's last update cycle.
425
+
426
+ Each key of the map represents a reactive property of the provided store that has changed since the last update cycle of the host Lit component and the corresponding value will be the previous value of that reactive property.
427
+
428
+ This map serves a similar function to the `changedProperties` map that [Lit provides](https://lit.dev/docs/components/lifecycle/#changed-properties) as an argument to lifecycle methods like `shouldUpdate` and `willUpdate`.
429
+
430
+ Note that `changedProperties` is unique for each instance of the context `Consumer` class and is explicitly tied to the hosting component's update cycle.
431
+
432
+ ### Instance Methods
433
+
434
+ #### `constructor(host)`
435
+
436
+ The constructor for the context `Consumer` class accepts the following parameters:
437
+
438
+ | Parameter Name | Type | Description | Required |
439
+ |---|---|---|---|
440
+ | `host` | LitElement instance | The host Lit element that the `Consumer` is to be connected to. | True |
441
+
442
+ #### `forceUpdate()`
443
+
444
+ This method can be used to call the provided store's own `forceUpdate()` method, which will trigger an update for the `Provider`'s host component as well as all context `Consumer` host components. See the store's `forceUpdate()` definition for additional details.
@@ -0,0 +1,87 @@
1
+ import {
2
+ createContext,
3
+ ContextConsumer as LitContextConsumer,
4
+ ContextProvider as LitContextProvider
5
+ } from '@lit/context';
6
+ import StoreConsumer from './store-consumer.js';
7
+
8
+ export class ContextProvider {
9
+ constructor(host, StoreClass, store = new StoreClass()) {
10
+ const { properties } = StoreClass;
11
+ this._storeConsumer = new StoreConsumer(host, store, properties);
12
+ this._provider = new LitContextProvider(host, {
13
+ context: createContext(StoreClass),
14
+ initialValue: store,
15
+ });
16
+ this._defineProperties(properties);
17
+ }
18
+
19
+ get changedProperties() {
20
+ return this._storeConsumer.changedProperties;
21
+ }
22
+
23
+ forceUpdate() {
24
+ this._storeConsumer.forceUpdate();
25
+ }
26
+
27
+ _defineProperties(properties) {
28
+ Object.keys(properties).forEach((property) => {
29
+ Object.defineProperty(this, property, {
30
+ get() {
31
+ return this._storeConsumer[property];
32
+ },
33
+ set(value) {
34
+ this._storeConsumer[property] = value;
35
+ }
36
+ });
37
+ });
38
+ }
39
+ }
40
+ export class ContextConsumer {
41
+ constructor(host, StoreClass) {
42
+ const { properties } = StoreClass;
43
+ this._contextConsumer = new LitContextConsumer(host, {
44
+ context: createContext(StoreClass),
45
+ callback: (store) => {
46
+ this._storeConsumer = new StoreConsumer(host, store, properties);
47
+ this._defineProperties(properties);
48
+ },
49
+ });
50
+ }
51
+
52
+ get changedProperties() {
53
+ return this._storeConsumer?.changedProperties;
54
+ }
55
+
56
+ forceUpdate() {
57
+ this._storeConsumer.forceUpdate();
58
+ }
59
+
60
+ _defineProperties(properties) {
61
+ Object.keys(properties).forEach((property) => {
62
+ Object.defineProperty(this, property, {
63
+ get() {
64
+ return this._storeConsumer[property];
65
+ },
66
+ set(value) {
67
+ this._storeConsumer[property] = value;
68
+ }
69
+ });
70
+ });
71
+ }
72
+ }
73
+
74
+ export function createContextControllers(StoreClass) {
75
+ return {
76
+ Provider: class extends ContextProvider {
77
+ constructor(host, store = new StoreClass()) {
78
+ super(host, StoreClass, store);
79
+ }
80
+ },
81
+ Consumer: class extends ContextConsumer {
82
+ constructor(host) {
83
+ super(host, StoreClass);
84
+ }
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,57 @@
1
+ import { createContextControllers } from './context-controllers.js';
2
+ import PubSub from '../pub-sub/pub-sub.js';
3
+ import StoreConsumer from './store-consumer.js';
4
+
5
+ const defaultHasChanged = (oldValue, newValue) => oldValue !== newValue;
6
+
7
+ export default class ReactiveStore {
8
+ static createContextControllers() {
9
+ return createContextControllers(this);
10
+ }
11
+
12
+ constructor() {
13
+ this._pubSub = new PubSub();
14
+ this._state = {};
15
+ this._defineProperties(this.constructor.properties);
16
+ }
17
+
18
+ createConsumer() {
19
+ const store = this;
20
+ return class extends StoreConsumer {
21
+ constructor(host) {
22
+ super(host, store);
23
+ }
24
+ };
25
+ }
26
+
27
+ forceUpdate() {
28
+ this._pubSub.publish({ forceUpdate: true });
29
+ }
30
+
31
+ subscribe(callback) {
32
+ this._pubSub.subscribe(callback);
33
+ }
34
+
35
+ unsubscribe(callback) {
36
+ this._pubSub.unsubscribe(callback);
37
+ }
38
+
39
+ _defineProperties(properties) {
40
+ Object.keys(properties).forEach((property) => {
41
+ Object.defineProperty(this, property, {
42
+ get() {
43
+ return this._state[property];
44
+ },
45
+ set(value) {
46
+ const { hasChanged = defaultHasChanged } = properties[property];
47
+ if (!hasChanged(this._state[property], value)) return;
48
+
49
+ const prevValue = this._state[property];
50
+ this._state[property] = value;
51
+
52
+ this._pubSub.publish({ property, value, prevValue, forceUpdate: false });
53
+ }
54
+ });
55
+ });
56
+ }
57
+ }
@@ -0,0 +1,66 @@
1
+ export default class StoreConsumer {
2
+ constructor(host, store, properties = store.constructor.properties) {
3
+ this._host = host;
4
+ this._host.addController(this);
5
+ this._store = store;
6
+
7
+ this.changedProperties = new Map();
8
+
9
+ this._onPropertyChange = this._onPropertyChange.bind(this);
10
+ this._store.subscribe(this._onPropertyChange);
11
+
12
+ this._defineProperties(properties);
13
+ this._initializeChangedProperties(properties);
14
+ }
15
+
16
+ forceUpdate() {
17
+ this._store.forceUpdate();
18
+ }
19
+
20
+ hostDisconnected() {
21
+ this._store.unsubscribe(this._onPropertyChange);
22
+ }
23
+
24
+ _defineProperties(properties) {
25
+ Object.keys(properties).forEach((property) => {
26
+ Object.defineProperty(this, property, {
27
+ get() {
28
+ return this._store[property];
29
+ },
30
+ set(value) {
31
+ this._store[property] = value;
32
+ }
33
+ });
34
+ });
35
+ }
36
+
37
+ _initializeChangedProperties(properties) {
38
+ let shouldUpdate = false;
39
+ Object.keys(properties).forEach((property) => {
40
+ if (this._store[property] === undefined) return;
41
+
42
+ this.changedProperties.set(property, undefined);
43
+ shouldUpdate = true;
44
+ });
45
+
46
+ if (!shouldUpdate) return;
47
+
48
+ this._host.requestUpdate();
49
+ this._host.updateComplete.then(() => {
50
+ this.changedProperties.clear();
51
+ });
52
+ }
53
+
54
+ _onPropertyChange({
55
+ property,
56
+ prevValue,
57
+ forceUpdate = false,
58
+ }) {
59
+ if (!forceUpdate && !this.changedProperties.has(property)) this.changedProperties.set(property, prevValue);
60
+
61
+ this._host.requestUpdate();
62
+ this._host.updateComplete.then(() => {
63
+ this.changedProperties.clear();
64
+ });
65
+ }
66
+ }