Reputation: 4998
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
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