UNPKG

@v4fire/client

Version:

V4Fire client core library

416 lines (323 loc) • 8.24 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ /** * [[include:super/i-block/modules/state/README.md]] * @packageDocumentation */ import symbolGenerator from 'core/symbol'; import iBlock from 'super/i-block/i-block'; import Friend from 'super/i-block/modules/friend'; export * from 'super/i-block/modules/state/interface'; export const $$ = symbolGenerator(); let baseSyncRouterState; /** * Class provides some helper methods to initialize a component state */ export default class State extends Friend { /** * True if needed synchronization with a router */ get needRouterSync(): boolean { return baseSyncRouterState !== this.instance.syncRouterState; } /** @see [[iBlock.instance]] */ protected get instance(): this['CTX']['instance'] { // @ts-ignore (access) // eslint-disable-next-line @typescript-eslint/unbound-method baseSyncRouterState ??= iBlock.prototype.syncRouterState; return this.ctx.instance; } /** * Retrieves object values and saves it to a state of the current component * (you can pass the complex property path using dots as separators). * * If a key from the object is matched with a component method, this method will be invoked with a value from this key * (if the value is an array, it will be spread to the method as arguments). * * The method returns an array of promises of executed operations. * * @param data * * @example * ```js * await Promise.all(this.state.set({ * someProperty: 1, * 'mods.someMod': true, * someMethod: [1, 2, 3], * anotherMethod: {} * })); * ``` */ set(data: Nullable<Dictionary>): Array<Promise<unknown>> { if (data == null) { return []; } const promises = <Array<Promise<unknown>>>[]; for (let keys = Object.keys(data), i = 0; i < keys.length; i++) { const key = keys[i], p = key.split('.'); const newVal = data[key], originalVal = this.field.get(key); if (Object.isFunction(originalVal)) { const res = originalVal.call(this.ctx, ...Array.concat([], newVal)); if (Object.isPromise(res)) { promises.push(res); } } else if (p[0] === 'mods') { let res; if (newVal == null) { res = this.ctx.removeMod(p[1]); } else { res = this.ctx.setMod(p[1], newVal); } if (Object.isPromise(res)) { promises.push(res); } } else if (!Object.fastCompare(newVal, originalVal)) { this.field.set(key, newVal); } } return promises; } /** * Saves a state of the current component to a local storage * @param [data] - additional data to save */ async saveToStorage(data?: Dictionary): Promise<boolean> { //#if runtime has core/kv-storage if (this.globalName == null) { return false; } const {ctx} = this; data = ctx.syncStorageState(data, 'remote'); this.set(ctx.syncStorageState(data)); await this.storage.set(data, '[[STORE]]'); ctx.log('state:save:storage', this, data); return true; //#endif } /** * Initializes a state of the current component from a local storage */ initFromStorage(): CanPromise<boolean> { //#if runtime has core/kv-storage if (this.globalName == null) { return false; } const key = $$.pendingLocalStore; if (this[key] != null) { return this[key]; } const {ctx} = this; const storeWatchers = {group: 'storeWatchers'}, $a = this.async.clearAll(storeWatchers); return this[key] = $a.promise(async () => { const data = await this.storage.get('[[STORE]]'); void this.lfc.execCbAtTheRightTime(() => { const stateFields = ctx.syncStorageState(data); this.set( stateFields ); const sync = $a.debounce(this.saveToStorage.bind(this), 0, { label: $$.syncLocalStorage }); if (Object.isDictionary(stateFields)) { for (let keys = Object.keys(stateFields), i = 0; i < keys.length; i++) { const key = keys[i], p = key.split('.'); if (p[0] === 'mods') { $a.on(this.localEmitter, `block.mod.*.${p[1]}.*`, sync, storeWatchers); } else { ctx.watch(key, (val, ...args) => { if (!Object.fastCompare(val, args[0])) { sync(); } }, { ...storeWatchers, deep: true }); } } } ctx.log('state:init:storage', this, stateFields); }); return true; }, { group: 'loadStore', join: true }); //#endif } /** * Resets a storage state of the current component */ async resetStorage(): Promise<boolean> { //#if runtime has core/kv-storage if (this.globalName == null) { return false; } const {ctx} = this; const stateFields = ctx.convertStateToStorageReset(); this.set( stateFields ); await this.saveToStorage(); ctx.log('state:reset:storage', this, stateFields); return true; //#endif } /** * Saves a state of the current component to a router * @param [data] - additional data to save */ async saveToRouter(data?: Dictionary): Promise<boolean> { //#if runtime has bRouter if (!this.needRouterSync) { return false; } const {ctx} = this, {router} = ctx.r, {routerStateUpdateMethod} = ctx; data = ctx.syncRouterState(data, 'remote'); this.set(ctx.syncRouterState(data)); if (!ctx.isActivated || !router) { return false; } await router[routerStateUpdateMethod](null, { query: data }); ctx.log('state:save:router', this, data); return true; //#endif } /** * Initializes a state of the current component from a router */ initFromRouter(): boolean { //#if runtime has bRouter if (!this.needRouterSync) { return false; } const {ctx} = this; const routerWatchers = {group: 'routerWatchers'}, $a = this.async.clearAll(routerWatchers); void this.lfc.execCbAtTheRightTime(async () => { const {r} = ctx; let {router} = r; if (!router) { await ($a.promisifyOnce(r, 'initRouter', { label: $$.initFromRouter })); ({router} = r); } if (!router) { return; } const route = Object.mixin({deep: true, withProto: true}, {}, r.route), stateFields = ctx.syncRouterState(Object.assign(Object.create(route), route.params, route.query)); this.set( stateFields ); if (ctx.syncRouterStoreOnInit) { const stateForRouter = ctx.syncRouterState(stateFields, 'remote'), stateKeys = Object.keys(stateForRouter); if (stateKeys.length > 0) { let query; for (let i = 0; i < stateKeys.length; i++) { const key = stateKeys[i]; const currentParams = route.params, currentQuery = route.query; const val = stateForRouter[key], currentVal = Object.get(currentParams, key) ?? Object.get(currentQuery, key); if (currentVal === undefined && val !== undefined) { query ??= {}; query[key] = val; } } if (query != null) { await router.replace(null, {query}); } } } const sync = $a.debounce(this.saveToRouter.bind(this), 0, { label: $$.syncRouter }); if (Object.isDictionary(stateFields)) { for (let keys = Object.keys(stateFields), i = 0; i < keys.length; i++) { const key = keys[i], p = key.split('.'); if (p[0] === 'mods') { $a.on(this.localEmitter, `block.mod.*.${p[1]}.*`, sync, routerWatchers); } else { ctx.watch(key, (val, ...args) => { if (!Object.fastCompare(val, args[0])) { sync(); } }, { ...routerWatchers, deep: true }); } } } ctx.log('state:init:router', this, stateFields); }, { label: $$.initFromRouter }); return true; //#endif } /** * Resets a router state of the current component */ resetRouter(): boolean { //#if runtime has bRouter const {ctx} = this, {router} = ctx.r; const stateFields = ctx.convertStateToRouterReset(); this.set( stateFields ); if (!ctx.isActivated || !router) { return false; } ctx.log('state:reset:router', this, stateFields); return true; //#endif } }