Platform: Entities: List & Detail data

Created on 26 Feb 2018  ·  4Comments  ·  Source: ngrx/platform

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[X] Feature request
[ ] Documentation issue or request

What is the current behavior?


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.

  • Could we help programmers by adding optional key to connector where in the feature state entities are stored?
  • Or could we add an example how to combine list & detail view data in with createSelector and how to handle delete and update in reducers for the 2 feature states.

Expected behavior:

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"]
}

Other

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.

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 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))),
      )
    )
  )

All 4 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mappedinn picture mappedinn  ·  3Comments

ghost picture ghost  ·  3Comments

gperdomor picture gperdomor  ·  3Comments

bhaidar picture bhaidar  ·  3Comments

NathanWalker picture NathanWalker  ·  3Comments