+
+
+
`;
}
- _click() {
- this.consumer.bar += 1;
+
+ _decrement() {
+ this.consumer.decrement();
+ }
+
+ _increment() {
+ this.consumer.increment();
+ }
+
+ _reset() {
+ this.consumer.count = 0;
}
}
diff --git a/demo/utilities/reactive-store/my-store.js b/demo/utilities/reactive-store/my-store.js
index 75b8b09..044e205 100644
--- a/demo/utilities/reactive-store/my-store.js
+++ b/demo/utilities/reactive-store/my-store.js
@@ -3,15 +3,21 @@ import ReactiveStore from '../../../src/utilities/reactive-store/reactive-store.
export default class MyStore extends ReactiveStore {
static get properties() {
return {
- foo: { type: Number },
- bar: { type: Number },
+ count: { type: Number },
};
}
constructor() {
super();
- this.foo = 0;
- this.bar = 0;
+ this.count = 0;
+ }
+
+ decrement() {
+ this.count--;
+ }
+
+ increment() {
+ this.count++;
}
}
diff --git a/src/utilities/reactive-store/README.md b/src/utilities/reactive-store/README.md
index b41eead..172232f 100644
--- a/src/utilities/reactive-store/README.md
+++ b/src/utilities/reactive-store/README.md
@@ -19,14 +19,22 @@ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
class MyStore extends ReactiveStore {
static get properties() {
return {
- foo: { type: Number },
+ count: { type: Number },
};
}
constructor() {
super();
- this.foo = 0;
+ this.count = 0;
+ }
+
+ increment() {
+ this.count++;
+ }
+
+ decrement() {
+ this.count--;
}
}
@@ -56,15 +64,27 @@ class MyComponent extends LitElement {
render() {
// The consumer will have all the same properties defined in your store.
return html`
-
Foo: ${this.myStoreConsumer.foo}
-
+
Count: ${this.myStoreConsumer.count}
+
+
+
`;
}
- _click() {
+ _reset() {
// Updating the values from the consumer will update the store, which will then
// notify all consumers of the changes and trigger component updates.
- this.myStoreConsumer.foo += 1;
+ this.myStoreConsumer.count = 0;
+ }
+
+ _increment() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreConsumer.increment();
+ }
+
+ _decrement() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreConsumer.decrement();
}
}
@@ -86,14 +106,22 @@ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
class MyStore extends ReactiveStore {
static get properties() {
return {
- foo: { type: Number },
+ count: { type: Number },
};
}
constructor() {
super();
- this.foo = 0;
+ this.count = 0;
+ }
+
+ increment() {
+ this.count++;
+ }
+
+ decrement() {
+ this.count--;
}
}
@@ -127,16 +155,28 @@ class MyComponent extends LitElement {
// The provider will have all the same properties defined in your store, so you can
// access your store data from the provider if you wish.
return html`
-
Foo: ${this.myStoreProvider.foo}
-
+
Count: ${this.myStoreProvider.count}
+
+
+
`;
}
- _click() {
+ _reset() {
// Updating the values from the provider will update the store, which will then
// notify all consumers of the changes and trigger component updates.
- this.myStoreProvider.foo += 1;
+ this.myStoreProvider.count = 0;
+ }
+
+ _increment() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreProvider.increment();
+ }
+
+ _decrement() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreProvider.decrement();
}
}
@@ -162,16 +202,28 @@ class MyDescendantComponent extends LitElement {
render() {
// The consumer will have all the same properties defined in your store.
return html`
-
Foo: ${this.myStoreConsumer.foo}
-
+
Count: ${this.myStoreConsumer.count}
+
+
+
`;
}
- _click() {
+ _reset() {
// Updating the values from the consumer will update the store, which will then
// notify all consumers of the changes and trigger component updates for all consumers and
// the provider as well.
- this.myStoreConsumer.foo += 1;
+ this.myStoreConsumer.count = 0;
+ }
+
+ _increment() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreConsumer.increment();
+ }
+
+ _decrement() {
+ // You can access any method or property defined in the store, not just reactive properties.
+ this.myStoreConsumer.decrement();
}
}
@@ -193,14 +245,14 @@ import ReactiveStore from '@brightspace-ui/labs/utilites/reactive-store.js';
class MyStore extends ReactiveStore {
static get properties() {
return {
- foo: { type: Number },
+ count: { type: Number },
};
}
constructor() {
super();
- this.foo = 0;
+ this.count = 0;
}
}
@@ -220,7 +272,7 @@ function handlePropertyChange({ property, value, prevValue }) {
myStore.subscribe(handlePropertyChange);
// When a store property is changed, any subscribed callback functions will be invoked synchronously
-myStore.foo += 1; // console: The "foo" property changed from 0 to 1
+myStore.count += 1; // console: The "count" property changed from 0 to 1
// Unsubscribe your callback function when you no longer want to receive store updates
myStore.unsubscribe(handlePropertyChange);
@@ -324,17 +376,19 @@ If the callback function passed in does not match a currently subscribed functio
This is the class that is returned by the `createConsumer()` instance method on an instance of the store.
-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.
+This class is a [Lit Reactive Controller](https://lit.dev/docs/composition/controllers/) that when instantiated acts as a proxy for the originating store itself.
-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.
+Any Consumer class instances will have access to all the same properties and methods that the originating store does and will automatically trigger the update cycle on the host component whenever a reactive property of the store changes.
### Instance Properties
-#### The Reactive `properties`
+#### Proxied Properties and Methods
-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.
+Since the Consumer acts as a proxy for the originating store, all properties and methods from the original store will also be accessible from the Consumer. The Consumer's properties will be directly connected to the corresponding properties on the originating store instance, so they can be used as if interacting with the store directly.
-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.
+The proxied properties and methods includes methods on the `ReactiveStore` base class like `forceUpdate`, `subscribe`, `unsubscribe`, etc. However, any properties/methods that conflict with the properties/methods defined by the Consumer class itself (`changedProperties`, `hostDisconnected`, etc.) will be ignored.
+
+Since updating a reactive property on the Consumer is the same as setting it on the store, updating a reactive property will notify all consumers of changes and, in turn, all consumers will then trigger the update cycle for their respective host components.
#### `changedProperties`
@@ -356,10 +410,6 @@ The constructor for the Consumer class accepts the following parameters:
|---|---|---|---|
| `host` | LitElement | The host Lit element that the Consumer is to be connected to. | True |
-#### `forceUpdate()`
-
-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.
-
## The context `Provider` class
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.
@@ -370,11 +420,13 @@ This class is based on (and internally uses) the `ContextProvider` class from Li
### Instance Properties
-#### The Reactive `properties`
+#### Proxied Properties and Methods
+
+The context `Provider` acts as a proxy for the originating store, which means that all properties and methods from the original store will also be accessible from the `Provider`. The `Provider`'s properties will be directly connected to the corresponding properties on the originating store instance, so they can be used as if interacting with the store directly.
-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.
+The proxied properties and methods includes methods on the `ReactiveStore` base class like `forceUpdate`, `subscribe`, `unsubscribe`, etc. However, any properties/methods that conflict with the properties/methods defined by the `Provider` class itself (`changedProperties`, `hostDisconnected`, etc.) will be ignored.
-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.
+Since updating a reactive property on the `Provider` is the same as setting it on the store, updating a reactive property will notify the `Provider` and all descendant `Consumer` instances of changes and, in turn, the `Provider` and `Consumer` instances will then trigger the update cycle for their respective host components.
#### `changedProperties`
@@ -397,10 +449,6 @@ The constructor for the context `Provider` class accepts the following parameter
| `host` | LitElement instance | The host Lit element that the `Provider` is to be connected to. | True | |
| `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()` |
-#### `forceUpdate()`
-
-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.
-
## The context `Consumer` class
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.
@@ -413,11 +461,13 @@ This class is based on (and internally uses) the `ContextConsumer` class from Li
### Instance Properties
-#### The Reactive `properties`
+#### Proxied Properties and Methods
-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.
+The context `Consumer` acts as a proxy for the originating store (which it receives from the `Provider` ancestor), which means that all properties and methods from the original store will also be accessible from the `Consumer`. The `Consumer`'s properties will be directly connected to the corresponding properties on the originating store instance, so they can be used as if interacting with the store directly.
-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.
+The proxied properties and methods includes methods on the `ReactiveStore` base class like `forceUpdate`, `subscribe`, `unsubscribe`, etc. However, any properties/methods that conflict with the properties/methods defined by the `Consumer` class itself (`changedProperties`, `hostDisconnected`, etc.) will be ignored.
+
+Since updating a reactive property on the `Consumer` is the same as setting it on the store, updating a reactive property will notify the `Provider` and all descendant `Consumer` instances of changes and, in turn, the `Provider` and `Consumer` instances will then trigger the update cycle for their respective host components.
#### `changedProperties`
@@ -438,7 +488,3 @@ The constructor for the context `Consumer` class accepts the following parameter
| Parameter Name | Type | Description | Required |
|---|---|---|---|
| `host` | LitElement instance | The host Lit element that the `Consumer` is to be connected to. | True |
-
-#### `forceUpdate()`
-
-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.
diff --git a/src/utilities/reactive-store/context-controllers.js b/src/utilities/reactive-store/context-controllers.js
index 3df9df3..6552f16 100644
--- a/src/utilities/reactive-store/context-controllers.js
+++ b/src/utilities/reactive-store/context-controllers.js
@@ -3,70 +3,59 @@ import {
ContextConsumer as LitContextConsumer,
ContextProvider as LitContextProvider
} from '@lit/context';
-import StoreConsumer from './store-consumer.js';
+import StoreReactor from './store-reactor.js';
export class ContextProvider {
constructor(host, StoreClass, store = new StoreClass()) {
const { properties } = StoreClass;
- this._storeConsumer = new StoreConsumer(host, store, properties);
- this._provider = new LitContextProvider(host, {
+ const storeReactor = new StoreReactor(host, store, properties);
+ new LitContextProvider(host, {
context: createContext(StoreClass),
initialValue: store,
});
- this._defineProperties(properties);
- }
-
- get changedProperties() {
- return this._storeConsumer.changedProperties;
- }
- forceUpdate() {
- this._storeConsumer.forceUpdate();
- }
-
- _defineProperties(properties) {
- Object.keys(properties).forEach((property) => {
- Object.defineProperty(this, property, {
- get() {
- return this._storeConsumer[property];
- },
- set(value) {
- this._storeConsumer[property] = value;
+ return new Proxy(store, {
+ get(target, prop) {
+ if (prop in storeReactor) return storeReactor[prop];
+ return Reflect.get(target, prop);
+ },
+ set(target, prop, value) {
+ if (prop in storeReactor) {
+ storeReactor[prop] = value;
+ return true;
}
- });
+ return Reflect.set(target, prop, value);
+ }
});
}
}
export class ContextConsumer {
constructor(host, StoreClass) {
const { properties } = StoreClass;
- this._contextConsumer = new LitContextConsumer(host, {
+ const target = {
+ store: {},
+ storeReactor: {},
+ };
+ new LitContextConsumer(host, {
context: createContext(StoreClass),
callback: (store) => {
- this._storeConsumer = new StoreConsumer(host, store, properties);
- this._defineProperties(properties);
+ target.store = store;
+ target.storeReactor = new StoreReactor(host, store, properties);
},
});
- }
-
- get changedProperties() {
- return this._storeConsumer?.changedProperties;
- }
- forceUpdate() {
- this._storeConsumer.forceUpdate();
- }
-
- _defineProperties(properties) {
- Object.keys(properties).forEach((property) => {
- Object.defineProperty(this, property, {
- get() {
- return this._storeConsumer[property];
- },
- set(value) {
- this._storeConsumer[property] = value;
+ return new Proxy(target, {
+ get({ store, storeReactor }, prop) {
+ if (prop in storeReactor) return storeReactor[prop];
+ return Reflect.get(store, prop);
+ },
+ set({ store, storeReactor }, prop, value) {
+ if (prop in storeReactor) {
+ storeReactor[prop] = value;
+ return true;
}
- });
+ return Reflect.set(store, prop, value);
+ }
});
}
}
diff --git a/src/utilities/reactive-store/store-consumer.js b/src/utilities/reactive-store/store-consumer.js
index 416751e..b681de2 100644
--- a/src/utilities/reactive-store/store-consumer.js
+++ b/src/utilities/reactive-store/store-consumer.js
@@ -1,66 +1,21 @@
+import StoreReactor from './store-reactor.js';
+
export default class StoreConsumer {
constructor(host, store, properties = store.constructor.properties) {
- this._host = host;
- this._host.addController(this);
- this._store = store;
-
- this.changedProperties = new Map();
-
- this._onPropertyChange = this._onPropertyChange.bind(this);
- this._store.subscribe(this._onPropertyChange);
-
- this._defineProperties(properties);
- this._initializeChangedProperties(properties);
- }
-
- forceUpdate() {
- this._store.forceUpdate();
- }
-
- hostDisconnected() {
- this._store.unsubscribe(this._onPropertyChange);
- }
-
- _defineProperties(properties) {
- Object.keys(properties).forEach((property) => {
- Object.defineProperty(this, property, {
- get() {
- return this._store[property];
- },
- set(value) {
- this._store[property] = value;
+ const storeReactor = new StoreReactor(host, store, properties);
+
+ return new Proxy(store, {
+ get(target, prop) {
+ if (prop in storeReactor) return storeReactor[prop];
+ return Reflect.get(target, prop);
+ },
+ set(target, prop, value) {
+ if (prop in storeReactor) {
+ storeReactor[prop] = value;
+ return true;
}
- });
- });
- }
-
- _initializeChangedProperties(properties) {
- let shouldUpdate = false;
- Object.keys(properties).forEach((property) => {
- if (this._store[property] === undefined) return;
-
- this.changedProperties.set(property, undefined);
- shouldUpdate = true;
- });
-
- if (!shouldUpdate) return;
-
- this._host.requestUpdate();
- this._host.updateComplete.then(() => {
- this.changedProperties.clear();
- });
- }
-
- _onPropertyChange({
- property,
- prevValue,
- forceUpdate = false,
- }) {
- if (!forceUpdate && !this.changedProperties.has(property)) this.changedProperties.set(property, prevValue);
-
- this._host.requestUpdate();
- this._host.updateComplete.then(() => {
- this.changedProperties.clear();
+ return Reflect.set(target, prop, value);
+ }
});
}
}
diff --git a/src/utilities/reactive-store/store-reactor.js b/src/utilities/reactive-store/store-reactor.js
new file mode 100644
index 0000000..1191e86
--- /dev/null
+++ b/src/utilities/reactive-store/store-reactor.js
@@ -0,0 +1,52 @@
+export default class StoreReactor {
+ changedProperties;
+
+ constructor(host, store, properties = store.constructor.properties) {
+ this.#host = host;
+ this.#host.addController(this);
+ this.#store = store;
+
+ this.changedProperties = new Map();
+
+ this.#store.subscribe(this.#onPropertyChange);
+
+ this.#initializeChangedProperties(properties);
+ }
+
+ hostDisconnected() {
+ this.#store.unsubscribe(this.#onPropertyChange);
+ }
+
+ #host;
+ #store;
+
+ #onPropertyChange = ({
+ property,
+ prevValue,
+ forceUpdate = false,
+ }) => {
+ if (!forceUpdate && !this.changedProperties.has(property)) this.changedProperties.set(property, prevValue);
+
+ this.#host.requestUpdate();
+ this.#host.updateComplete.then(() => {
+ this.changedProperties.clear();
+ });
+ };
+
+ #initializeChangedProperties(properties) {
+ let shouldUpdate = false;
+ Object.keys(properties).forEach((property) => {
+ if (this.#store[property] === undefined) return;
+
+ this.changedProperties.set(property, undefined);
+ shouldUpdate = true;
+ });
+
+ if (!shouldUpdate) return;
+
+ this.#host.requestUpdate();
+ this.#host.updateComplete.then(() => {
+ this.changedProperties.clear();
+ });
+ }
+}
diff --git a/test/utilities/reactive-store/context-controllers.test.js b/test/utilities/reactive-store/context-controllers.test.js
index 8ed9740..6d016ba 100644
--- a/test/utilities/reactive-store/context-controllers.test.js
+++ b/test/utilities/reactive-store/context-controllers.test.js
@@ -18,6 +18,12 @@ class TestStore1 extends ReactiveStore {
this.objectProp = {
nestedProp: 'default'
};
+
+ this.nonReactiveProp = 'default';
+ }
+
+ testMethod() {
+ return 'test';
}
}
const { Provider: Provider1, Consumer: Consumer1 } = TestStore1.createContextControllers();
@@ -58,6 +64,7 @@ class HostingComponent extends LitElement {
${this.storeProvider1.prop1}
${this.storeProvider1.prop2}
${this.storeProvider1.objectProp.nestedProp}
+
${this.storeProvider1.nonReactiveProp}
${this.storeProvider2.foo}
${this.storeProvider2.bar}
@@ -101,6 +108,7 @@ class ConsumingComponent extends LitElement {