/** * DataStore class to manage application state * Designed to for Best with Webflow Webapps * You can add data to the store, update data, delete data, and subscribe to changes * Once data is updated, all subscribers are notified */ export class DataStore { private data: any; private listeners: Map> = new Map(); private cacheAll: boolean; private cachePrefix: string; constructor( initialData: Record, options: { caching?: boolean; prefix?: string } = {} ) { this.cacheAll = options.caching || false; this.cachePrefix = options.prefix || "th_js-cache"; this.data = new Proxy(initialData, { get: (target, property, receiver) => Reflect.get(target, property, receiver), set: (target, property, value) => { let oldValue = target[String(property)]; if (oldValue !== value) { target[String(property)] = value; this.notifyListeners(String(property)); this.cacheData(); // Cache all keys if caching is enabled } return true; }, }); this.loadFromCache(); } /** * Get the prefixed key for the cache stored in localStorage * @param key * @returns * @private * @memberof DataStore * @method getPrefixedKey * @example * getPrefixedKey('key') */ private getPrefixedKey(key: string): string { return `${this.cachePrefix}-${key}`; } /** * Load data from cache * @private * @memberof DataStore * @method loadFromCache * @example * loadFromCache() */ private loadFromCache() { if (this.cacheAll) { Object.keys(localStorage).forEach((key) => { if (key.startsWith(this.cachePrefix)) { const item = localStorage.getItem(key); if (item && item !== "undefined") { try { const originalKey = key.replace(`${this.cachePrefix}-`, ""); this.data[originalKey] = JSON.parse(item); } catch (error) { console.error( "Error parsing JSON from localStorage for key:", key, error ); } } } }); } } /** * Cache all data in the store. If caching is enabled, this method is called after every updatea and it caches all keys in the store and local storage * @private * @memberof DataStore * @method cacheData * @example * cacheData() */ private cacheData() { if (this.cacheAll) { Object.keys(this.data).forEach((key) => { localStorage.setItem( this.getPrefixedKey(key), JSON.stringify(this.data[key]) ); }); } } /** * Resolve the path of a property in the data store * @param path * @returns * @private * @memberof DataStore * @method resolvePath * @example * resolvePath('users.0.name') */ private resolvePath(path) { const segments = path.split("."); let current = this.data; for (let i = 0; i < segments.length - 1; i++) { if (!(segments[i] in current)) { current[segments[i]] = {}; } current = current[segments[i]]; } return { parent: current, key: segments[segments.length - 1] }; } /** * Get a value from the data store * @param path * @returns * @memberof DataStore * @method get * @example * get('users.0.name') */ get(path: string): any { const result = this.resolvePath(path); return result.parent[result.key]; } /** * Add a value to the data store * @param path * @param value * @memberof DataStore * @method add * @example * add('users', { name: 'John' }) * This will add a new user to the users array */ add(path: string, value: any) { const { parent, key } = this.resolvePath(path); if (!Array.isArray(parent[key])) { parent[key] = []; } parent[key].push(value); this.notifyListeners(path); } /** * Update a value in the data store * @param path * @param value * @memberof DataStore * @method update * @example * update('users.0.name', 'Jane') * This will update the name of the first user in the users array */ update(path: string, value: any) { const { parent, key } = this.resolvePath(path); parent[key] = value; this.notifyListeners(path); } /** * Delete a value from the data store * @param path * @memberof DataStore * @method delete * @example * delete('users.0') * This will delete the first user from the users array */ delete(path: string) { const { parent, key } = this.resolvePath(path); if (Array.isArray(parent)) { const index = parseInt(key, 10); if (!isNaN(index)) { parent.splice(index, 1); } } else if (Array.isArray(parent[key])) { parent[key] = []; } else { delete parent[key]; } this.notifyListeners(path); } /** * Notify all listeners for a property that has been updated * @param property * @private * @memberof DataStore * @method notifyListeners * @example * notifyListeners('users') */ notifyListeners(property: string) { const segments = property.split("."); let currentProperty = ""; for (let i = 0; i < segments.length; i++) { currentProperty += segments[i]; if (this.listeners.has(currentProperty)) { let propertyListeners = this.listeners.get(currentProperty); propertyListeners.forEach((listener: Function) => listener(this.get(currentProperty)) ); } currentProperty += "."; } } /** * Subscribe to changes in a property in the data store * @param property * @param listener * @returns * @memberof DataStore * @method subscribe * @example * const unsubscribe = subscribe('users', (users) => { * console.log('Users have been updated:', users); * }); * This will log a message whenever the users property is updated */ subscribe(property: string, listener: Function): () => void { const segments = property.split("."); let currentProperty = ""; for (let i = 0; i < segments.length; i++) { currentProperty += segments[i]; if (!this.listeners.has(currentProperty)) { this.listeners.set(currentProperty, new Set()); } currentProperty += "."; } let propertyListeners = this.listeners.get(property); propertyListeners.add(listener); listener(this.get(property)); // Immediately invoke listener with initial value return () => propertyListeners.delete(listener); } get state(): Record { return this.data; } }