From 6c30655356382fc5207bda38976340fa24410d61 Mon Sep 17 00:00:00 2001 From: Sebastian Hoffmann Date: Wed, 28 Feb 2024 07:17:05 +0100 Subject: [PATCH 1/7] build: increase demo budgets --- apps/demo/project.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/demo/project.json b/apps/demo/project.json index 483a3605..36b34013 100644 --- a/apps/demo/project.json +++ b/apps/demo/project.json @@ -41,8 +41,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1mb", + "maximumError": "1.5mb" }, { "type": "anyComponentStyle", @@ -94,4 +94,4 @@ } } } -} \ No newline at end of file +} From 690c88aa0c7d7e38b72118e6184434ce3953ee03 Mon Sep 17 00:00:00 2001 From: Sebastian Hoffmann Date: Wed, 28 Feb 2024 07:27:17 +0100 Subject: [PATCH 2/7] lint: update lib dependencies according to dep check --- libs/ngrx-toolkit/package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/ngrx-toolkit/package.json b/libs/ngrx-toolkit/package.json index bfedf6ee..7b160be7 100644 --- a/libs/ngrx-toolkit/package.json +++ b/libs/ngrx-toolkit/package.json @@ -9,10 +9,9 @@ "peerDependencies": { "@angular/common": "^17.0.0", "@angular/core": "^17.0.0", - "@ngrx/signals": "^17.0.0" - }, - "dependencies": { - "tslib": "^2.3.0" + "@ngrx/signals": "^17.0.0", + "@ngrx/store": "^17.0.0", + "rxjs": "^6.5.3 || ^7.5.0" }, "sideEffects": false } From e5efa94c9eb135e7b0bea2ed21c21b901c6c826a Mon Sep 17 00:00:00 2001 From: Sebastian Hoffmann Date: Wed, 28 Feb 2024 07:28:11 +0100 Subject: [PATCH 3/7] refactor: unify empty type usage - remove unused type - rename empty type --- libs/ngrx-toolkit/src/lib/shared/empty.ts | 2 +- libs/ngrx-toolkit/src/lib/with-call-state.ts | 85 +-- .../ngrx-toolkit/src/lib/with-data-service.ts | 683 ++++++++++-------- libs/ngrx-toolkit/src/lib/with-devtools.ts | 3 +- .../ngrx-toolkit/src/lib/with-storage-sync.ts | 6 +- libs/ngrx-toolkit/src/lib/with-undo-redo.ts | 326 +++++---- 6 files changed, 609 insertions(+), 496 deletions(-) diff --git a/libs/ngrx-toolkit/src/lib/shared/empty.ts b/libs/ngrx-toolkit/src/lib/shared/empty.ts index daeb13ad..28a0fa01 100644 --- a/libs/ngrx-toolkit/src/lib/shared/empty.ts +++ b/libs/ngrx-toolkit/src/lib/shared/empty.ts @@ -1,2 +1,2 @@ // eslint-disable-next-line @typescript-eslint/ban-types -export type Emtpy = {}; \ No newline at end of file +export type Empty = {}; diff --git a/libs/ngrx-toolkit/src/lib/with-call-state.ts b/libs/ngrx-toolkit/src/lib/with-call-state.ts index f52410d2..12520d21 100644 --- a/libs/ngrx-toolkit/src/lib/with-call-state.ts +++ b/libs/ngrx-toolkit/src/lib/with-call-state.ts @@ -5,7 +5,7 @@ import { withComputed, withState, } from '@ngrx/signals'; -import { Emtpy } from './shared/empty'; +import { Empty } from './shared/empty'; export type CallState = 'init' | 'loading' | 'loaded' | { error: string }; @@ -14,27 +14,27 @@ export type NamedCallStateSlice = { }; export type CallStateSlice = { - callState: CallState -} + callState: CallState; +}; export type NamedCallStateSignals = { [K in Prop as `${K}Loading`]: Signal; } & { - [K in Prop as `${K}Loaded`]: Signal; - } & { - [K in Prop as `${K}Error`]: Signal; - } + [K in Prop as `${K}Loaded`]: Signal; +} & { + [K in Prop as `${K}Error`]: Signal; +}; export type CallStateSignals = { loading: Signal; loaded: Signal; - error: Signal -} + error: Signal; +}; export function getCallStateKeys(config?: { collection?: string }) { const prop = config?.collection; return { - callStateKey: prop ? `${config.collection}CallState` : 'callState', + callStateKey: prop ? `${config.collection}CallState` : 'callState', loadingKey: prop ? `${config.collection}Loading` : 'loading', loadedKey: prop ? `${config.collection}Loaded` : 'loaded', errorKey: prop ? `${config.collection}Error` : 'error', @@ -44,19 +44,19 @@ export function getCallStateKeys(config?: { collection?: string }) { export function withCallState(config: { collection: Collection; }): SignalStoreFeature< - { state: Emtpy, signals: Emtpy, methods: Emtpy }, + { state: Empty; signals: Empty; methods: Empty }, { - state: NamedCallStateSlice, - signals: NamedCallStateSignals, - methods: Emtpy + state: NamedCallStateSlice; + signals: NamedCallStateSignals; + methods: Empty; } >; export function withCallState(): SignalStoreFeature< - { state: Emtpy, signals: Emtpy, methods: Emtpy }, + { state: Empty; signals: Empty; methods: Empty }, { - state: CallStateSlice, - signals: CallStateSignals, - methods: Emtpy + state: CallStateSlice; + signals: CallStateSignals; + methods: Empty; } >; export function withCallState(config?: { @@ -68,7 +68,6 @@ export function withCallState(config?: { return signalStoreFeature( withState({ [callStateKey]: 'init' }), withComputed((state: Record>) => { - const callState = state[callStateKey] as Signal; return { @@ -77,8 +76,8 @@ export function withCallState(config?: { [errorKey]: computed(() => { const v = callState(); return typeof v === 'object' ? v.error : null; - }) - } + }), + }; }) ); } @@ -96,38 +95,32 @@ export function setLoading( export function setLoaded( prop?: Prop ): NamedCallStateSlice | CallStateSlice { - if (prop) { return { [`${prop}CallState`]: 'loaded' } as NamedCallStateSlice; - } - else { + } else { return { callState: 'loaded' }; - } } export function setError( error: unknown, - prop?: Prop, - ): NamedCallStateSlice | CallStateSlice { - - let errorMessage = ''; - - if (!error) { - errorMessage = ''; - } - else if (typeof error === 'object' && 'message' in error) { - errorMessage = String(error.message); - } - else { - errorMessage = String(error); - } - + prop?: Prop +): NamedCallStateSlice | CallStateSlice { + let errorMessage = ''; + + if (!error) { + errorMessage = ''; + } else if (typeof error === 'object' && 'message' in error) { + errorMessage = String(error.message); + } else { + errorMessage = String(error); + } - if (prop) { - return { [`${prop}CallState`]: { error: errorMessage } } as NamedCallStateSlice; - } - else { - return { callState: { error: errorMessage } }; - } + if (prop) { + return { + [`${prop}CallState`]: { error: errorMessage }, + } as NamedCallStateSlice; + } else { + return { callState: { error: errorMessage } }; + } } diff --git a/libs/ngrx-toolkit/src/lib/with-data-service.ts b/libs/ngrx-toolkit/src/lib/with-data-service.ts index 2a2baadd..877f41a7 100644 --- a/libs/ngrx-toolkit/src/lib/with-data-service.ts +++ b/libs/ngrx-toolkit/src/lib/with-data-service.ts @@ -1,312 +1,411 @@ -import { ProviderToken, Signal, computed, inject } from "@angular/core"; -import { SignalStoreFeature, patchState, signalStoreFeature, withComputed, withMethods, withState } from "@ngrx/signals"; -import { CallState, getCallStateKeys, setError, setLoaded, setLoading } from "./with-call-state"; -import { setAllEntities, EntityId, addEntity, updateEntity, removeEntity } from "@ngrx/signals/entities"; -import { EntityState, NamedEntitySignals } from "@ngrx/signals/entities/src/models"; -import { StateSignal } from "@ngrx/signals/src/state-signal"; -import { Emtpy } from "./shared/empty"; +import { ProviderToken, Signal, computed, inject } from '@angular/core'; +import { + SignalStoreFeature, + patchState, + signalStoreFeature, + withComputed, + withMethods, + withState, +} from '@ngrx/signals'; +import { + CallState, + getCallStateKeys, + setError, + setLoaded, + setLoading, +} from './with-call-state'; +import { + setAllEntities, + EntityId, + addEntity, + updateEntity, + removeEntity, +} from '@ngrx/signals/entities'; +import { + EntityState, + NamedEntitySignals, +} from '@ngrx/signals/entities/src/models'; +import { StateSignal } from '@ngrx/signals/src/state-signal'; +import { Empty } from './shared/empty'; export type Filter = Record; export type Entity = { id: EntityId }; export interface DataService { - load(filter: F): Promise; - loadById(id: EntityId): Promise; - create(entity: E): Promise; - update(entity: E): Promise; - updateAll(entity: E[]): Promise; - delete(entity: E): Promise; + load(filter: F): Promise; + loadById(id: EntityId): Promise; + create(entity: E): Promise; + update(entity: E): Promise; + updateAll(entity: E[]): Promise; + delete(entity: E): Promise; } export function capitalize(str: string): string { - return str ? str[0].toUpperCase() + str.substring(1) : str; + return str ? str[0].toUpperCase() + str.substring(1) : str; } export function getDataServiceKeys(options: { collection?: string }) { - const filterKey = options.collection ? `${options.collection}Filter` : 'filter'; - const selectedIdsKey = options.collection ? `selected${capitalize(options.collection)}Ids` : 'selectedIds'; - const selectedEntitiesKey = options.collection ? `selected${capitalize(options.collection)}Entities` : 'selectedEntities'; - - const updateFilterKey = options.collection ? `update${capitalize(options.collection)}Filter` : 'updateFilter'; - const updateSelectedKey = options.collection ? `updateSelected${capitalize(options.collection)}Entities` : 'updateSelected'; - const loadKey = options.collection ? `load${capitalize(options.collection)}Entities` : 'load'; - - const currentKey = options.collection ? `current${capitalize(options.collection)}` : 'current'; - const loadByIdKey = options.collection ? `load${capitalize(options.collection)}ById` : 'loadById'; - const setCurrentKey = options.collection ? `setCurrent${capitalize(options.collection)}` : 'setCurrent'; - const createKey = options.collection ? `create${capitalize(options.collection)}` : 'create'; - const updateKey = options.collection ? `update${capitalize(options.collection)}` : 'update'; - const updateAllKey = options.collection ? `updateAll${capitalize(options.collection)}` : 'updateAll'; - const deleteKey = options.collection ? `delete${capitalize(options.collection)}` : 'delete'; - - // TODO: Take these from @ngrx/signals/entities, when they are exported - const entitiesKey = options.collection ? `${options.collection}Entities` : 'entities'; - const entityMapKey = options.collection ? `${options.collection}EntityMap` : 'entityMap'; - const idsKey = options.collection ? `${options.collection}Ids` : 'ids'; - - return { - filterKey, - selectedIdsKey, - selectedEntitiesKey, - updateFilterKey, - updateSelectedKey, - loadKey, - entitiesKey, - entityMapKey, - idsKey, - - currentKey, - loadByIdKey, - setCurrentKey, - createKey, - updateKey, - updateAllKey, - deleteKey - }; + const filterKey = options.collection + ? `${options.collection}Filter` + : 'filter'; + const selectedIdsKey = options.collection + ? `selected${capitalize(options.collection)}Ids` + : 'selectedIds'; + const selectedEntitiesKey = options.collection + ? `selected${capitalize(options.collection)}Entities` + : 'selectedEntities'; + + const updateFilterKey = options.collection + ? `update${capitalize(options.collection)}Filter` + : 'updateFilter'; + const updateSelectedKey = options.collection + ? `updateSelected${capitalize(options.collection)}Entities` + : 'updateSelected'; + const loadKey = options.collection + ? `load${capitalize(options.collection)}Entities` + : 'load'; + + const currentKey = options.collection + ? `current${capitalize(options.collection)}` + : 'current'; + const loadByIdKey = options.collection + ? `load${capitalize(options.collection)}ById` + : 'loadById'; + const setCurrentKey = options.collection + ? `setCurrent${capitalize(options.collection)}` + : 'setCurrent'; + const createKey = options.collection + ? `create${capitalize(options.collection)}` + : 'create'; + const updateKey = options.collection + ? `update${capitalize(options.collection)}` + : 'update'; + const updateAllKey = options.collection + ? `updateAll${capitalize(options.collection)}` + : 'updateAll'; + const deleteKey = options.collection + ? `delete${capitalize(options.collection)}` + : 'delete'; + + // TODO: Take these from @ngrx/signals/entities, when they are exported + const entitiesKey = options.collection + ? `${options.collection}Entities` + : 'entities'; + const entityMapKey = options.collection + ? `${options.collection}EntityMap` + : 'entityMap'; + const idsKey = options.collection ? `${options.collection}Ids` : 'ids'; + + return { + filterKey, + selectedIdsKey, + selectedEntitiesKey, + updateFilterKey, + updateSelectedKey, + loadKey, + entitiesKey, + entityMapKey, + idsKey, + + currentKey, + loadByIdKey, + setCurrentKey, + createKey, + updateKey, + updateAllKey, + deleteKey, + }; } -export type NamedDataServiceState = - { - [K in Collection as `${K}Filter`]: F; - } & { - [K in Collection as `selected${Capitalize}Ids`]: Record; - } & { - [K in Collection as `current${Capitalize}`]: E; - } +export type NamedDataServiceState< + E extends Entity, + F extends Filter, + Collection extends string +> = { + [K in Collection as `${K}Filter`]: F; +} & { + [K in Collection as `selected${Capitalize}Ids`]: Record; +} & { + [K in Collection as `current${Capitalize}`]: E; +}; export type DataServiceState = { - filter: F; - selectedIds: Record; - current: E; -} - -export type NamedDataServiceSignals = - { - [K in Collection as `selected${Capitalize}Entities`]: Signal; - } - -export type DataServiceSignals = - { - selectedEntities: Signal; - } - -export type NamedDataServiceMethods = - { - [K in Collection as `update${Capitalize}Filter`]: (filter: F) => void; - } & - { - [K in Collection as `updateSelected${Capitalize}Entities`]: (id: EntityId, selected: boolean) => void; - } & - { - [K in Collection as `load${Capitalize}Entities`]: () => Promise; - } & - - { - [K in Collection as `setCurrent${Capitalize}`]: (entity: E) => void; - } & - { - [K in Collection as `load${Capitalize}ById`]: (id: EntityId) => Promise; - } & - { - [K in Collection as `create${Capitalize}`]: (entity: E) => Promise; - } & - { - [K in Collection as `update${Capitalize}`]: (entity: E) => Promise; - } & - { - [K in Collection as `updateAll${Capitalize}`]: (entity: E[]) => Promise; - } & - { - [K in Collection as `delete${Capitalize}`]: (entity: E) => Promise; - }; - - -export type DataServiceMethods = - { - updateFilter: (filter: F) => void; - updateSelected: (id: EntityId, selected: boolean) => void; - load: () => Promise; - - setCurrent(entity: E): void; - loadById(id: EntityId): Promise; - create(entity: E): Promise; - update(entity: E): Promise; - updateAll(entities: E[]): Promise; - delete(entity: E): Promise; - } - -export type Empty = Record - -export function withDataService(options: { dataServiceType: ProviderToken>, filter: F, collection: Collection }): SignalStoreFeature< - { - state: Emtpy, - // These alternatives break type inference: - // state: { callState: CallState } & NamedEntityState, - // state: NamedEntityState, - - signals: NamedEntitySignals, - methods: Emtpy, - }, - { - state: NamedDataServiceState - signals: NamedDataServiceSignals - methods: NamedDataServiceMethods - } + filter: F; + selectedIds: Record; + current: E; +}; + +export type NamedDataServiceSignals< + E extends Entity, + Collection extends string +> = { + [K in Collection as `selected${Capitalize}Entities`]: Signal; +}; + +export type DataServiceSignals = { + selectedEntities: Signal; +}; + +export type NamedDataServiceMethods< + E extends Entity, + F extends Filter, + Collection extends string +> = { + [K in Collection as `update${Capitalize}Filter`]: (filter: F) => void; +} & { + [K in Collection as `updateSelected${Capitalize}Entities`]: ( + id: EntityId, + selected: boolean + ) => void; +} & { + [K in Collection as `load${Capitalize}Entities`]: () => Promise; +} & { + [K in Collection as `setCurrent${Capitalize}`]: (entity: E) => void; +} & { + [K in Collection as `load${Capitalize}ById`]: ( + id: EntityId + ) => Promise; +} & { + [K in Collection as `create${Capitalize}`]: (entity: E) => Promise; +} & { + [K in Collection as `update${Capitalize}`]: (entity: E) => Promise; +} & { + [K in Collection as `updateAll${Capitalize}`]: ( + entity: E[] + ) => Promise; +} & { + [K in Collection as `delete${Capitalize}`]: (entity: E) => Promise; +}; + +export type DataServiceMethods = { + updateFilter: (filter: F) => void; + updateSelected: (id: EntityId, selected: boolean) => void; + load: () => Promise; + + setCurrent(entity: E): void; + loadById(id: EntityId): Promise; + create(entity: E): Promise; + update(entity: E): Promise; + updateAll(entities: E[]): Promise; + delete(entity: E): Promise; +}; + +export function withDataService< + E extends Entity, + F extends Filter, + Collection extends string +>(options: { + dataServiceType: ProviderToken>; + filter: F; + collection: Collection; +}): SignalStoreFeature< + { + state: Empty; + // These alternatives break type inference: + // state: { callState: CallState } & NamedEntityState, + // state: NamedEntityState, + + signals: NamedEntitySignals; + methods: Empty; + }, + { + state: NamedDataServiceState; + signals: NamedDataServiceSignals; + methods: NamedDataServiceMethods; + } +>; +export function withDataService(options: { + dataServiceType: ProviderToken>; + filter: F; +}): SignalStoreFeature< + { + state: { callState: CallState } & EntityState; + signals: Empty; + methods: Empty; + }, + { + state: DataServiceState; + signals: DataServiceSignals; + methods: DataServiceMethods; + } >; -export function withDataService(options: { dataServiceType: ProviderToken>, filter: F }): SignalStoreFeature< - { - state: { callState: CallState } & EntityState - signals: Emtpy, - methods: Emtpy, - }, - { - state: DataServiceState - signals: DataServiceSignals - methods: DataServiceMethods - }>; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function withDataService(options: { dataServiceType: ProviderToken>, filter: F, collection?: Collection }): SignalStoreFeature { - const { dataServiceType, filter, collection: prefix } = options; - const { - entitiesKey, - filterKey, - loadKey, - selectedEntitiesKey, - selectedIdsKey, - updateFilterKey, - updateSelectedKey, - - currentKey, - createKey, - updateKey, - updateAllKey, - deleteKey, - loadByIdKey, - setCurrentKey - } = getDataServiceKeys(options); - - const { callStateKey } = getCallStateKeys({ collection: prefix }); - - return signalStoreFeature( - withState(() => ({ - [filterKey]: filter, - [selectedIdsKey]: {} as Record, - [currentKey]: undefined as E | undefined - })), - withComputed((store: Record) => { - const entities = store[entitiesKey] as Signal; - const selectedIds = store[selectedIdsKey] as Signal>; - - return { - [selectedEntitiesKey]: computed(() => entities().filter(e => selectedIds()[e.id])) - } - }), - withMethods((store: Record & StateSignal) => { - const dataService = inject(dataServiceType) - return { - [updateFilterKey]: (filter: F): void => { - patchState(store, { [filterKey]: filter }); - }, - [updateSelectedKey]: (id: EntityId, selected: boolean): void => { - patchState(store, (state: Record) => ({ - [selectedIdsKey]: { - ...state[selectedIdsKey] as Record, - [id]: selected, - } - })); - }, - [loadKey]: async (): Promise => { - const filter = store[filterKey] as Signal; - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const result = await dataService.load(filter()); - patchState(store, prefix ? setAllEntities(result, { collection: prefix }) : setAllEntities(result)); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [loadByIdKey]: async (id: EntityId): Promise => { - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const current = await dataService.loadById(id); - store[callStateKey] && patchState(store, setLoaded(prefix)); - patchState(store, { [currentKey]: current }); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [setCurrentKey]: (current: E): void => { - patchState(store, { [currentKey]: current }); - }, - [createKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const created = await dataService.create(entity); - patchState(store, { [currentKey]: created }); - patchState(store, prefix ? addEntity(created, { collection: prefix }) : addEntity(created)); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [updateKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const updated = await dataService.update(entity); - patchState(store, { [currentKey]: updated }); - // Why do we need this cast to Partial? - const updateArg = { id: updated.id, changes: updated as Partial }; - patchState(store, prefix ? updateEntity(updateArg, { collection: prefix }) : updateEntity(updateArg)); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [updateAllKey]: async (entities: E[]): Promise => { - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const result = await dataService.updateAll(entities); - patchState(store, prefix ? setAllEntities(result, { collection: prefix }) : setAllEntities(result)); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [deleteKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - await dataService.delete(entity); - patchState(store, { [currentKey]: undefined }); - patchState(store, prefix ? removeEntity(entity.id, { collection: prefix }) : removeEntity(entity.id)); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } - catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, +export function withDataService< + E extends Entity, + F extends Filter, + Collection extends string +>(options: { + dataServiceType: ProviderToken>; + filter: F; + collection?: Collection; +}): SignalStoreFeature { + const { dataServiceType, filter, collection: prefix } = options; + const { + entitiesKey, + filterKey, + loadKey, + selectedEntitiesKey, + selectedIdsKey, + updateFilterKey, + updateSelectedKey, + + currentKey, + createKey, + updateKey, + updateAllKey, + deleteKey, + loadByIdKey, + setCurrentKey, + } = getDataServiceKeys(options); + + const { callStateKey } = getCallStateKeys({ collection: prefix }); + + return signalStoreFeature( + withState(() => ({ + [filterKey]: filter, + [selectedIdsKey]: {} as Record, + [currentKey]: undefined as E | undefined, + })), + withComputed((store: Record) => { + const entities = store[entitiesKey] as Signal; + const selectedIds = store[selectedIdsKey] as Signal< + Record + >; + + return { + [selectedEntitiesKey]: computed(() => + entities().filter((e) => selectedIds()[e.id]) + ), + }; + }), + withMethods((store: Record & StateSignal) => { + const dataService = inject(dataServiceType); + return { + [updateFilterKey]: (filter: F): void => { + patchState(store, { [filterKey]: filter }); + }, + [updateSelectedKey]: (id: EntityId, selected: boolean): void => { + patchState(store, (state: Record) => ({ + [selectedIdsKey]: { + ...(state[selectedIdsKey] as Record), + [id]: selected, + }, + })); + }, + [loadKey]: async (): Promise => { + const filter = store[filterKey] as Signal; + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const result = await dataService.load(filter()); + patchState( + store, + prefix + ? setAllEntities(result, { collection: prefix }) + : setAllEntities(result) + ); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [loadByIdKey]: async (id: EntityId): Promise => { + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const current = await dataService.loadById(id); + store[callStateKey] && patchState(store, setLoaded(prefix)); + patchState(store, { [currentKey]: current }); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [setCurrentKey]: (current: E): void => { + patchState(store, { [currentKey]: current }); + }, + [createKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const created = await dataService.create(entity); + patchState(store, { [currentKey]: created }); + patchState( + store, + prefix + ? addEntity(created, { collection: prefix }) + : addEntity(created) + ); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [updateKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const updated = await dataService.update(entity); + patchState(store, { [currentKey]: updated }); + // Why do we need this cast to Partial? + const updateArg = { + id: updated.id, + changes: updated as Partial, }; - }) - ); + patchState( + store, + prefix + ? updateEntity(updateArg, { collection: prefix }) + : updateEntity(updateArg) + ); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [updateAllKey]: async (entities: E[]): Promise => { + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const result = await dataService.updateAll(entities); + patchState( + store, + prefix + ? setAllEntities(result, { collection: prefix }) + : setAllEntities(result) + ); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [deleteKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + await dataService.delete(entity); + patchState(store, { [currentKey]: undefined }); + patchState( + store, + prefix + ? removeEntity(entity.id, { collection: prefix }) + : removeEntity(entity.id) + ); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + }; + }) + ); } diff --git a/libs/ngrx-toolkit/src/lib/with-devtools.ts b/libs/ngrx-toolkit/src/lib/with-devtools.ts index aa76ec3f..707b2b11 100644 --- a/libs/ngrx-toolkit/src/lib/with-devtools.ts +++ b/libs/ngrx-toolkit/src/lib/with-devtools.ts @@ -5,6 +5,7 @@ import { import { SignalStoreFeatureResult } from '@ngrx/signals/src/signal-store-models'; import { effect, inject, PLATFORM_ID, signal, Signal } from '@angular/core'; import { isPlatformServer } from '@angular/common'; +import { Empty } from './shared/empty'; declare global { interface Window { @@ -18,7 +19,7 @@ declare global { } } -type EmptyFeatureResult = { state: {}; signals: {}; methods: {} }; +type EmptyFeatureResult = { state: Empty; signals: Empty; methods: Empty }; export type Action = { type: string }; const storeRegistry = signal>>({}); diff --git a/libs/ngrx-toolkit/src/lib/with-storage-sync.ts b/libs/ngrx-toolkit/src/lib/with-storage-sync.ts index cc775e75..9bd3b050 100644 --- a/libs/ngrx-toolkit/src/lib/with-storage-sync.ts +++ b/libs/ngrx-toolkit/src/lib/with-storage-sync.ts @@ -8,7 +8,7 @@ import { withHooks, withMethods, } from '@ngrx/signals'; -import { Emtpy } from './shared/empty'; +import { Empty } from './shared/empty'; type SignalStoreFeatureInput = Pick< Parameters[0], @@ -20,8 +20,8 @@ type SignalStoreFeatureInput = Pick< const NOOP = () => {}; type WithStorageSyncFeatureResult = { - state: Emtpy; - signals: Emtpy; + state: Empty; + signals: Empty; methods: { clearStorage(): void; readFromStorage(): void; diff --git a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts index b38de847..b6ed8597 100644 --- a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts @@ -1,184 +1,204 @@ -import { SignalStoreFeature, patchState, signalStoreFeature, withComputed, withHooks, withMethods } from "@ngrx/signals"; -import { EntityId, EntityMap, EntityState } from "@ngrx/signals/entities"; -import { Signal, effect, signal, untracked, isSignal } from "@angular/core"; -import { EntitySignals, NamedEntitySignals } from "@ngrx/signals/entities/src/models"; -import { Entity, capitalize } from "./with-data-service"; -import { Emtpy } from "./shared/empty"; +import { + SignalStoreFeature, + patchState, + signalStoreFeature, + withComputed, + withHooks, + withMethods, +} from '@ngrx/signals'; +import { EntityId, EntityMap, EntityState } from '@ngrx/signals/entities'; +import { Signal, effect, signal, untracked, isSignal } from '@angular/core'; +import { + EntitySignals, + NamedEntitySignals, +} from '@ngrx/signals/entities/src/models'; +import { Entity, capitalize } from './with-data-service'; +import { Empty } from './shared/empty'; export type StackItem = Record; export type NormalizedUndoRedoOptions = { - maxStackSize: number; - collections?: string[] -} + maxStackSize: number; + collections?: string[]; +}; const defaultOptions: NormalizedUndoRedoOptions = { - maxStackSize: 100 + maxStackSize: 100, }; export type NamedUndoRedoState = { - [K in Collection as `${K}EntityMap`]: EntityMap; + [K in Collection as `${K}EntityMap`]: EntityMap; } & { - [K in Collection as `${K}Ids`]: EntityId[]; - } + [K in Collection as `${K}Ids`]: EntityId[]; +}; export type NamedUndoRedoSignals = { - [K in Collection as `${K}Entities`]: Signal -} + [K in Collection as `${K}Entities`]: Signal; +}; export function getUndoRedoKeys(collections?: string[]): string[] { - if (collections) { - return collections.flatMap(c => [`${c}EntityMap`, `${c}Ids`, `selected${capitalize(c)}Ids`, `${c}Filter`]) - } - return ['entityMap', 'ids', 'selectedIds', 'filter']; + if (collections) { + return collections.flatMap((c) => [ + `${c}EntityMap`, + `${c}Ids`, + `selected${capitalize(c)}Ids`, + `${c}Filter`, + ]); + } + return ['entityMap', 'ids', 'selectedIds', 'filter']; } -export function withUndoRedo(options?: { maxStackSize?: number; collections: Collection[] }): SignalStoreFeature< - { - state: Emtpy, - // This alternative breaks type inference: - // state: NamedEntityState - signals: NamedEntitySignals, - methods: Emtpy - }, - { - state: Emtpy, - signals: { - canUndo: Signal, - canRedo: Signal - }, - methods: { - undo: () => void, - redo: () => void - } - }>; - -export function withUndoRedo(options?: { maxStackSize?: number }): SignalStoreFeature< - { - state: EntityState, - signals: EntitySignals, - methods: Emtpy - }, - { - state: Emtpy, - signals: { - canUndo: Signal, - canRedo: Signal - }, - methods: { - undo: () => void, - redo: () => void - } - }>; +export function withUndoRedo(options?: { + maxStackSize?: number; + collections: Collection[]; +}): SignalStoreFeature< + { + state: Empty; + // This alternative breaks type inference: + // state: NamedEntityState + signals: NamedEntitySignals; + methods: Empty; + }, + { + state: Empty; + signals: { + canUndo: Signal; + canRedo: Signal; + }; + methods: { + undo: () => void; + redo: () => void; + }; + } +>; + +export function withUndoRedo(options?: { + maxStackSize?: number; +}): SignalStoreFeature< + { + state: EntityState; + signals: EntitySignals; + methods: Empty; + }, + { + state: Empty; + signals: { + canUndo: Signal; + canRedo: Signal; + }; + methods: { + undo: () => void; + redo: () => void; + }; + } +>; -export function withUndoRedo(options: { +export function withUndoRedo( + options: { maxStackSize?: number; - collections?: Collection[] -} = {}): -// eslint-disable-next-line @typescript-eslint/no-explicit-any + collections?: Collection[]; + } = {} +): // eslint-disable-next-line @typescript-eslint/no-explicit-any SignalStoreFeature { - let previous: StackItem | null = null; - let skipOnce = false; - - const normalized = { - ...defaultOptions, - ...options - }; - - // - // Design Decision: This feature has its own - // internal state. - // - - const undoStack: StackItem[] = []; - const redoStack: StackItem[] = []; - - const canUndo = signal(false); - const canRedo = signal(false); + let previous: StackItem | null = null; + let skipOnce = false; + + const normalized = { + ...defaultOptions, + ...options, + }; + + // + // Design Decision: This feature has its own + // internal state. + // + + const undoStack: StackItem[] = []; + const redoStack: StackItem[] = []; + + const canUndo = signal(false); + const canRedo = signal(false); + + const updateInternal = () => { + canUndo.set(undoStack.length !== 0); + canRedo.set(redoStack.length !== 0); + }; + + const keys = getUndoRedoKeys(normalized?.collections); + + return signalStoreFeature( + withComputed(() => ({ + canUndo: canUndo.asReadonly(), + canRedo: canRedo.asReadonly(), + })), + withMethods((store) => ({ + undo(): void { + const item = undoStack.pop(); + + if (item && previous) { + redoStack.push(previous); + } - const updateInternal = () => { - canUndo.set(undoStack.length !== 0); - canRedo.set(redoStack.length !== 0); - }; + if (item) { + skipOnce = true; + patchState(store, item); + previous = item; + } - const keys = getUndoRedoKeys(normalized?.collections); + updateInternal(); + }, + redo(): void { + const item = redoStack.pop(); - return signalStoreFeature( + if (item && previous) { + undoStack.push(previous); + } - withComputed(() => ({ - canUndo: canUndo.asReadonly(), - canRedo: canRedo.asReadonly() - })), - withMethods((store) => ({ - undo(): void { - const item = undoStack.pop(); + if (item) { + skipOnce = true; + patchState(store, item); + previous = item; + } - if (item && previous) { - redoStack.push(previous); - } + updateInternal(); + }, + })), + withHooks({ + onInit(store: Record) { + effect(() => { + const cand = keys.reduce((acc, key) => { + const s = store[key]; + if (s && isSignal(s)) { + return { + ...acc, + [key]: s(), + }; + } + return acc; + }, {}); - if (item) { - skipOnce = true; - patchState(store, item); - previous = item; - } + if (skipOnce) { + skipOnce = false; + return; + } - updateInternal(); - }, - redo(): void { - const item = redoStack.pop(); + // Clear redoStack after recorded action + redoStack.splice(0); - if (item && previous) { - undoStack.push(previous); - } + if (previous) { + undoStack.push(previous); + } - if (item) { - skipOnce = true; - patchState(store, item); - previous = item; - } + if (redoStack.length > normalized.maxStackSize) { + undoStack.unshift(); + } - updateInternal(); - } - })), - withHooks({ - onInit(store: Record) { - effect(() => { - - const cand = keys.reduce((acc, key) => { - const s = store[key]; - if (s && isSignal(s)) { - return { - ...acc, - [key]: s() - } - } - return acc; - }, {}); - - if (skipOnce) { - skipOnce = false; - return; - } - - // Clear redoStack after recorded action - redoStack.splice(0); - - if (previous) { - undoStack.push(previous); - } - - if (redoStack.length > normalized.maxStackSize) { - undoStack.unshift(); - } - - previous = cand; - - // Don't propogate current reactive context - untracked(() => updateInternal()); - }) - } - }) + previous = cand; - ) + // Don't propogate current reactive context + untracked(() => updateInternal()); + }); + }, + }) + ); } From 5877b7ffee710c220b141b15858f83e65e3db033 Mon Sep 17 00:00:00 2001 From: bohoffi Date: Fri, 1 Mar 2024 23:51:03 +0100 Subject: [PATCH 4/7] Revert "build: increase demo budgets" This reverts commit 6c30655356382fc5207bda38976340fa24410d61. --- apps/demo/project.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/demo/project.json b/apps/demo/project.json index 36b34013..483a3605 100644 --- a/apps/demo/project.json +++ b/apps/demo/project.json @@ -41,8 +41,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "1mb", - "maximumError": "1.5mb" + "maximumWarning": "500kb", + "maximumError": "1mb" }, { "type": "anyComponentStyle", @@ -94,4 +94,4 @@ } } } -} +} \ No newline at end of file From 8d526b05030b0ebee9dea21feef5d3eb1e153405 Mon Sep 17 00:00:00 2001 From: bohoffi Date: Fri, 1 Mar 2024 23:51:30 +0100 Subject: [PATCH 5/7] Revert "lint: update lib dependencies according to dep check" This reverts commit 690c88aa0c7d7e38b72118e6184434ce3953ee03. --- libs/ngrx-toolkit/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/ngrx-toolkit/package.json b/libs/ngrx-toolkit/package.json index 7b160be7..bfedf6ee 100644 --- a/libs/ngrx-toolkit/package.json +++ b/libs/ngrx-toolkit/package.json @@ -9,9 +9,10 @@ "peerDependencies": { "@angular/common": "^17.0.0", "@angular/core": "^17.0.0", - "@ngrx/signals": "^17.0.0", - "@ngrx/store": "^17.0.0", - "rxjs": "^6.5.3 || ^7.5.0" + "@ngrx/signals": "^17.0.0" + }, + "dependencies": { + "tslib": "^2.3.0" }, "sideEffects": false } From 34b45c00fd1dd822a34f75b88e7e526b5b2f989a Mon Sep 17 00:00:00 2001 From: bohoffi Date: Fri, 1 Mar 2024 23:52:31 +0100 Subject: [PATCH 6/7] Revert "refactor: unify empty type usage" This reverts commit e5efa94c9eb135e7b0bea2ed21c21b901c6c826a. --- libs/ngrx-toolkit/src/lib/shared/empty.ts | 2 +- libs/ngrx-toolkit/src/lib/with-call-state.ts | 85 ++- .../ngrx-toolkit/src/lib/with-data-service.ts | 683 ++++++++---------- libs/ngrx-toolkit/src/lib/with-devtools.ts | 3 +- .../ngrx-toolkit/src/lib/with-storage-sync.ts | 6 +- libs/ngrx-toolkit/src/lib/with-undo-redo.ts | 326 ++++----- 6 files changed, 496 insertions(+), 609 deletions(-) diff --git a/libs/ngrx-toolkit/src/lib/shared/empty.ts b/libs/ngrx-toolkit/src/lib/shared/empty.ts index 28a0fa01..daeb13ad 100644 --- a/libs/ngrx-toolkit/src/lib/shared/empty.ts +++ b/libs/ngrx-toolkit/src/lib/shared/empty.ts @@ -1,2 +1,2 @@ // eslint-disable-next-line @typescript-eslint/ban-types -export type Empty = {}; +export type Emtpy = {}; \ No newline at end of file diff --git a/libs/ngrx-toolkit/src/lib/with-call-state.ts b/libs/ngrx-toolkit/src/lib/with-call-state.ts index 12520d21..f52410d2 100644 --- a/libs/ngrx-toolkit/src/lib/with-call-state.ts +++ b/libs/ngrx-toolkit/src/lib/with-call-state.ts @@ -5,7 +5,7 @@ import { withComputed, withState, } from '@ngrx/signals'; -import { Empty } from './shared/empty'; +import { Emtpy } from './shared/empty'; export type CallState = 'init' | 'loading' | 'loaded' | { error: string }; @@ -14,27 +14,27 @@ export type NamedCallStateSlice = { }; export type CallStateSlice = { - callState: CallState; -}; + callState: CallState +} export type NamedCallStateSignals = { [K in Prop as `${K}Loading`]: Signal; } & { - [K in Prop as `${K}Loaded`]: Signal; -} & { - [K in Prop as `${K}Error`]: Signal; -}; + [K in Prop as `${K}Loaded`]: Signal; + } & { + [K in Prop as `${K}Error`]: Signal; + } export type CallStateSignals = { loading: Signal; loaded: Signal; - error: Signal; -}; + error: Signal +} export function getCallStateKeys(config?: { collection?: string }) { const prop = config?.collection; return { - callStateKey: prop ? `${config.collection}CallState` : 'callState', + callStateKey: prop ? `${config.collection}CallState` : 'callState', loadingKey: prop ? `${config.collection}Loading` : 'loading', loadedKey: prop ? `${config.collection}Loaded` : 'loaded', errorKey: prop ? `${config.collection}Error` : 'error', @@ -44,19 +44,19 @@ export function getCallStateKeys(config?: { collection?: string }) { export function withCallState(config: { collection: Collection; }): SignalStoreFeature< - { state: Empty; signals: Empty; methods: Empty }, + { state: Emtpy, signals: Emtpy, methods: Emtpy }, { - state: NamedCallStateSlice; - signals: NamedCallStateSignals; - methods: Empty; + state: NamedCallStateSlice, + signals: NamedCallStateSignals, + methods: Emtpy } >; export function withCallState(): SignalStoreFeature< - { state: Empty; signals: Empty; methods: Empty }, + { state: Emtpy, signals: Emtpy, methods: Emtpy }, { - state: CallStateSlice; - signals: CallStateSignals; - methods: Empty; + state: CallStateSlice, + signals: CallStateSignals, + methods: Emtpy } >; export function withCallState(config?: { @@ -68,6 +68,7 @@ export function withCallState(config?: { return signalStoreFeature( withState({ [callStateKey]: 'init' }), withComputed((state: Record>) => { + const callState = state[callStateKey] as Signal; return { @@ -76,8 +77,8 @@ export function withCallState(config?: { [errorKey]: computed(() => { const v = callState(); return typeof v === 'object' ? v.error : null; - }), - }; + }) + } }) ); } @@ -95,32 +96,38 @@ export function setLoading( export function setLoaded( prop?: Prop ): NamedCallStateSlice | CallStateSlice { + if (prop) { return { [`${prop}CallState`]: 'loaded' } as NamedCallStateSlice; - } else { + } + else { return { callState: 'loaded' }; + } } export function setError( error: unknown, - prop?: Prop -): NamedCallStateSlice | CallStateSlice { - let errorMessage = ''; - - if (!error) { - errorMessage = ''; - } else if (typeof error === 'object' && 'message' in error) { - errorMessage = String(error.message); - } else { - errorMessage = String(error); - } + prop?: Prop, + ): NamedCallStateSlice | CallStateSlice { - if (prop) { - return { - [`${prop}CallState`]: { error: errorMessage }, - } as NamedCallStateSlice; - } else { - return { callState: { error: errorMessage } }; - } + let errorMessage = ''; + + if (!error) { + errorMessage = ''; + } + else if (typeof error === 'object' && 'message' in error) { + errorMessage = String(error.message); + } + else { + errorMessage = String(error); + } + + + if (prop) { + return { [`${prop}CallState`]: { error: errorMessage } } as NamedCallStateSlice; + } + else { + return { callState: { error: errorMessage } }; + } } diff --git a/libs/ngrx-toolkit/src/lib/with-data-service.ts b/libs/ngrx-toolkit/src/lib/with-data-service.ts index 877f41a7..2a2baadd 100644 --- a/libs/ngrx-toolkit/src/lib/with-data-service.ts +++ b/libs/ngrx-toolkit/src/lib/with-data-service.ts @@ -1,411 +1,312 @@ -import { ProviderToken, Signal, computed, inject } from '@angular/core'; -import { - SignalStoreFeature, - patchState, - signalStoreFeature, - withComputed, - withMethods, - withState, -} from '@ngrx/signals'; -import { - CallState, - getCallStateKeys, - setError, - setLoaded, - setLoading, -} from './with-call-state'; -import { - setAllEntities, - EntityId, - addEntity, - updateEntity, - removeEntity, -} from '@ngrx/signals/entities'; -import { - EntityState, - NamedEntitySignals, -} from '@ngrx/signals/entities/src/models'; -import { StateSignal } from '@ngrx/signals/src/state-signal'; -import { Empty } from './shared/empty'; +import { ProviderToken, Signal, computed, inject } from "@angular/core"; +import { SignalStoreFeature, patchState, signalStoreFeature, withComputed, withMethods, withState } from "@ngrx/signals"; +import { CallState, getCallStateKeys, setError, setLoaded, setLoading } from "./with-call-state"; +import { setAllEntities, EntityId, addEntity, updateEntity, removeEntity } from "@ngrx/signals/entities"; +import { EntityState, NamedEntitySignals } from "@ngrx/signals/entities/src/models"; +import { StateSignal } from "@ngrx/signals/src/state-signal"; +import { Emtpy } from "./shared/empty"; export type Filter = Record; export type Entity = { id: EntityId }; export interface DataService { - load(filter: F): Promise; - loadById(id: EntityId): Promise; - create(entity: E): Promise; - update(entity: E): Promise; - updateAll(entity: E[]): Promise; - delete(entity: E): Promise; + load(filter: F): Promise; + loadById(id: EntityId): Promise; + create(entity: E): Promise; + update(entity: E): Promise; + updateAll(entity: E[]): Promise; + delete(entity: E): Promise; } export function capitalize(str: string): string { - return str ? str[0].toUpperCase() + str.substring(1) : str; + return str ? str[0].toUpperCase() + str.substring(1) : str; } export function getDataServiceKeys(options: { collection?: string }) { - const filterKey = options.collection - ? `${options.collection}Filter` - : 'filter'; - const selectedIdsKey = options.collection - ? `selected${capitalize(options.collection)}Ids` - : 'selectedIds'; - const selectedEntitiesKey = options.collection - ? `selected${capitalize(options.collection)}Entities` - : 'selectedEntities'; - - const updateFilterKey = options.collection - ? `update${capitalize(options.collection)}Filter` - : 'updateFilter'; - const updateSelectedKey = options.collection - ? `updateSelected${capitalize(options.collection)}Entities` - : 'updateSelected'; - const loadKey = options.collection - ? `load${capitalize(options.collection)}Entities` - : 'load'; - - const currentKey = options.collection - ? `current${capitalize(options.collection)}` - : 'current'; - const loadByIdKey = options.collection - ? `load${capitalize(options.collection)}ById` - : 'loadById'; - const setCurrentKey = options.collection - ? `setCurrent${capitalize(options.collection)}` - : 'setCurrent'; - const createKey = options.collection - ? `create${capitalize(options.collection)}` - : 'create'; - const updateKey = options.collection - ? `update${capitalize(options.collection)}` - : 'update'; - const updateAllKey = options.collection - ? `updateAll${capitalize(options.collection)}` - : 'updateAll'; - const deleteKey = options.collection - ? `delete${capitalize(options.collection)}` - : 'delete'; - - // TODO: Take these from @ngrx/signals/entities, when they are exported - const entitiesKey = options.collection - ? `${options.collection}Entities` - : 'entities'; - const entityMapKey = options.collection - ? `${options.collection}EntityMap` - : 'entityMap'; - const idsKey = options.collection ? `${options.collection}Ids` : 'ids'; - - return { - filterKey, - selectedIdsKey, - selectedEntitiesKey, - updateFilterKey, - updateSelectedKey, - loadKey, - entitiesKey, - entityMapKey, - idsKey, - - currentKey, - loadByIdKey, - setCurrentKey, - createKey, - updateKey, - updateAllKey, - deleteKey, - }; + const filterKey = options.collection ? `${options.collection}Filter` : 'filter'; + const selectedIdsKey = options.collection ? `selected${capitalize(options.collection)}Ids` : 'selectedIds'; + const selectedEntitiesKey = options.collection ? `selected${capitalize(options.collection)}Entities` : 'selectedEntities'; + + const updateFilterKey = options.collection ? `update${capitalize(options.collection)}Filter` : 'updateFilter'; + const updateSelectedKey = options.collection ? `updateSelected${capitalize(options.collection)}Entities` : 'updateSelected'; + const loadKey = options.collection ? `load${capitalize(options.collection)}Entities` : 'load'; + + const currentKey = options.collection ? `current${capitalize(options.collection)}` : 'current'; + const loadByIdKey = options.collection ? `load${capitalize(options.collection)}ById` : 'loadById'; + const setCurrentKey = options.collection ? `setCurrent${capitalize(options.collection)}` : 'setCurrent'; + const createKey = options.collection ? `create${capitalize(options.collection)}` : 'create'; + const updateKey = options.collection ? `update${capitalize(options.collection)}` : 'update'; + const updateAllKey = options.collection ? `updateAll${capitalize(options.collection)}` : 'updateAll'; + const deleteKey = options.collection ? `delete${capitalize(options.collection)}` : 'delete'; + + // TODO: Take these from @ngrx/signals/entities, when they are exported + const entitiesKey = options.collection ? `${options.collection}Entities` : 'entities'; + const entityMapKey = options.collection ? `${options.collection}EntityMap` : 'entityMap'; + const idsKey = options.collection ? `${options.collection}Ids` : 'ids'; + + return { + filterKey, + selectedIdsKey, + selectedEntitiesKey, + updateFilterKey, + updateSelectedKey, + loadKey, + entitiesKey, + entityMapKey, + idsKey, + + currentKey, + loadByIdKey, + setCurrentKey, + createKey, + updateKey, + updateAllKey, + deleteKey + }; } -export type NamedDataServiceState< - E extends Entity, - F extends Filter, - Collection extends string -> = { - [K in Collection as `${K}Filter`]: F; -} & { - [K in Collection as `selected${Capitalize}Ids`]: Record; -} & { - [K in Collection as `current${Capitalize}`]: E; -}; +export type NamedDataServiceState = + { + [K in Collection as `${K}Filter`]: F; + } & { + [K in Collection as `selected${Capitalize}Ids`]: Record; + } & { + [K in Collection as `current${Capitalize}`]: E; + } export type DataServiceState = { - filter: F; - selectedIds: Record; - current: E; -}; - -export type NamedDataServiceSignals< - E extends Entity, - Collection extends string -> = { - [K in Collection as `selected${Capitalize}Entities`]: Signal; -}; - -export type DataServiceSignals = { - selectedEntities: Signal; -}; - -export type NamedDataServiceMethods< - E extends Entity, - F extends Filter, - Collection extends string -> = { - [K in Collection as `update${Capitalize}Filter`]: (filter: F) => void; -} & { - [K in Collection as `updateSelected${Capitalize}Entities`]: ( - id: EntityId, - selected: boolean - ) => void; -} & { - [K in Collection as `load${Capitalize}Entities`]: () => Promise; -} & { - [K in Collection as `setCurrent${Capitalize}`]: (entity: E) => void; -} & { - [K in Collection as `load${Capitalize}ById`]: ( - id: EntityId - ) => Promise; -} & { - [K in Collection as `create${Capitalize}`]: (entity: E) => Promise; -} & { - [K in Collection as `update${Capitalize}`]: (entity: E) => Promise; -} & { - [K in Collection as `updateAll${Capitalize}`]: ( - entity: E[] - ) => Promise; -} & { - [K in Collection as `delete${Capitalize}`]: (entity: E) => Promise; -}; - -export type DataServiceMethods = { - updateFilter: (filter: F) => void; - updateSelected: (id: EntityId, selected: boolean) => void; - load: () => Promise; - - setCurrent(entity: E): void; - loadById(id: EntityId): Promise; - create(entity: E): Promise; - update(entity: E): Promise; - updateAll(entities: E[]): Promise; - delete(entity: E): Promise; -}; - -export function withDataService< - E extends Entity, - F extends Filter, - Collection extends string ->(options: { - dataServiceType: ProviderToken>; - filter: F; - collection: Collection; -}): SignalStoreFeature< - { - state: Empty; - // These alternatives break type inference: - // state: { callState: CallState } & NamedEntityState, - // state: NamedEntityState, - - signals: NamedEntitySignals; - methods: Empty; - }, - { - state: NamedDataServiceState; - signals: NamedDataServiceSignals; - methods: NamedDataServiceMethods; - } ->; -export function withDataService(options: { - dataServiceType: ProviderToken>; - filter: F; -}): SignalStoreFeature< - { - state: { callState: CallState } & EntityState; - signals: Empty; - methods: Empty; - }, - { - state: DataServiceState; - signals: DataServiceSignals; - methods: DataServiceMethods; - } + filter: F; + selectedIds: Record; + current: E; +} + +export type NamedDataServiceSignals = + { + [K in Collection as `selected${Capitalize}Entities`]: Signal; + } + +export type DataServiceSignals = + { + selectedEntities: Signal; + } + +export type NamedDataServiceMethods = + { + [K in Collection as `update${Capitalize}Filter`]: (filter: F) => void; + } & + { + [K in Collection as `updateSelected${Capitalize}Entities`]: (id: EntityId, selected: boolean) => void; + } & + { + [K in Collection as `load${Capitalize}Entities`]: () => Promise; + } & + + { + [K in Collection as `setCurrent${Capitalize}`]: (entity: E) => void; + } & + { + [K in Collection as `load${Capitalize}ById`]: (id: EntityId) => Promise; + } & + { + [K in Collection as `create${Capitalize}`]: (entity: E) => Promise; + } & + { + [K in Collection as `update${Capitalize}`]: (entity: E) => Promise; + } & + { + [K in Collection as `updateAll${Capitalize}`]: (entity: E[]) => Promise; + } & + { + [K in Collection as `delete${Capitalize}`]: (entity: E) => Promise; + }; + + +export type DataServiceMethods = + { + updateFilter: (filter: F) => void; + updateSelected: (id: EntityId, selected: boolean) => void; + load: () => Promise; + + setCurrent(entity: E): void; + loadById(id: EntityId): Promise; + create(entity: E): Promise; + update(entity: E): Promise; + updateAll(entities: E[]): Promise; + delete(entity: E): Promise; + } + +export type Empty = Record + +export function withDataService(options: { dataServiceType: ProviderToken>, filter: F, collection: Collection }): SignalStoreFeature< + { + state: Emtpy, + // These alternatives break type inference: + // state: { callState: CallState } & NamedEntityState, + // state: NamedEntityState, + + signals: NamedEntitySignals, + methods: Emtpy, + }, + { + state: NamedDataServiceState + signals: NamedDataServiceSignals + methods: NamedDataServiceMethods + } >; +export function withDataService(options: { dataServiceType: ProviderToken>, filter: F }): SignalStoreFeature< + { + state: { callState: CallState } & EntityState + signals: Emtpy, + methods: Emtpy, + }, + { + state: DataServiceState + signals: DataServiceSignals + methods: DataServiceMethods + }>; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function withDataService< - E extends Entity, - F extends Filter, - Collection extends string ->(options: { - dataServiceType: ProviderToken>; - filter: F; - collection?: Collection; -}): SignalStoreFeature { - const { dataServiceType, filter, collection: prefix } = options; - const { - entitiesKey, - filterKey, - loadKey, - selectedEntitiesKey, - selectedIdsKey, - updateFilterKey, - updateSelectedKey, - - currentKey, - createKey, - updateKey, - updateAllKey, - deleteKey, - loadByIdKey, - setCurrentKey, - } = getDataServiceKeys(options); - - const { callStateKey } = getCallStateKeys({ collection: prefix }); - - return signalStoreFeature( - withState(() => ({ - [filterKey]: filter, - [selectedIdsKey]: {} as Record, - [currentKey]: undefined as E | undefined, - })), - withComputed((store: Record) => { - const entities = store[entitiesKey] as Signal; - const selectedIds = store[selectedIdsKey] as Signal< - Record - >; - - return { - [selectedEntitiesKey]: computed(() => - entities().filter((e) => selectedIds()[e.id]) - ), - }; - }), - withMethods((store: Record & StateSignal) => { - const dataService = inject(dataServiceType); - return { - [updateFilterKey]: (filter: F): void => { - patchState(store, { [filterKey]: filter }); - }, - [updateSelectedKey]: (id: EntityId, selected: boolean): void => { - patchState(store, (state: Record) => ({ - [selectedIdsKey]: { - ...(state[selectedIdsKey] as Record), - [id]: selected, - }, - })); - }, - [loadKey]: async (): Promise => { - const filter = store[filterKey] as Signal; - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const result = await dataService.load(filter()); - patchState( - store, - prefix - ? setAllEntities(result, { collection: prefix }) - : setAllEntities(result) - ); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [loadByIdKey]: async (id: EntityId): Promise => { - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const current = await dataService.loadById(id); - store[callStateKey] && patchState(store, setLoaded(prefix)); - patchState(store, { [currentKey]: current }); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [setCurrentKey]: (current: E): void => { - patchState(store, { [currentKey]: current }); - }, - [createKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const created = await dataService.create(entity); - patchState(store, { [currentKey]: created }); - patchState( - store, - prefix - ? addEntity(created, { collection: prefix }) - : addEntity(created) - ); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [updateKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const updated = await dataService.update(entity); - patchState(store, { [currentKey]: updated }); - // Why do we need this cast to Partial? - const updateArg = { - id: updated.id, - changes: updated as Partial, +export function withDataService(options: { dataServiceType: ProviderToken>, filter: F, collection?: Collection }): SignalStoreFeature { + const { dataServiceType, filter, collection: prefix } = options; + const { + entitiesKey, + filterKey, + loadKey, + selectedEntitiesKey, + selectedIdsKey, + updateFilterKey, + updateSelectedKey, + + currentKey, + createKey, + updateKey, + updateAllKey, + deleteKey, + loadByIdKey, + setCurrentKey + } = getDataServiceKeys(options); + + const { callStateKey } = getCallStateKeys({ collection: prefix }); + + return signalStoreFeature( + withState(() => ({ + [filterKey]: filter, + [selectedIdsKey]: {} as Record, + [currentKey]: undefined as E | undefined + })), + withComputed((store: Record) => { + const entities = store[entitiesKey] as Signal; + const selectedIds = store[selectedIdsKey] as Signal>; + + return { + [selectedEntitiesKey]: computed(() => entities().filter(e => selectedIds()[e.id])) + } + }), + withMethods((store: Record & StateSignal) => { + const dataService = inject(dataServiceType) + return { + [updateFilterKey]: (filter: F): void => { + patchState(store, { [filterKey]: filter }); + }, + [updateSelectedKey]: (id: EntityId, selected: boolean): void => { + patchState(store, (state: Record) => ({ + [selectedIdsKey]: { + ...state[selectedIdsKey] as Record, + [id]: selected, + } + })); + }, + [loadKey]: async (): Promise => { + const filter = store[filterKey] as Signal; + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const result = await dataService.load(filter()); + patchState(store, prefix ? setAllEntities(result, { collection: prefix }) : setAllEntities(result)); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [loadByIdKey]: async (id: EntityId): Promise => { + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const current = await dataService.loadById(id); + store[callStateKey] && patchState(store, setLoaded(prefix)); + patchState(store, { [currentKey]: current }); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [setCurrentKey]: (current: E): void => { + patchState(store, { [currentKey]: current }); + }, + [createKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const created = await dataService.create(entity); + patchState(store, { [currentKey]: created }); + patchState(store, prefix ? addEntity(created, { collection: prefix }) : addEntity(created)); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [updateKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const updated = await dataService.update(entity); + patchState(store, { [currentKey]: updated }); + // Why do we need this cast to Partial? + const updateArg = { id: updated.id, changes: updated as Partial }; + patchState(store, prefix ? updateEntity(updateArg, { collection: prefix }) : updateEntity(updateArg)); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [updateAllKey]: async (entities: E[]): Promise => { + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + const result = await dataService.updateAll(entities); + patchState(store, prefix ? setAllEntities(result, { collection: prefix }) : setAllEntities(result)); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, + [deleteKey]: async (entity: E): Promise => { + patchState(store, { [currentKey]: entity }); + store[callStateKey] && patchState(store, setLoading(prefix)); + + try { + await dataService.delete(entity); + patchState(store, { [currentKey]: undefined }); + patchState(store, prefix ? removeEntity(entity.id, { collection: prefix }) : removeEntity(entity.id)); + store[callStateKey] && patchState(store, setLoaded(prefix)); + } + catch (e) { + store[callStateKey] && patchState(store, setError(e, prefix)); + throw e; + } + }, }; - patchState( - store, - prefix - ? updateEntity(updateArg, { collection: prefix }) - : updateEntity(updateArg) - ); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [updateAllKey]: async (entities: E[]): Promise => { - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - const result = await dataService.updateAll(entities); - patchState( - store, - prefix - ? setAllEntities(result, { collection: prefix }) - : setAllEntities(result) - ); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - [deleteKey]: async (entity: E): Promise => { - patchState(store, { [currentKey]: entity }); - store[callStateKey] && patchState(store, setLoading(prefix)); - - try { - await dataService.delete(entity); - patchState(store, { [currentKey]: undefined }); - patchState( - store, - prefix - ? removeEntity(entity.id, { collection: prefix }) - : removeEntity(entity.id) - ); - store[callStateKey] && patchState(store, setLoaded(prefix)); - } catch (e) { - store[callStateKey] && patchState(store, setError(e, prefix)); - throw e; - } - }, - }; - }) - ); + }) + ); } diff --git a/libs/ngrx-toolkit/src/lib/with-devtools.ts b/libs/ngrx-toolkit/src/lib/with-devtools.ts index 707b2b11..aa76ec3f 100644 --- a/libs/ngrx-toolkit/src/lib/with-devtools.ts +++ b/libs/ngrx-toolkit/src/lib/with-devtools.ts @@ -5,7 +5,6 @@ import { import { SignalStoreFeatureResult } from '@ngrx/signals/src/signal-store-models'; import { effect, inject, PLATFORM_ID, signal, Signal } from '@angular/core'; import { isPlatformServer } from '@angular/common'; -import { Empty } from './shared/empty'; declare global { interface Window { @@ -19,7 +18,7 @@ declare global { } } -type EmptyFeatureResult = { state: Empty; signals: Empty; methods: Empty }; +type EmptyFeatureResult = { state: {}; signals: {}; methods: {} }; export type Action = { type: string }; const storeRegistry = signal>>({}); diff --git a/libs/ngrx-toolkit/src/lib/with-storage-sync.ts b/libs/ngrx-toolkit/src/lib/with-storage-sync.ts index 9bd3b050..cc775e75 100644 --- a/libs/ngrx-toolkit/src/lib/with-storage-sync.ts +++ b/libs/ngrx-toolkit/src/lib/with-storage-sync.ts @@ -8,7 +8,7 @@ import { withHooks, withMethods, } from '@ngrx/signals'; -import { Empty } from './shared/empty'; +import { Emtpy } from './shared/empty'; type SignalStoreFeatureInput = Pick< Parameters[0], @@ -20,8 +20,8 @@ type SignalStoreFeatureInput = Pick< const NOOP = () => {}; type WithStorageSyncFeatureResult = { - state: Empty; - signals: Empty; + state: Emtpy; + signals: Emtpy; methods: { clearStorage(): void; readFromStorage(): void; diff --git a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts index b6ed8597..b38de847 100644 --- a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts @@ -1,204 +1,184 @@ -import { - SignalStoreFeature, - patchState, - signalStoreFeature, - withComputed, - withHooks, - withMethods, -} from '@ngrx/signals'; -import { EntityId, EntityMap, EntityState } from '@ngrx/signals/entities'; -import { Signal, effect, signal, untracked, isSignal } from '@angular/core'; -import { - EntitySignals, - NamedEntitySignals, -} from '@ngrx/signals/entities/src/models'; -import { Entity, capitalize } from './with-data-service'; -import { Empty } from './shared/empty'; +import { SignalStoreFeature, patchState, signalStoreFeature, withComputed, withHooks, withMethods } from "@ngrx/signals"; +import { EntityId, EntityMap, EntityState } from "@ngrx/signals/entities"; +import { Signal, effect, signal, untracked, isSignal } from "@angular/core"; +import { EntitySignals, NamedEntitySignals } from "@ngrx/signals/entities/src/models"; +import { Entity, capitalize } from "./with-data-service"; +import { Emtpy } from "./shared/empty"; export type StackItem = Record; export type NormalizedUndoRedoOptions = { - maxStackSize: number; - collections?: string[]; -}; + maxStackSize: number; + collections?: string[] +} const defaultOptions: NormalizedUndoRedoOptions = { - maxStackSize: 100, + maxStackSize: 100 }; export type NamedUndoRedoState = { - [K in Collection as `${K}EntityMap`]: EntityMap; + [K in Collection as `${K}EntityMap`]: EntityMap; } & { - [K in Collection as `${K}Ids`]: EntityId[]; -}; + [K in Collection as `${K}Ids`]: EntityId[]; + } export type NamedUndoRedoSignals = { - [K in Collection as `${K}Entities`]: Signal; -}; + [K in Collection as `${K}Entities`]: Signal +} export function getUndoRedoKeys(collections?: string[]): string[] { - if (collections) { - return collections.flatMap((c) => [ - `${c}EntityMap`, - `${c}Ids`, - `selected${capitalize(c)}Ids`, - `${c}Filter`, - ]); - } - return ['entityMap', 'ids', 'selectedIds', 'filter']; + if (collections) { + return collections.flatMap(c => [`${c}EntityMap`, `${c}Ids`, `selected${capitalize(c)}Ids`, `${c}Filter`]) + } + return ['entityMap', 'ids', 'selectedIds', 'filter']; } -export function withUndoRedo(options?: { - maxStackSize?: number; - collections: Collection[]; -}): SignalStoreFeature< - { - state: Empty; - // This alternative breaks type inference: - // state: NamedEntityState - signals: NamedEntitySignals; - methods: Empty; - }, - { - state: Empty; - signals: { - canUndo: Signal; - canRedo: Signal; - }; - methods: { - undo: () => void; - redo: () => void; - }; - } ->; - -export function withUndoRedo(options?: { - maxStackSize?: number; -}): SignalStoreFeature< - { - state: EntityState; - signals: EntitySignals; - methods: Empty; - }, - { - state: Empty; - signals: { - canUndo: Signal; - canRedo: Signal; - }; - methods: { - undo: () => void; - redo: () => void; - }; - } ->; +export function withUndoRedo(options?: { maxStackSize?: number; collections: Collection[] }): SignalStoreFeature< + { + state: Emtpy, + // This alternative breaks type inference: + // state: NamedEntityState + signals: NamedEntitySignals, + methods: Emtpy + }, + { + state: Emtpy, + signals: { + canUndo: Signal, + canRedo: Signal + }, + methods: { + undo: () => void, + redo: () => void + } + }>; + +export function withUndoRedo(options?: { maxStackSize?: number }): SignalStoreFeature< + { + state: EntityState, + signals: EntitySignals, + methods: Emtpy + }, + { + state: Emtpy, + signals: { + canUndo: Signal, + canRedo: Signal + }, + methods: { + undo: () => void, + redo: () => void + } + }>; -export function withUndoRedo( - options: { +export function withUndoRedo(options: { maxStackSize?: number; - collections?: Collection[]; - } = {} -): // eslint-disable-next-line @typescript-eslint/no-explicit-any + collections?: Collection[] +} = {}): +// eslint-disable-next-line @typescript-eslint/no-explicit-any SignalStoreFeature { - let previous: StackItem | null = null; - let skipOnce = false; - - const normalized = { - ...defaultOptions, - ...options, - }; - - // - // Design Decision: This feature has its own - // internal state. - // - - const undoStack: StackItem[] = []; - const redoStack: StackItem[] = []; - - const canUndo = signal(false); - const canRedo = signal(false); - - const updateInternal = () => { - canUndo.set(undoStack.length !== 0); - canRedo.set(redoStack.length !== 0); - }; - - const keys = getUndoRedoKeys(normalized?.collections); - - return signalStoreFeature( - withComputed(() => ({ - canUndo: canUndo.asReadonly(), - canRedo: canRedo.asReadonly(), - })), - withMethods((store) => ({ - undo(): void { - const item = undoStack.pop(); - - if (item && previous) { - redoStack.push(previous); - } + let previous: StackItem | null = null; + let skipOnce = false; - if (item) { - skipOnce = true; - patchState(store, item); - previous = item; - } + const normalized = { + ...defaultOptions, + ...options + }; - updateInternal(); - }, - redo(): void { - const item = redoStack.pop(); + // + // Design Decision: This feature has its own + // internal state. + // - if (item && previous) { - undoStack.push(previous); - } + const undoStack: StackItem[] = []; + const redoStack: StackItem[] = []; - if (item) { - skipOnce = true; - patchState(store, item); - previous = item; - } + const canUndo = signal(false); + const canRedo = signal(false); - updateInternal(); - }, - })), - withHooks({ - onInit(store: Record) { - effect(() => { - const cand = keys.reduce((acc, key) => { - const s = store[key]; - if (s && isSignal(s)) { - return { - ...acc, - [key]: s(), - }; - } - return acc; - }, {}); + const updateInternal = () => { + canUndo.set(undoStack.length !== 0); + canRedo.set(redoStack.length !== 0); + }; - if (skipOnce) { - skipOnce = false; - return; - } + const keys = getUndoRedoKeys(normalized?.collections); - // Clear redoStack after recorded action - redoStack.splice(0); + return signalStoreFeature( - if (previous) { - undoStack.push(previous); - } + withComputed(() => ({ + canUndo: canUndo.asReadonly(), + canRedo: canRedo.asReadonly() + })), + withMethods((store) => ({ + undo(): void { + const item = undoStack.pop(); - if (redoStack.length > normalized.maxStackSize) { - undoStack.unshift(); - } + if (item && previous) { + redoStack.push(previous); + } - previous = cand; + if (item) { + skipOnce = true; + patchState(store, item); + previous = item; + } + + updateInternal(); + }, + redo(): void { + const item = redoStack.pop(); + + if (item && previous) { + undoStack.push(previous); + } + + if (item) { + skipOnce = true; + patchState(store, item); + previous = item; + } + + updateInternal(); + } + })), + withHooks({ + onInit(store: Record) { + effect(() => { + + const cand = keys.reduce((acc, key) => { + const s = store[key]; + if (s && isSignal(s)) { + return { + ...acc, + [key]: s() + } + } + return acc; + }, {}); + + if (skipOnce) { + skipOnce = false; + return; + } + + // Clear redoStack after recorded action + redoStack.splice(0); + + if (previous) { + undoStack.push(previous); + } + + if (redoStack.length > normalized.maxStackSize) { + undoStack.unshift(); + } + + previous = cand; + + // Don't propogate current reactive context + untracked(() => updateInternal()); + }) + } + }) - // Don't propogate current reactive context - untracked(() => updateInternal()); - }); - }, - }) - ); + ) } From e123f287808469150407eb1608fb702bf25c9a54 Mon Sep 17 00:00:00 2001 From: bohoffi Date: Sat, 2 Mar 2024 00:00:55 +0100 Subject: [PATCH 7/7] build: lazy load routes to reduce initial bundle size --- apps/demo/src/app/app.routes.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/demo/src/app/app.routes.ts b/apps/demo/src/app/app.routes.ts index 7d26ae6e..91ace70b 100644 --- a/apps/demo/src/app/app.routes.ts +++ b/apps/demo/src/app/app.routes.ts @@ -1,31 +1,23 @@ import { Route } from '@angular/router'; -import { TodoComponent } from './todo/todo.component'; -import { FlightSearchComponent } from './flight-search/flight-search.component'; -import { FlightSearchSimpleComponent } from './flight-search-data-service-simple/flight-search-simple.component'; -import { FlightEditSimpleComponent } from './flight-search-data-service-simple/flight-edit-simple.component'; -import { FlightSearchDynamicComponent } from './flight-search-data-service-dynamic/flight-search.component'; -import { FlightEditDynamicComponent } from './flight-search-data-service-dynamic/flight-edit.component'; -import { TodoStorageSyncComponent } from './todo-storage-sync/todo-storage-sync.component'; -import { FlightSearchReducConnectorComponent } from './flight-search-redux-connector/flight-search.component'; import { provideFlightStore } from './flight-search-redux-connector/+state/redux'; export const appRoutes: Route[] = [ - { path: 'todo', component: TodoComponent }, - { path: 'flight-search', component: FlightSearchComponent }, + { path: 'todo', loadComponent: () => import('./todo/todo.component').then(m => m.TodoComponent) }, + { path: 'flight-search', loadComponent: () => import('./flight-search/flight-search.component').then(m => m.FlightSearchComponent) }, { path: 'flight-search-data-service-simple', - component: FlightSearchSimpleComponent, + loadComponent: () => import('./flight-search-data-service-simple/flight-search-simple.component').then(m => m.FlightSearchSimpleComponent) }, - { path: 'flight-edit-simple/:id', component: FlightEditSimpleComponent }, + { path: 'flight-edit-simple/:id', loadComponent: () => import('./flight-search-data-service-simple/flight-edit-simple.component').then(m => m.FlightEditSimpleComponent) }, { path: 'flight-search-data-service-dynamic', - component: FlightSearchDynamicComponent, + loadComponent: () => import('./flight-search-data-service-dynamic/flight-search.component').then(m => m.FlightSearchDynamicComponent) }, - { path: 'flight-edit-dynamic/:id', component: FlightEditDynamicComponent }, - { path: 'todo-storage-sync', component: TodoStorageSyncComponent }, + { path: 'flight-edit-dynamic/:id', loadComponent: () => import('./flight-search-data-service-dynamic/flight-edit.component').then(m => m.FlightEditDynamicComponent) }, + { path: 'todo-storage-sync', loadComponent: () => import('./todo-storage-sync/todo-storage-sync.component').then(m => m.TodoStorageSyncComponent) }, { path: 'flight-search-redux-connector', providers: [provideFlightStore()], - component: FlightSearchReducConnectorComponent + loadComponent: () => import('./flight-search-redux-connector/flight-search.component').then(m => m.FlightSearchReducConnectorComponent) }, ];