1

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 class="posts">
  <!--search-->
    <input
      [(ngModel)]="searchValue"
      class="p-2 rounded border w-100 bg-secondary text-light" type="search"
      placeholder="Search..." id="search">

  <!--posts-->
  <ng-container *ngIf="(posts | async).length > 0; else noPosts">
    <div class="post" *ngFor="let post of posts | async | filterPosts:searchValue">
      <div class="p-title">
        <a (click)="goTo('blog/posts/' + post.id.toString(), post)">{{post.title}}</a>
      </div>
      <div class="p-image">
        <img [src]="post.imageSrc" [alt]="post.title">
      </div>
      <div class="p-cont">
        {{post.content | slice:0:80}}
      </div>
      <div class="p-category">
        <a *ngFor="let category of post.categories" class="p-1 m-1 text-light fw-bold">{{category}}</a>
      </div>
      <div class="p-date">
        {{post.date}}
      </div>
      <app-author class="p-author d-block" [author]="post.author"></app-author>
    </div>
  </ng-container>
  <ng-template #noPosts>
    <div class="bg-danger w-100">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())
    );
  }
}
muaaz
  • 101
  • 11
  • It seems you're not dispatching an event when you start typing in the input . `(ngModelChange)="onSearchTextChanged($event)"` something like. – Robin Mar 04 '22 at 04:08
  • I don't see the part where to send `searchValue` to find the posts. Pelase include this. Also, isn't it a little bit counter intuitive if using `searchValue` you go to search posts you want and then you also apply same `searchValue` filter pipe to the incoming list? I mean, requesting with `searchValue` is already like a filter. – Joosep Parts Mar 04 '22 at 05:01
  • I'm not dispatching an event for that, the pipe should return the filtered array but it seems now a bad idea – muaaz Mar 05 '22 at 05:18

1 Answers1

1

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.

Daniel B
  • 8,770
  • 5
  • 43
  • 76