@v4fire/client
Version:
V4Fire client core library
416 lines (323 loc) • 8.24 kB
text/typescript
/*!
* 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
}
}