UNPKG

ngx-base-state

Version:

Base classes for creation state service via Rxjs observable

707 lines (687 loc) 24.5 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, Inject, Optional, NgModule } from '@angular/core'; import { __decorate } from 'tslib'; import { BehaviorSubject, map, shareReplay, ReplaySubject } from 'rxjs'; class NgxBaseStateDevtoolsConfig { constructor(params) { this.isEnabled = false; Object.assign(this, params); } } const NGX_BASE_STATE_DEVTOOLS_CONFIG = new InjectionToken('NGX_BASE_STATE_DEVTOOLS_CONFIG', { providedIn: 'root', factory: () => new NgxBaseStateDevtoolsConfig({ isEnabled: false }) }); const ɵNGX_STATE_DECORATOR_METADATA_FIELD = 'ɵNgxState'; const tryDoActionMethodName = 'tryDoAction'; const actionLikeInvokedMethodName = '_onActionLikeInvoked'; const actionLikeInvokeEndMethodName = '_onActionLikeInvokeEnd'; function patchedActionFunction(actionName, actionFunction) { return function innerFunction(...args) { return this[tryDoActionMethodName](actionName, () => { this[actionLikeInvokedMethodName](actionName); const originalMethodResult = actionFunction.apply(this, args); this[actionLikeInvokeEndMethodName](); return originalMethodResult; }); }; } const forbiddenMethodNamesToPatch = [ 'constructor', 'ngOnDestroy' ]; function NgxState() { return function InnerFunction(targetClass) { const prototype = targetClass.prototype; markTargetClassWithMetadata(prototype); Object.getOwnPropertyNames(prototype).forEach((fieldName) => { const isFieldNameForbidden = forbiddenMethodNamesToPatch.includes(fieldName); if (!isFieldNameForbidden && isFieldHaveTypeFunction(prototype, fieldName)) { markMethodOfStateAsAction(prototype, fieldName, prototype[fieldName]); } }); }; } function markTargetClassWithMetadata(stateClass) { stateClass[ɵNGX_STATE_DECORATOR_METADATA_FIELD] = true; } function markMethodOfStateAsAction(stateClass, fieldName, originalMethod) { stateClass[fieldName] = patchedActionFunction(fieldName, originalMethod); } function isFieldHaveTypeFunction(stateClass, fieldName) { var _a, _b; return (!((_a = Object.getOwnPropertyDescriptor(stateClass, fieldName)) === null || _a === void 0 ? void 0 : _a.get) && !((_b = Object.getOwnPropertyDescriptor(stateClass, fieldName)) === null || _b === void 0 ? void 0 : _b.set) && (typeof stateClass[fieldName] === 'function')); } function ɵAction(targetClass, fieldName, descriptor) { const originalMethod = descriptor.value; descriptor.value = patchedActionFunction(fieldName, originalMethod); } var ɵMetadataKeyEnum; (function (ɵMetadataKeyEnum) { ɵMetadataKeyEnum["DevtoolsEnabled"] = "__NGX_BASE_STATE_DEVTOOLS_ENABLED"; ɵMetadataKeyEnum["MetadataOperation"] = "__NGX_BASE_STATE_METADATA_OPERATION"; })(ɵMetadataKeyEnum || (ɵMetadataKeyEnum = {})); var ɵMetadataOperationTypeEnum; (function (ɵMetadataOperationTypeEnum) { ɵMetadataOperationTypeEnum[ɵMetadataOperationTypeEnum["Init"] = 1] = "Init"; ɵMetadataOperationTypeEnum[ɵMetadataOperationTypeEnum["Update"] = 2] = "Update"; ɵMetadataOperationTypeEnum[ɵMetadataOperationTypeEnum["Destroy"] = 3] = "Destroy"; })(ɵMetadataOperationTypeEnum || (ɵMetadataOperationTypeEnum = {})); function isObject(data) { return ((typeof data === 'object') && !Array.isArray(data) && (data !== null)); } class ɵMetadataStorage { get window() { return window; } get(key) { return this.window[key]; } set(key, data) { this.window[key] = data; } } ɵMetadataStorage.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ɵMetadataStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); ɵMetadataStorage.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ɵMetadataStorage, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ɵMetadataStorage, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class ɵStackTrace { static capture() { // FIXME: Refactor and investigate const stackTrace = new Error().stack; const rawMethods = stackTrace.split('at '); const methods = rawMethods .map((rawMethod) => rawMethod.slice(0, rawMethod.indexOf(' ('))) .filter((method) => !method.startsWith('http')); return methods; } } const INITIAL_DATA = new InjectionToken('__NGX_BASE_STATE_INITIAL_DATA'); const INITIAL_CONFIG = new InjectionToken('__NGX_BASE_STATE_INITIAL_CONFIG'); const CLASS_ID_FIELD = '_ɵID'; /** * @class * @classdes This is a base class that used for creating hight level state classes */ class BaseState { constructor( /** Initial data should be passed via the `super` method call. */ initialData = null, /** Initial config should be passed via the `super` method call. */ initialConfig = null) { this.initialData = initialData; this.initialConfig = initialConfig; this._devtoolsConfig = inject(NGX_BASE_STATE_DEVTOOLS_CONFIG); this._metadataStorage = inject(ɵMetadataStorage); this._currentlyInvokedAction = null; this._stackTraceOfCurrentlyInvokedAction = null; this.validateNullableDataType(this.initialData); this._data$ = new BehaviorSubject(this.initialData); this.initClassIdIfAbsent(); this.showConsoleWarningIfClassHaveNotDecorator(); this.init(); } /** * Get `Observable` with state data. * @public * @return {Generic} Observable with the state data. */ get data$() { return this._data$.asObservable(); } /** * Get state data. * @public * @return {Generic} State data. */ get data() { return this._data$.value; } /** * Base implementation of `ngOnDestroy`. * Don't forget to call `super.ngOnDestroy` in case of override. * @public */ ngOnDestroy() { this.emitMetadataOperation(ɵMetadataOperationTypeEnum.Destroy); this._data$.complete(); } /** * Set new value to state * @public * @param {Generic} value - the value that should be set to update `BehaviorSubject`. */ set(value) { this.setNewValue(value); } /** * Clear state value. (Will be set `null`) * @public */ clear() { this.setNewValue(null); } /** * Restore initial data from constructor. * @public */ restoreInitialData() { this.setNewValue(this.initialData); } /** * Method for set data functionality. It may be expanded. * The idea is to process the creation of new instances of complex structures. * @protected * @param {Generic | null} value - the value that should be set to update `BehaviorSubject`. */ setNewValue(value) { this.validateNullableDataType(value); this._data$.next(value); this.emitMetadataOperation(ɵMetadataOperationTypeEnum.Update); } /** * Method used for try to work out any method * @protected * @param {string} actionName - Action you try to fire. Used to show in Error text when something went wrong. * @param {Function} actionFunc - Callback with logic. When something goes wrong - Error will be created. * @return {Generic} result of the callback call. */ tryDoAction(actionName, actionFunc) { try { return actionFunc(); } catch (error) { this.catchError(error, actionName); return undefined; } } /** * Method that processed error for user friendly error messages * @protected * @param {Error} error - Error. * @param {string} actionName - Name of the action where error happened. */ catchError(error, actionName) { throw new Error(`\n${this.constructor.name} [${actionName}]: ${error.message}`); } init() { this.emitMetadataOperation(ɵMetadataOperationTypeEnum.Init); } // Using by decorators _onActionLikeInvoked(actionName) { if (this._devtoolsConfig.isEnabled && !this._currentlyInvokedAction) { this._currentlyInvokedAction = actionName; this._stackTraceOfCurrentlyInvokedAction = ɵStackTrace.capture(); } } // Using by decorators _onActionLikeInvokeEnd() { if (this._devtoolsConfig.isEnabled) { this._currentlyInvokedAction = null; this._stackTraceOfCurrentlyInvokedAction = null; } } showConsoleWarningIfClassHaveNotDecorator() { if (this._devtoolsConfig.isEnabled && !this[ɵNGX_STATE_DECORATOR_METADATA_FIELD]) { console.warn(`${this.constructor.name} class is missed @NgxState() decorator. ` + `Some features of DevTools will work incorrectly!`); } } initClassIdIfAbsent() { if (!this.constructor[CLASS_ID_FIELD]) { this.constructor[CLASS_ID_FIELD] = Math.random(); } } validateNullableDataType(data) { var _a; if (data !== null) { (_a = this.validateDataType) === null || _a === void 0 ? void 0 : _a.call(this, data); } } /** * Emits information about state changes into `ReplaySubject` at the `window`. * Extension use this information to visually represent current state and history of states changes. * @private */ emitMetadataOperation(type) { var _a; if (this._devtoolsConfig.isEnabled) { const operationEmitter$ = this._metadataStorage .get(ɵMetadataKeyEnum.MetadataOperation); operationEmitter$.next({ type, classId: this.constructor[CLASS_ID_FIELD], className: this.constructor.name, classContext: (_a = this.initialConfig) === null || _a === void 0 ? void 0 : _a.context, actionName: this._currentlyInvokedAction, date: new Date().toJSON(), data: this.data, stackTrace: this._stackTraceOfCurrentlyInvokedAction }); } } } BaseState.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: BaseState, deps: [{ token: INITIAL_DATA, optional: true }, { token: INITIAL_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); BaseState.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: BaseState }); __decorate([ ɵAction ], BaseState.prototype, "set", null); __decorate([ ɵAction ], BaseState.prototype, "clear", null); __decorate([ ɵAction ], BaseState.prototype, "restoreInitialData", null); __decorate([ ɵAction ], BaseState.prototype, "init", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: BaseState, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [INITIAL_DATA] }, { type: Optional }] }, { type: undefined, decorators: [{ type: Inject, args: [INITIAL_CONFIG] }, { type: Optional }] }]; }, propDecorators: { set: [], clear: [], restoreInitialData: [], init: [] } }); /** * @class * @abstract * @classdes Array state class. Implementing base array functionality. */ class ArrayState extends BaseState { /** * Return item by quired index. * @public * @param {Number} index - Quired index * @deprecated use `this.data[index]` instead * @return {Generic} quired item. */ getByIndex(index) { const items = this.data; return items[index]; } /** * Unshift item to array in state. * @public * @param {Generic} item - Item needs to unshift. */ unshiftItem(item) { const items = this.data; items.unshift(item); this.setNewValue(items); } /** * Shift array in state. * @public */ shift() { const items = this.data; items.shift(); this.setNewValue(items); } /** * Pop array in state. * @public */ pop() { const items = this.data; items.pop(); this.setNewValue(items); } /** * Concat current state with another array. * @param {T[]} array - Another array to concat with the current state. * @public */ concatWith(array) { const items = this.data; const newItems = items.concat(array); this.setNewValue(newItems); } /** * Push item to array in state. * @public * @param {Generic} item - Item needs to push */ pushItem(item) { const items = this.data; items.push(item); this.setNewValue(items); } /** * Insert item in array by index. * @public * @param {number} index - Index where to insert new item. * @param {Generic} item - Item need to insert. */ insertItemByIndex(index, item) { const items = this.data; items.splice(index, 0, item); this.setNewValue(items); } /** * Remove item in array by item identify param (using `compareItems` method). * @public * @param {Generic} itemId - Id of item you want to remove. */ removeItem(item) { const index = this.data.findIndex((_item) => this.compareItems(item, _item)); return this.removeItemByIndex(index); } /** * Remove item in array by item id (using `getItemId` method). * @public * @param {Generic} itemId - Id of item you want to remove. */ removeItemById(itemId) { const index = this.data.findIndex((_item) => itemId === this.getItemId(_item)); return this.removeItemByIndex(index); } /** * Remove item in array by index. * @public * @param {number} index - Index of item you want to remove. */ removeItemByIndex(index) { const items = this.data; const removedItem = this.data[index]; items.splice(index, 1); this.setNewValue(items); return removedItem; } /** * Update item in array by item identify param (using `compareItems` method). * @public * @param {Generic} itemToUpdate - item that will be update. */ updateItem(itemToUpdate) { const items = this.data; const newItemToUpdate = Object.assign({}, itemToUpdate); const itemIndex = items.findIndex((_currentItem) => this.compareItems(_currentItem, newItemToUpdate)); items[itemIndex] = newItemToUpdate; this.setNewValue(items); } /** * Update item in array by index. * @public * @param {Generic} itemToUpdate - item that will be update. * @param {Generic} index - index of item that need to update. */ updateItemByIndex(itemToUpdate, index) { const items = this.data; items[index] = itemToUpdate; this.setNewValue(items); } setNewValue(value) { if (value) { this.validateDataType(value); super.setNewValue([...value]); } else { super.setNewValue(null); } } catchError(error, actionName) { if (error instanceof TypeError) { throw new Error(`\n${this.constructor.name} [${actionName}]: ` + `Firstly set Array.\n\n${error.message}`); } super.catchError(error, actionName); } /** * Must return identify param of item. * Method must be filled in child classes. * Used for compare two any items. * @protected * @param {Generic} item - item of your state. * @return {any} identify param of item. */ getItemId(item) { return item; } validateDataType(data) { if (!Array.isArray(data)) { throw new Error(`${this.constructor.name}: Expected data in Array format!`); } } /** * Compare two items via `getItemId` * @private * @param {Generic} itemToUpdate - item that will be update. * @return {boolean} result of comparing two items via `getItemId`. */ compareItems(firstItem, secondItem) { return (this.getItemId(firstItem) === this.getItemId(secondItem)); } } __decorate([ ɵAction ], ArrayState.prototype, "unshiftItem", null); __decorate([ ɵAction ], ArrayState.prototype, "shift", null); __decorate([ ɵAction ], ArrayState.prototype, "pop", null); __decorate([ ɵAction ], ArrayState.prototype, "concatWith", null); __decorate([ ɵAction ], ArrayState.prototype, "pushItem", null); __decorate([ ɵAction ], ArrayState.prototype, "insertItemByIndex", null); __decorate([ ɵAction ], ArrayState.prototype, "removeItem", null); __decorate([ ɵAction ], ArrayState.prototype, "removeItemById", null); __decorate([ ɵAction ], ArrayState.prototype, "removeItemByIndex", null); __decorate([ ɵAction ], ArrayState.prototype, "updateItem", null); __decorate([ ɵAction ], ArrayState.prototype, "updateItemByIndex", null); /** * @class * @classdes Object state class. Used for save state with Object type. */ class ObjectState extends BaseState { /** * Updates state by merging new partial object with the existing one. * @public * @param {T | null} value - the value that should be set to update `BehaviorSubject`. */ updateWithPartial(value) { this.set(Object.assign(Object.assign({}, this.data), value)); } setNewValue(value) { if (value) { super.setNewValue(Object.assign({}, value)); } else { super.setNewValue(null); } } catchError(error, actionName) { if (error instanceof TypeError) { throw new Error(`\n${this.constructor.name} [${actionName}]:` + `Firstly set Object.\n\n${error.message}`); } super.catchError(error, actionName); } validateDataType(data) { if (!isObject(data)) { throw new Error(`${this.constructor.name}: Expected data in Object format!`); } } } __decorate([ ɵAction ], ObjectState.prototype, "updateWithPartial", null); /** * @class * @classdes Primitive state class. Used for save state with Primitive type (Like boolean, number or string). */ class PrimitiveState extends BaseState { validateDataType(data) { const isDataValid = ((typeof data === 'number') || (typeof data === 'string') || (typeof data === 'boolean') || (typeof data === 'bigint') || (typeof data === 'symbol')); if (!isDataValid) { throw new Error(`${this.constructor.name}: Expected data in Primitive format!`); } } } /** * @class * @classdes Record state class. Used for save state with Record type. */ class RecordState extends BaseState { constructor() { super(...arguments); /** * Get `Observable` with all `keys` of your `Record` object state. * @public */ this.keys$ = this.data$ .pipe(map(() => this.keys), shareReplay(1)); /** * Get `Observable` with all `values` of your `Record` object state. * @public */ this.values$ = this.data$ .pipe(map(() => this.values), shareReplay(1)); } /** * Get all `keys` of your `Record` object state. * @public */ get keys() { return (this.data) ? Object.keys(this.data) : []; } /** * Get all `values` of your `Record` object state. * @public */ get values() { return (this.data) ? Object.values(this.data) : []; } /** * Set item to object in state. * @public * @param {TKey} key - Key to set into. * @param {TValue} value - Value to set within `key`. */ setItem(key, value) { this.data[key] = value; this.setNewValue(this.data); } /** * Remove item from object in state. * @public * @param {TKey} key - Key to remove item within. */ removeItem(key) { delete this.data[key]; this.setNewValue(this.data); } /** * Remove all items from object in state. * @public */ removeAllItems() { this.set({}); } setNewValue(value) { if (value) { super.setNewValue(Object.assign({}, value)); } else { super.setNewValue(null); } } catchError(error, actionName) { if (error instanceof TypeError) { throw new Error(`\n${this.constructor.name} [${actionName}]: ` + `Firstly set Object [Record].\n\n${error.message}`); } super.catchError(error, actionName); } validateDataType(data) { if (!isObject(data)) { throw new Error(`${this.constructor.name}: Expected data in Object (Record) format!`); } } } __decorate([ ɵAction ], RecordState.prototype, "setItem", null); __decorate([ ɵAction ], RecordState.prototype, "removeItem", null); __decorate([ ɵAction ], RecordState.prototype, "removeAllItems", null); class NgxBaseStateDevtoolsModule { constructor(config, metadataStorage) { this.config = config; this.metadataStorage = metadataStorage; if (this.config.isEnabled) { this.metadataStorage.set(ɵMetadataKeyEnum.DevtoolsEnabled, true); this.metadataStorage.set(ɵMetadataKeyEnum.MetadataOperation, new ReplaySubject()); } } static forRoot(options) { return { ngModule: NgxBaseStateDevtoolsModule, providers: [ { provide: NGX_BASE_STATE_DEVTOOLS_CONFIG, useValue: options } ] }; } } NgxBaseStateDevtoolsModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: NgxBaseStateDevtoolsModule, deps: [{ token: NGX_BASE_STATE_DEVTOOLS_CONFIG }, { token: ɵMetadataStorage }], target: i0.ɵɵFactoryTarget.NgModule }); NgxBaseStateDevtoolsModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.2.4", ngImport: i0, type: NgxBaseStateDevtoolsModule }); NgxBaseStateDevtoolsModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: NgxBaseStateDevtoolsModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: NgxBaseStateDevtoolsModule, decorators: [{ type: NgModule, args: [{}] }], ctorParameters: function () { return [{ type: NgxBaseStateDevtoolsConfig, decorators: [{ type: Inject, args: [NGX_BASE_STATE_DEVTOOLS_CONFIG] }] }, { type: ɵMetadataStorage }]; } }); /** * Generated bundle index. Do not edit. */ export { ArrayState, BaseState, NGX_BASE_STATE_DEVTOOLS_CONFIG, NgxBaseStateDevtoolsConfig, NgxBaseStateDevtoolsModule, NgxState, ObjectState, PrimitiveState, RecordState }; //# sourceMappingURL=ngx-base-state.mjs.map