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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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