Mario
Mario

Reputation: 4998

how to filter collection of observables from a value that is an observable?

I practice ngrx store. I'm trying to filter a task list. The collection of tasks and the filter to apply are observable. In react, I think I could apply a switch and apply the filter, pass the value to the view, but in this case, since the values are observable, I am not sure of the correct way to do this.

model

export interface TodoModel {
  id: string;
  text: string;
  todo: boolean;
  deleted: boolean
}

actions

export const todoAppActions = createActionGroup({
  source: "Todo app component",
  events: {
    add: props<{ model: TodoModel }>(),
    delete: props<{ id: string }>(),
    toggle: props<{ id: string }>(),
    filter: props<{ value: "all" | "deleted" | "pending" }>(),
  },
});

reducer

export interface TodoAppState {
  collection: TodoModel[];
  filterBy: "all" | "deleted" | "pending";
}

export const initialState: TodoAppState = {
  collection: [],
  filterBy: "all",
};

export const todoAppReducer = createReducer(
  initialState,
  on(todoAppActions.add, (state, { model }) => ({
    ...state,
    collection: [...state.collection, model],
  })),
  on(todoAppActions.delete, (state, { id }) => ({
    ...state,
    collection: state.collection.map((item) =>
      item.id === id ? { ...item, deleted: true } : { ...item }
    ),
  })),
  on(todoAppActions.toggle, (state, { id }) => ({
    ...state,
    collection: state.collection.map((item) =>
      item.id === id ? { ...item, todo: !item.todo } : { ...item }
    ),
  })),
  on(todoAppActions.filter, (state, { value }) => ({
    ...state,
    filterBy: value,
  }))
);

component

export class TodoAppComponent {
  collection$: Observable<TodoModel[]>;
  filter$: Observable<"all" | "deleted" | "pending">;

  constructor(private readonly store: Store<{ todo: TodoAppState }>) {
    this.collection$ = this.store.select((state) => state.todo.collection);
    this.filter$ = this.store.select((state) => state.todo.filterBy);
  }

  add() {
    const model = <TodoModel>{
      id: crypto.randomUUID(),
      text: "lorem ipsum",
      deleted: false,
      todo: false,
    };

    this.store.dispatch(todoAppActions.add({ model }));
  }

  delete(id: string) {
    this.store.dispatch(todoAppActions.delete({ id }));
  }

  toggle(id: string) {
    this.store.dispatch(todoAppActions.toggle({ id }));
  }

  filter(value: "all" | "deleted" | "pending") {
    this.store.dispatch(todoAppActions.filter({ value }));
  }

  applyFilter() {
    return this.collection$.pipe(map((value) => {})); // <--
  }
}

template

<button (click)="add()">Add task</button>

<div *ngIf="(collection$ | async)!.length > 0">
  <button (click)="filter('all')">All</button>
  <button (click)="filter('deleted')">Deleted</button>
  <button (click)="filter('pending')">Pending</button>
</div>

<ul>
  <li *ngFor="let todo of collection$ | async">
    <span
      [ngStyle]="{ 'text-decoration': todo.deleted ? 'line-through' : 'none' }"
      >{{ todo.text }}
    </span>
    <div>
      <button (click)="delete(todo.id)" [disabled]="todo.deleted || todo.todo">
        Delete
      </button>
      <button (click)="toggle(todo.id)" [disabled]="todo.deleted">
        Toggle
      </button>
    </div>
  </li>
</ul>

I thought that *ngFor would take the data from this function but I'm not sure how to previously apply the filter

applyFilter() {
  return this.collection$.pipe(map((value) => {}));
}

What is the correct way for the collection listed in the template to have the filter applied?

update 1

I have solved it as follows

I have added the following member property

currentFilter!: "all" | "deleted" | "pending";

In the constructor I subscribe to filter$ to keep the currentFilter property updated

this.filter$.subscribe({
   next: (value) => (this.currentFilter = value),
});

I get the filtered data as follows

getFilter() {
  return this.collection$.pipe(
    map((value) => {
      switch (this.currentFilter) {
        case "all":
          return value;
        case "deleted":
          return value.filter((item) => item.deleted);
        case "pending":
          return value.filter((item) => !item.deleted);
      }
    })
  );
}

In the template

<ul>
  <li *ngFor="let todo of getFilter() | async">
    <span
      [ngStyle]="{ 'text-decoration': todo.deleted ? 'line-through' : 'none' }"
      >{{ todo.text }}
    </span>
    <div>
      <button (click)="delete(todo.id)" [disabled]="todo.deleted || todo.todo">
        Delete
      </button>
      <button (click)="toggle(todo.id)" [disabled]="todo.deleted">
        Toggle
      </button>
    </div>
  </li>
</ul>

This works as expected. But how can it be done without this extra property?

Upvotes: 1

Views: 160

Answers (1)

B1ker4nt3nd
B1ker4nt3nd

Reputation: 36

If you have both your filter and collection in the store, than I would create a selector based on these information.

export const selectTodo = (state: any) => state.todo as TodoAppState;

export const collection$ = createSelector(selectTodo, (state) => state.collection);

export const filter$ = createSelector(selectTodo, (state) => state.filterBy);

export const selectFilteredCollection$ = 
       createSelector(collection$, filter$, (collection, filteredBy) => 
        collection.filter(x=> filteredBy === 'all' || 
          (filteredBy === "deleted" && x.deleted) || 
          (filteredBy === "pending" && !x.deleted)
    )
);

You can simply display result of selectFilteredCollection$ in your component.

Upvotes: 1

Related Questions