Add EntityComponentStore #3085
Replies: 7 comments
-
I'd rather have the Granted, this is even further reduction of code. @timdeschryver @brandonroberts Thoughts? 🙂 |
Beta Was this translation helpful? Give feedback.
-
@alex-okrushko By the way, Entity Component Store as a separate library in |
Beta Was this translation helpful? Give feedback.
-
I think this can be accomplished with splitting out selectors into a separate library that we already discussed. That decouples entity from the store library also. I'm not sure about making a separate library for each Store/ComponentStore combination. |
Beta Was this translation helpful? Give feedback.
-
Simple solution is // Angular specific
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { EMPTY, Observable } from 'rxjs';
import { catchError, concatMap, finalize, tap } from 'rxjs/operators';
// NGRX specific
import { ComponentStore } from '@ngrx/component-store';
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { EntityMapOne, EntitySelectors } from '@ngrx/entity/src/models';
export interface Model {
id: string;
// ... more fields
}
interface State {
models: EntityState<Model>;
error: Error;
loading: boolean;
}
@Injectable()
export class MyStore extends ComponentStore<State> {
private entityAdapter: EntityAdapter<Model>;
private entitySelectors: EntitySelectors<Model, State>;
constructor(private modelService: ModelService) {
super();
this.entityAdapter = createEntityAdapter<Model>({
selectId: (model) => model.id,
sortComparer: null,
});
this.entitySelectors = this.entityAdapter.getSelectors(
(state) => state.models
);
this.setState({
models: this.entityAdapter.getInitialState(),
error: null,
loading: false,
});
}
/**
* Effects
* */
readonly loadAll = this.effect((origin$: Observable<void>) =>
origin$.pipe(
tap(() => this.setLoading(true)),
concatMap(() =>
this.modelService.getAll().pipe(
tap((models) => {
this.setAll(models);
}),
catchError((error: HttpErrorResponse) => {
this.setError(error.error);
return EMPTY;
}),
finalize(() => {
this.setLoading(false);
})
)
)
)
);
/**
* Updaters
* */
readonly setAll = this.updater((state, models: Model[]) => ({
...state,
models: this.entityAdapter.setAll(models, state.models),
}));
readonly mapOne = this.updater((state, map: EntityMapOne<Model>) => ({
...state,
models: this.entityAdapter.mapOne(map, state.models),
}));
readonly setError = this.updater((state, error: Error) => ({
...state,
error,
}));
readonly setLoading = this.updater((state, loading: boolean) => ({
...state,
loading,
}));
/**
* Selectors
* */
readonly loading$ = this.select((state) => state.loading);
readonly all$ = this.select((state) => this.entitySelectors.selectAll(state));
readonly entityMap$ = this.select((state) =>
this.entitySelectors.selectEntities(state)
);
readonly ids$ = this.select((state) => this.entitySelectors.selectIds(state));
readonly total$ = this.select((state) =>
this.entitySelectors.selectTotal(state)
);
/**
* Abstraction over entity (model) operations
* */
private deeplyUpdateModel(id: string) {
this.mapOne({
id,
map: (model) => ({
...model,
// update here
}),
});
}
}
@Injectable({
providedIn: 'root',
})
export class ModelService {
private readonly apiUrl: string;
constructor(private httpClient: HttpClient) {
this.apiUrl = 'https://my-api/models';
}
getAll(): Observable<Model[]> {
return this.httpClient.get<Model[]>(this.apiUrl);
}
}
function logger(state: any) {
console.groupCollapsed('%c[NewRegisteredProfiles] state', 'color: skyblue;');
console.log(state);
console.groupEnd();
} |
Beta Was this translation helpful? Give feedback.
-
@Ash-kosakyan thanks for suggestion.
I'd rather avoid predefined effects, because that would have a lot of limitations (similar to ngrx/data limitations). Btw, entity updaters could accept partial updater as an optional second argument, for more flexibility: this.setAll(entities, { loading: false }); |
Beta Was this translation helpful? Give feedback.
-
This is my try to implement it: type UpdaterSignature<T> = (observableOrValue: T | Observable<T>) => Subscription
@Injectable()
export class EntityComponentStore<
T,
Entity extends object = T extends EntityState<infer E> ? E extends object ? E : never : never,
Rest extends object = T extends object ? Omit<T, 'ids' | 'entities'> : never
> extends ComponentStore<EntityState<Entity> & Rest>{
protected readonly adapter: Omit<EntityAdapter<Entity>, 'getSelectors'>;
readonly ids$ = this.select(({ ids }) => ids);
readonly entities$ = this.select(({ entities }) => entities);
readonly all$ = this.select(({ ids, entities }) => ids.map((id) => entities[id]!))
readonly total$ = this.select(({ ids }) => ids.length);
constructor(state: Rest, options?: { selectId?: IdSelector<Entity>; sortComparer?: false | Comparer<Entity> }) {
super();
this.adapter = createEntityAdapter(options)
this.setState(this.adapter.getInitialState<Rest>(state));
}
readonly addOne = this.updater((state, entity: Entity) => this.adapter.addOne(entity, state));
readonly addMany = this.updater((state, entities: Entity[]) => this.adapter.addMany(entities, state));
readonly setAll = this.updater((state, entities: Entity[]) => this.adapter.setAll(entities, state));
readonly setOne = this.updater((state, entity: Entity) => this.adapter.setOne(entity, state));
readonly setMany = this.updater((state, entities: Entity[]) => this.adapter.setMany(entities, state));
readonly removeOne: UpdaterSignature<string> | UpdaterSignature<number> = this.updater((state, key: any) => this.adapter.removeOne(key, state));
readonly removeMany: UpdaterSignature<string[]> | UpdaterSignature<number[]> | UpdaterSignature<Predicate<Entity>>
= this.updater((state, keys: any[]) => this.adapter.removeMany(keys, state));
readonly removeAll = this.updater((state) => this.adapter.removeAll(state));
readonly updateOne = this.updater((state, update: Update<Entity>) => this.adapter.updateOne(update, state));
readonly updateMany = this.updater((state, updates: Update<Entity>[]) => this.adapter.updateMany(updates, state));
readonly upsertOne = this.updater((state, entity: Entity) => this.adapter.upsertOne(entity, state))
readonly upsertMany = this.updater((state, entities: Entity[]) => this.adapter.upsertMany(entities, state));
readonly mapOne = this.updater((state, map: EntityMapOne<Entity>) => this.adapter.mapOne(map, state));
readonly map = this.updater((state, map: EntityMap<Entity>) => this.adapter.map(map, state));
} edit: Forgot the Injectable |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
With
EntityComponentStore
, the code that is repeated in most component stores will be reduced.Prototype: EDIT: Improved types
Usage:
If accepted, I would be willing to submit a PR for this feature
[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No
Beta Was this translation helpful? Give feedback.
All reactions