[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[X] Feature request
[ ] Documentation issue or request
Currently if you have a list view with simplified data and detail view with additional properties tied to list data you can merge feature states in selectors but this is one of the basic application structures.
Simple programmer friendly API or documentation to handle list / detail state with @ngrx/entity
{
"entities": {
"1": { "name": "whatever"},
"2": { "name": "whatever 2"}
},
"entityDetails": {
"1": { "email": "[email protected]" },
"2": { "email": "[email protected]" }
},
"selectedUserId": "1",
"ids": ["1","2"]
}
At minimum could entities property of @ngrx/entity be configured with createEntityAdapter? All reducer functions are the same in both entities and entityDetails . From how I look at it state.ids property is handled properly with removeManyMutably in case of store removals only thing left for the programmer would be to tie in removes from collections entities & entityDetails in his/her reducer.
this may be similar to a ticket i just submitted #871
The difference being I wouldn't expect to maintain two object maps in state (summary vs detail), I would expect to merge summary data with a constant default value for the entity type.
I'd rather add an example on using selectors to merge feature states together since this scenario should be relatively straightforward to remove collections from both states with a single action.
I know this isn't a very active issue, perhaps it's backlogged for adding to an examples section. Was just passing through and thought I'd add some thoughts for those looking for help on this scenario. I'm down to write up a real example if there's a good place to do such a thing.
I handle this scenario with using adapter.upsertX, type guards, and a BaseDTO interface that gets extended for the detail interface.
As a user,
When I navigate to the entity detail page,
I want the entity detail data to load into the view,
So that I can read detail information.
Starting with the models:
interface PostDTO {
id: string;
title: string;
}
interface PostDetailDTO extends PostDTO {
content: string;
}
The PostsFeatureState looks like this:
interface PostsState extends EntityState<PostDTO | PostDetailDTO> {};
Essential Derived Data — Immutable
Data of this kind can always be re-derived (from the input data — i.e. from
the essential state) whenever required. As a result we do not need to store
it in the ideal world (we just re-derive it when it is required) and it is clearly
accidental state.
Out of the Tarpit - pg. 25
In following the _derived data / projection / selector_ mantra (that @brandonroberts points out), we can derive the existence of a PostDetailDTO via a type guard, written like so:
function isPostDetailDTO(
post: PostDetailDTO | PostDTO,
): post is PostDetailDTO {
return post && !!(<PostDetailDTO>post).content;
}
Using the adapter, allPostDTO entities can be added, potentially when a user navigates to the posts route:
adapter.addAll(action.posts, state);
A PostDTO entitiy can be upated to a PostDetailDTO when they visit the post/:id route:
// upsert is my suggestion, will depend on the app
// a user might land directly on a detail page and you may not have added PostDTO entities yet
adapter.upsertOne(action.post, state);
To complete this user story we could:
1) Load the PostDetailDTO data upon navigating to the post/:id route
2) Hook up data to the view, and only let it through if the PostDetailDTO is available
Triggering a loading action can be handled in various ways, we'll just assume that it's all happening in an Effect via a route + load action(s). Caching strategies could be done by using the type guard there.
For hooking data up to the view, I'd setup something _similar_ this:
import { filter } from 'rxjs/operators';
@Component({
template: `
<div *ngIf="(postDetailData$ | async) as post">
<h1>{{ post?.title }}</h1>
<main [innerHTML]="post?.content"></main>
</div>
`
})
export class PostDetail {
postDetailData$ = this.store.pipe(
select(postsQuery.getPostByRoute),
filter(post => isPostDetailDTO(post)),
);
constructor(store) {}
}
Here's an example of how you could use the type guard to provide a caching strategy:
@Effect()
loadPost$ = this.actions.pipe(
ofType(PostsActionTypes.ResourceDetailLoadPost),
withLatestFrom(
this.store.pipe(select(postsQuery.allPosts)),
),
filter(([action, posts]) => !isPostDetailDTO(posts[action.postId])),
switchMap(([action]) =>
this.postsService.getPost(action.postId).pipe(
map(post => new fromPostsActions.PostApiPostLoaded(post)),
catchError(error => of(new fromPostsActions.PostApiError(error))),
)
)
)
Closing with example reference
Most helpful comment
I know this isn't a very active issue, perhaps it's backlogged for adding to an examples section. Was just passing through and thought I'd add some thoughts for those looking for help on this scenario. I'm down to write up a real example if there's a good place to do such a thing.
I handle this scenario with using
adapter.upsertX, type guards, and aBaseDTOinterface that gets extended for the detail interface.Starting with the models:
The
PostsFeatureStatelooks like this:In following the _derived data / projection / selector_ mantra (that @brandonroberts points out), we can derive the existence of a
PostDetailDTOvia a type guard, written like so:Using the adapter, all
PostDTOentities can be added, potentially when a user navigates to thepostsroute:A
PostDTOentitiy can be upated to aPostDetailDTOwhen they visit thepost/:idroute:To complete this user story we could:
1) Load the
PostDetailDTOdata upon navigating to thepost/:idroute2) Hook up data to the view, and only let it through if the
PostDetailDTOis availableTriggering a loading action can be handled in various ways, we'll just assume that it's all happening in an
Effectvia a route + load action(s). Caching strategies could be done by using the type guard there.For hooking data up to the view, I'd setup something _similar_ this:
Here's an example of how you could use the type guard to provide a caching strategy: