I'm Trying to do a search to filter posts in a blog
the posts are got from the ngrx store as an observable
so when the user changes the value of the input the posts (with async pipe) will be updated by a pipe called filter-posts
and if the new array is empty a message should appear 'no posts found'
the problem is that the message is not shown after changing the input value
this is the code:
posts.component.html:
<div >
<!--search-->
<input
[(ngModel)]="searchValue"
type="search"
placeholder="Search..." id="search">
<!--posts-->
<ng-container *ngIf="(posts | async).length > 0; else noPosts">
<div *ngFor="let post of posts | async | filterPosts:searchValue">
<div >
<a (click)="goTo('blog/posts/' post.id.toString(), post)">{{post.title}}</a>
</div>
<div >
<img [src]="post.imageSrc" [alt]="post.title">
</div>
<div >
{{post.content | slice:0:80}}
</div>
<div >
<a *ngFor="let category of post.categories" >{{category}}</a>
</div>
<div >
{{post.date}}
</div>
<app-author [author]="post.author"></app-author>
</div>
</ng-container>
<ng-template #noPosts>
<div >No Posts Found</div>
</ng-template>
</div>
posts.component.ts:
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Post } from 'src/app/models/post';
import { User } from 'src/app/models/user';
import { setSelectedPostId } from 'src/app/state/posts/actions';
import { getPosts, PostState } from 'src/app/state/posts/reducer';
import { getUserById } from 'src/app/state/users/reducer';
@Component({
selector: 'app-posts',
templateUrl: './posts.component.html',
styleUrls: ['./posts.component.scss']
})
export class PostsComponent {
constructor(private store: Store<PostState>, private router: Router) { }
search = faSearch;
searchValue: string = '';
goTo(path: string, post: Post) {
this.router.navigate([path]);
this.setSelectedPost(post.id);
}
setSelectedPost(id: string) {
this.store.dispatch(setSelectedPostId({id}));
}
getUser(id: string): Observable<User> {
return this.store.select(getUserById(id));
}
posts: Observable<Post[]> = this.store.select(getPosts);
}
filter-posts.pipe.ts:
import { Pipe, PipeTransform } from '@angular/core';
import { Post } from '../models/post';
@Pipe({
name: 'filterPosts',
pure: false
})
export class FilterPostsPipe implements PipeTransform {
transform(value: Post[], searchValue: string): Post[] {
return value.filter(
(post) =>
post.title.toLowerCase().includes(searchValue.toLowerCase()) ||
post.content.toLowerCase().includes(searchValue.toLowerCase()) ||
post.author.name.toLowerCase().includes(searchValue.toLowerCase()) ||
post.categories.includes(searchValue.toLowerCase())
);
}
}
CodePudding user response:
The use of a pipe
for this kind of operation may seem like a good idea initially, but when you rely on an impure pipe that will run on each change detection cycle, the performance of an app can be compromised.
There are more simple ways to filter an observable value based on another value, and my suggestion is to consider the values from the search input as a stream aswell.
If we instead of using [(ngModel)]
use a FormControl
to listen to the valueChanges
observable of the input, we can combine the streams and filter the posts
value based on the search
value.
You would en up with something like this
// Create a form control for the search input
searchControl = new FormControl();
..........................
posts: Observable<Post[]> = combineLatest([
this.store.select(getPosts),
this.searchControl.valueChanges.pipe( // Listen for the changes of the form control
debounceTime(250), // dont search for every keystroke
startWith(''), // start with an empty string to show all posts
)
]).pipe(
map(([ posts, searchValue ]) => posts.filter(post => {
return post.title.toLowerCase().includes(searchValue.toLowerCase()) ||
post.content.toLowerCase().includes(searchValue.toLowerCase()) ||
post.author.name.toLowerCase().includes(searchValue.toLowerCase()) ||
post.categories.includes(searchValue.toLowerCase())
})
)
);
And in your template connect the formcontrol
<input [formControl]="searchControl"
...>
With this, there is no need for a pipe and the view should update when the observable values change.