ngx-base-state
Version:
Base classes for creation state service via Rxjs observable
707 lines (687 loc) • 24.5 kB
JavaScript
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 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