UNPKG

@datorama/akita

Version:

A Reactive State Management Tailored-Made for JS Applications

531 lines 17.1 kB
var _a, _b; import { __decorate, __metadata } from "tslib"; import { Subject } from 'rxjs'; import { logAction, setAction } from './actions'; import { addEntities } from './addEntities'; import { coerceArray } from './coerceArray'; import { DEFAULT_ID_KEY } from './defaultIDKey'; import { EntityActions } from './entityActions'; import { isDev } from './env'; import { getActiveEntities } from './getActiveEntities'; import { getInitialEntitiesState } from './getInitialEntitiesState'; import { hasEntity } from './hasEntity'; import { isDefined } from './isDefined'; import { isEmpty } from './isEmpty'; import { isFunction } from './isFunction'; import { isNil } from './isNil'; import { isUndefined } from './isUndefined'; import { removeEntities } from './removeEntities'; import { setEntities } from './setEntities'; import { Store } from './store'; import { transaction } from './transaction'; import { updateEntities } from './updateEntities'; /** * * Store for managing a collection of entities * * @example * * export interface WidgetsState extends EntityState<Widget> { } * * @StoreConfig({ name: 'widgets' }) * export class WidgetsStore extends EntityStore<WidgetsState> { * constructor() { * super(); * } * } * * */ export class EntityStore extends Store { constructor(initialState = {}, options = {}) { super({ ...getInitialEntitiesState(), ...initialState }, options); this.options = options; this.entityActions = new Subject(); this.entityIdChanges = new Subject(); } // @internal get selectEntityAction$() { return this.entityActions.asObservable(); } // @internal get selectEntityIdChanges$() { return this.entityIdChanges.asObservable(); } // @internal get idKey() { return this.config.idKey || this.options.idKey || DEFAULT_ID_KEY; } /** * * Replace current collection with provided collection * * @example * * this.store.set([Entity, Entity]) * this.store.set({ids: [], entities: {}}) * this.store.set({ 1: {}, 2: {}}) * */ set(entities, options = {}) { if (isNil(entities)) return; isDev() && setAction('Set Entity'); const isNativePreAdd = this.akitaPreAddEntity === EntityStore.prototype.akitaPreAddEntity; this.setHasCache(true, { restartTTL: true }); this._setState((state) => { const newState = setEntities({ state, entities, idKey: this.idKey, preAddEntity: this.akitaPreAddEntity.bind(this), isNativePreAdd, }); if (isUndefined(options.activeId) === false) { newState.active = options.activeId; } return newState; }); if (this.hasInitialUIState()) { this.handleUICreation(); } this.entityActions.next({ type: EntityActions.Set, ids: this.ids }); } /** * Add entities * * @example * * this.store.add([Entity, Entity]) * this.store.add(Entity) * this.store.add(Entity, { prepend: true }) * * this.store.add(Entity, { loading: false }) */ add(entities, options = { loading: false }) { const collection = coerceArray(entities); if (isEmpty(collection)) return; const data = addEntities({ state: this._value(), preAddEntity: this.akitaPreAddEntity.bind(this), entities: collection, idKey: this.idKey, options, }); if (data) { isDev() && setAction('Add Entity'); data.newState.loading = options.loading; this._setState(() => data.newState); if (this.hasInitialUIState()) { this.handleUICreation(true); } this.entityActions.next({ type: EntityActions.Add, ids: data.newIds }); } } update(idsOrFnOrState, newStateOrFn) { if (isUndefined(newStateOrFn)) { super.update(idsOrFnOrState); return; } let ids = []; if (isFunction(idsOrFnOrState)) { // We need to filter according the predicate function ids = this.ids.filter((id) => idsOrFnOrState(this.entities[id])); } else { // If it's nil we want all of them ids = isNil(idsOrFnOrState) ? this.ids : coerceArray(idsOrFnOrState); } if (isEmpty(ids)) return; isDev() && setAction('Update Entity', ids); let entityIdChanged; this._setState((state) => updateEntities({ idKey: this.idKey, ids, preUpdateEntity: this.akitaPreUpdateEntity.bind(this), state, newStateOrFn, producerFn: this._producerFn, onEntityIdChanges: (oldId, newId) => { entityIdChanged = { oldId, newId }; this.entityIdChanges.next({ ...entityIdChanged, pending: true }); }, })); if (entityIdChanged) { this.entityIdChanges.next({ ...entityIdChanged, pending: false }); } this.entityActions.next({ type: EntityActions.Update, ids }); } upsert(ids, newState, onCreate, options = {}) { const toArray = coerceArray(ids); const predicate = (isUpdate) => (id) => hasEntity(this.entities, id) === isUpdate; const baseClass = isFunction(onCreate) ? options.baseClass : onCreate ? onCreate.baseClass : undefined; const isClassBased = isFunction(baseClass); const updateIds = toArray.filter(predicate(true)); const newEntities = toArray.filter(predicate(false)).map((id) => { const newStateObj = typeof newState === 'function' ? newState({}) : newState; const entity = isFunction(onCreate) ? onCreate(id, newStateObj) : newStateObj; const withId = { ...entity, [this.idKey]: id }; if (isClassBased) { return new baseClass(withId); } return withId; }); // it can be any of the three types this.update(updateIds, newState); this.add(newEntities); isDev() && logAction('Upsert Entity'); } /** * * Upsert entity collection (idKey must be present) * * @example * * store.upsertMany([ { id: 1 }, { id: 2 }]); * * store.upsertMany([ { id: 1 }, { id: 2 }], { loading: true }); * store.upsertMany([ { id: 1 }, { id: 2 }], { baseClass: Todo }); * */ upsertMany(entities, options = {}) { const addedIds = []; const updatedIds = []; const updatedEntities = {}; // Update the state directly to optimize performance for (const entity of entities) { const withPreCheckHook = this.akitaPreCheckEntity(entity); const id = withPreCheckHook[this.idKey]; if (hasEntity(this.entities, id)) { const prev = this._value().entities[id]; const merged = { ...this._value().entities[id], ...withPreCheckHook }; const next = options.baseClass ? new options.baseClass(merged) : merged; const withHook = this.akitaPreUpdateEntity(prev, next); const nextId = withHook[this.idKey]; updatedEntities[nextId] = withHook; updatedIds.push(nextId); } else { const newEntity = options.baseClass ? new options.baseClass(withPreCheckHook) : withPreCheckHook; const withHook = this.akitaPreAddEntity(newEntity); const nextId = withHook[this.idKey]; addedIds.push(nextId); updatedEntities[nextId] = withHook; } } isDev() && logAction('Upsert Many'); this._setState((state) => ({ ...state, ids: addedIds.length ? [...state.ids, ...addedIds] : state.ids, entities: { ...state.entities, ...updatedEntities, }, loading: !!options.loading, })); updatedIds.length && this.entityActions.next({ type: EntityActions.Update, ids: updatedIds }); addedIds.length && this.entityActions.next({ type: EntityActions.Add, ids: addedIds }); if (addedIds.length && this.hasUIStore()) { this.handleUICreation(true); } } /** * * Replace one or more entities (except the id property) * * * @example * * this.store.replace(5, newEntity) * this.store.replace([1,2,3], newEntity) */ replace(ids, newState) { const toArray = coerceArray(ids); if (isEmpty(toArray)) return; const replaced = {}; for (const id of toArray) { replaced[id] = { ...newState, [this.idKey]: id }; } isDev() && setAction('Replace Entity', ids); this._setState((state) => ({ ...state, entities: { ...state.entities, ...replaced, }, })); } /** * * Move entity inside the collection * * * @example * * this.store.move(fromIndex, toIndex) */ move(from, to) { const ids = this.ids.slice(); ids.splice(to < 0 ? ids.length + to : to, 0, ids.splice(from, 1)[0]); isDev() && setAction('Move Entity'); this._setState((state) => ({ ...state, // Change the entities reference so that selectAll emit entities: { ...state.entities, }, ids, })); } remove(idsOrFn) { if (isEmpty(this.ids)) return; const idPassed = isDefined(idsOrFn); // null means remove all let ids = []; if (isFunction(idsOrFn)) { ids = this.ids.filter((entityId) => idsOrFn(this.entities[entityId])); } else { ids = idPassed ? coerceArray(idsOrFn) : this.ids; } if (isEmpty(ids)) return; isDev() && setAction('Remove Entity', ids); this._setState((state) => removeEntities({ state, ids })); if (!idPassed) { this.setHasCache(false); } this.handleUIRemove(ids); this.entityActions.next({ type: EntityActions.Remove, ids }); } /** * * Update the active entity * * @example * * this.store.updateActive({ completed: true }) * this.store.updateActive(active => { * return { * config: { * ..active.config, * date * } * } * }) */ updateActive(newStateOrCallback) { const ids = coerceArray(this.active); isDev() && setAction('Update Active', ids); this.update(ids, newStateOrCallback); } setActive(idOrOptions) { const active = getActiveEntities(idOrOptions, this.ids, this.active); if (active === undefined) { return; } isDev() && setAction('Set Active', active); this._setActive(active); } /** * Add active entities * * @example * * store.addActive(2); * store.addActive([3, 4, 5]); */ addActive(ids) { const toArray = coerceArray(ids); if (isEmpty(toArray)) return; const everyExist = toArray.every((id) => this.active.indexOf(id) > -1); if (everyExist) return; isDev() && setAction('Add Active', ids); this._setState((state) => { /** Protect against case that one of the items in the array exist */ const uniques = Array.from(new Set([...state.active, ...toArray])); return { ...state, active: uniques, }; }); } /** * Remove active entities * * @example * * store.removeActive(2) * store.removeActive([3, 4, 5]) */ removeActive(ids) { const toArray = coerceArray(ids); if (isEmpty(toArray)) return; const someExist = toArray.some((id) => this.active.indexOf(id) > -1); if (!someExist) return; isDev() && setAction('Remove Active', ids); this._setState((state) => { return { ...state, active: Array.isArray(state.active) ? state.active.filter((currentId) => toArray.indexOf(currentId) === -1) : null, }; }); } /** * Toggle active entities * * @example * * store.toggle(2) * store.toggle([3, 4, 5]) */ toggleActive(ids) { const toArray = coerceArray(ids); const filterExists = (remove) => (id) => this.active.includes(id) === remove; const remove = toArray.filter(filterExists(true)); const add = toArray.filter(filterExists(false)); this.removeActive(remove); this.addActive(add); isDev() && logAction('Toggle Active'); } /** * * Create sub UI store for managing Entity's UI state * * @example * * export type ProductUI = { * isLoading: boolean; * isOpen: boolean * } * * interface ProductsUIState extends EntityState<ProductUI> {} * * export class ProductsStore EntityStore<ProductsState, Product> { * ui: EntityUIStore<ProductsUIState, ProductUI>; * * constructor() { * super(); * this.createUIStore(); * } * * } */ createUIStore(initialState = {}, storeConfig = {}) { const defaults = { name: `UI/${this.storeName}`, idKey: this.idKey }; this.ui = new EntityUIStore(initialState, { ...defaults, ...storeConfig }); return this.ui; } // @internal destroy() { super.destroy(); if (this.ui instanceof EntityStore) { this.ui.destroy(); } this.entityActions.complete(); } // @internal akitaPreUpdateEntity(_, nextEntity) { return nextEntity; } // @internal akitaPreAddEntity(newEntity) { return newEntity; } // @internal akitaPreCheckEntity(newEntity) { return newEntity; } get ids() { return this._value().ids; } get entities() { return this._value().entities; } get active() { return this._value().active; } _setActive(ids) { this._setState((state) => { return { ...state, active: ids, }; }); } handleUICreation(add = false) { const ids = this.ids; const isFunc = isFunction(this.ui._akitaCreateEntityFn); let uiEntities; const createFn = (id) => { const current = this.entities[id]; const ui = isFunc ? this.ui._akitaCreateEntityFn(current) : this.ui._akitaCreateEntityFn; return { [this.idKey]: current[this.idKey], ...ui, }; }; if (add) { uiEntities = this.ids.filter((id) => isUndefined(this.ui.entities[id])).map(createFn); } else { uiEntities = ids.map(createFn); } add ? this.ui.add(uiEntities) : this.ui.set(uiEntities); } hasInitialUIState() { return this.hasUIStore() && isUndefined(this.ui._akitaCreateEntityFn) === false; } handleUIRemove(ids) { if (this.hasUIStore()) { this.ui.remove(ids); } } hasUIStore() { return this.ui instanceof EntityUIStore; } } __decorate([ transaction(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object, Object, Object]), __metadata("design:returntype", void 0) ], EntityStore.prototype, "upsert", null); __decorate([ transaction(), __metadata("design:type", Function), __metadata("design:paramtypes", [typeof (_b = typeof T !== "undefined" && T) === "function" ? _b : Object]), __metadata("design:returntype", void 0) ], EntityStore.prototype, "toggleActive", null); // @internal export class EntityUIStore extends EntityStore { constructor(initialState = {}, storeConfig = {}) { super(initialState, storeConfig); } /** * * Set the initial UI entity state. This function will determine the entity's * initial state when we call `set()` or `add()`. * * @example * * constructor() { * super(); * this.createUIStore().setInitialEntityState(entity => ({ isLoading: false, isOpen: true })); * this.createUIStore().setInitialEntityState({ isLoading: false, isOpen: true }); * } * */ setInitialEntityState(createFn) { this._akitaCreateEntityFn = createFn; } } //# sourceMappingURL=entityStore.js.map