Skip to content

Commit

Permalink
feat: Enhance useStore to provide direct access to store instances an…
Browse files Browse the repository at this point in the history
…d values, update TypeScript types, and add corresponding tests
  • Loading branch information
omarluq committed Nov 23, 2023
1 parent dfa0012 commit 43ffbd2
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 31 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"@hotwired/stimulus": "^3.2.2"
}
}
6 changes: 3 additions & 3 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
export class Store<T> {
private value: T;
private subscribers: Set<UpdateMethod>;
name: String;
name: string;

constructor(name: String, initialValue: T) {
constructor(name: string, initialValue: T) {
if (typeof initialValue === "undefined") {
throw new Error("Store must be initialized with a value");
} else if (typeof name !== "string") {
throw new Error("Store name must be of Type String");
throw new Error("Store name must be of Type string");
}
this.name = name;
this.value = initialValue;
Expand Down
9 changes: 9 additions & 0 deletions src/storeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Controller } from "@hotwired/stimulus"
import type { Store } from './store';

export interface StoreController extends Controller {
[key: string]: any;
constructor: {
stores?: Store<any>[];
};
}
43 changes: 30 additions & 13 deletions src/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
* useStore Function
*
* The useStore function simplifies the process of subscribing to and handling updates from multiple store instances
* within a Stimulus controller.
* within a Stimulus controller. It also allows direct access to store values on the controller.
*
* @param {Object} controller - The Stimulus controller instance that wants to subscribe to the stores.
* @param {Array<Store<T>>} stores - An array of store instances that the controller wants to subscribe to.
* @template T - The type of data stored in the stores.
*
* How It Works:
* 1. Iterates over the stores.
* 2. Identifies the type of each store and constructs an update method name.
* 3. Creates update methods for stores if corresponding onStoreUpdate methods exist on the controller.
* 4. Dynamically assigns update methods to the controller with specific names based on store types.
* 5. Subscribes update methods to stores to handle updates.
* 6. Enhances the controller's disconnect method to include cleanup for all subscriptions.
* 1. Retrieves the stores from the controller's constructor.
* 2. Iterates over the stores.
* 3. Identifies the type of each store and constructs an update method name.
* 4. Creates update methods for stores if corresponding onStoreUpdate methods exist on the controller.
* 5. Dynamically assigns update methods to the controller with specific names based on store types.
* 6. Subscribes update methods to stores to handle updates.
* 7. Allows direct access to store values on the controller.
* 8. Enhances the controller's disconnect method to include cleanup for all subscriptions.
*
* Usage Example:
* ```javascript
Expand All @@ -23,9 +24,10 @@
* import { myStore } from "./stores/myStore"; // Import your store class
*
* export default class extends Controller {
* static stores = [myStore];
* connect() {
* // Use the useStore function to subscribe to specific stores
* useStore(this, [myStore]);
* useStore(this);
* }
*
* // Implement specific update methods for each store
Expand All @@ -36,14 +38,15 @@
* }
* ```
*/

import type { Store } from './store';
import type { StoreController } from './storeController'; // Adjust the path as needed

export function useStore<T>(controller: any, stores: Store<T>[]) {
export function useStore<T>(controller: StoreController) {
const stores: Store<T>[] = controller.constructor.stores || [];
const unsubscribeFunctions: UnsubscribeFunction[] = [];

stores.forEach((store) => {
const storeName = store.name;
const storeName: string = store.name; // Change this line
const onStoreUpdateMethodName = `on${storeName}Update`;
const onStoreUpdateMethod: UpdateMethod = controller[onStoreUpdateMethodName];

Expand All @@ -55,11 +58,25 @@ export function useStore<T>(controller: any, stores: Store<T>[]) {

// Set the update method on the controller with a specific name based on the store type
const methodName = `update${storeName}`;
controller[methodName] = updateMethod;
(controller as StoreController)[methodName] = updateMethod;

// Subscribe to the store using the specific update method
unsubscribeFunctions.push(store.subscribe(updateMethod));
}

// Allow direct access to store value on the controller
Object.defineProperty(controller, `${storeName}Value`, {
get: () => store.get(),
enumerable: true,
configurable: true,
});

// Allow direct access to store instance on the controller
Object.defineProperty(controller, storeName, {
get: () => store,
enumerable: true,
configurable: true,
});
});

// Enhance the controller's disconnect method to include cleanup for all subscriptions
Expand Down
69 changes: 55 additions & 14 deletions test/useStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,72 @@
import { Store } from '../src/store';
import { useStore } from '../src/useStore';
import type { StoreController } from '../src/storeController';

describe('useStore', () => {
it('should subscribe to stores and call update methods on value changes', () => {
const mockController = {
let mockController: StoreController;
let testStore: Store<number>;

beforeEach(() => {
testStore = new Store('TestStore', 0);
mockController = {
constructor: {
stores: [testStore]
},
onTestStoreUpdate: jest.fn(),
disconnect: jest.fn(),
};
context: jest.fn(),
application: jest.fn(),
scope: jest.fn(),
element: jest.fn(),
// Add the other missing properties here...
} as unknown as StoreController;
});

const testStore = new Store('TestStore', 0);
useStore(mockController, [testStore]);
it('should subscribe to stores and call update methods on value changes', () => {
useStore(mockController);

testStore.set(5);
expect(mockController.onTestStoreUpdate).toHaveBeenCalledWith(5);
});

it('should clean up subscriptions when controller disconnects', () => {
const mockController = {
onTestStoreUpdate: jest.fn(),
disconnect: jest.fn(),
};
it('should allow direct access to store values on the controller', () => {
useStore(mockController);

testStore.set(10);
expect(mockController.TestStoreValue).toBe(10);
});

const testStore = new Store('TestStore', 0);
useStore(mockController, [testStore]);
it('should allow direct access to store instances on the controller', () => {
useStore(mockController);

expect(mockController.TestStore).toBe(testStore);
});

it('should clean up subscriptions when controller disconnects', () => {
useStore(mockController);

const unsubscribe = jest.spyOn(testStore, 'unsubscribe');
mockController.disconnect();
testStore.set(10);
expect(mockController.onTestStoreUpdate).not.toHaveBeenCalledWith(10);
expect(unsubscribe).toHaveBeenCalled();
});

it('should add a getter for the store value to the controller', () => {
useStore(mockController);

testStore.set(7);
expect(mockController.TestStoreValue).toBe(7);
});

it('should add a getter for the store instance to the controller', () => {
useStore(mockController);

expect(mockController.TestStore).toBe(testStore);
});

it('should add an update method to the controller', () => {
useStore(mockController);

testStore.set(8);
expect(mockController.onTestStoreUpdate).toHaveBeenCalledWith(8);
});
});

0 comments on commit 43ffbd2

Please sign in to comment.