Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@ngrx 4 how to filter current loaded data

Tags:

ngrx

I am working on a new angular 4 plus @ngrx 4 project.

I wish to have a searching function on the loaded data.

For example, all the contacts info have been loaded in the component. The contacts list will be filtered which contact name matched with the search text.

Please see screenshot

As the data is existed in store and I do not wish to call web api service again.

Any idea or demo code would be appreciated.

like image 335
Wade REN Avatar asked Nov 09 '17 00:11

Wade REN


2 Answers

You can follow this flow to search what you need on already fetched content:

Use something like '(input)'='searchInputChange$.next(search)' in your input. So, each time the user changes the input, it will trigger our research.

Then, on your component, on the constructor, each time searchInputChange$ changes, we trigger a new SearchAction. Then, we will change our filtered contents on the reducers and the result will be inserted into contents$. On ngOnInit we just load the data from api the first time.

I'm using a model called Content, just an example, that has a string parameter title. We will use this field to filter our contents based on the search input.

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subject } from 'rxjs/Subject';
import {of} from 'rxjs/observable/of';

/** ngrx **/
import {AppState} from '../../app-state.interface';
import * as searchActions from './actions/search.actions';

/** App Models **/
import { Content } from './models/content.model';

export class SearchComponent implements OnInit {

    searchInputChange$ = new Subject<string>();
    contents$: Observable<Array<Content>>;

    constructor(private _store: Store<AppState>) {
      this.searchInputChange$
        .switchMap((text: string) => of(text))
        .subscribe((text: string) => this._store.dispatch(new searchActions.SearchAction(text)));
      this.contents$ = this._store.select(getSearchedContents);
    }

    ngOnInit() {
        this._store.dispatch(new searchActions.LoadAction());
    }

}

Then, we'll have our SearchActions. Load is triggered on the init of our component, fetches some contents from api. LoadSuccess is emitted on the effect of the load action in order to populate our reducer with fetched data and show it in our first component, this has a payload of an array of contents. Search will be triggered on change of our input field, this will have a string payload containing the search string.

import { Action } from '@ngrx/store';

/** App Models **/
import { Content } from '../models/content.model';

export const LOAD = '[Search] Load';
export const LOAD_SUCCESS = '[Search] Load Success';
export const SEARCH = '[Search] Search';

export class LoadAction implements Action {
  readonly type = LOAD;
  constructor() { }
}

export class LoadActionSuccess implements Action {
  readonly type = LOAD_SUCCESS;
  constructor(public payload: Content[]) { }
}

export class SearchAction implements Action {
  readonly type =  SEARCH;
  constructor(public payload: string) {}
}

export type All
  = LoadAction
  | LoadActionSuccess
  | SearchAction;

SearchEffect that will just fetch contents from api:

import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';

/** rxjs **/
import {of} from 'rxjs/observable/of';
import {map} from 'rxjs/operators/map';
import {mergeMap} from 'rxjs/operators/mergeMap';
import {catchError} from 'rxjs/operators/catchError';

/** ngrx **/
import * as searchActions from '../actions/search.actions';

/** App Services **/
import { SomeService } from '../services/some.service';

/** App Model **/
import {Content} from '../models/content.model';

@Injectable()
export class SearchEffects {

  @Effect() load$ = this.actions$
    .ofType(searchActions.LOAD)
      .pipe(
        mergeMap(() => {
          return this.someService.getContentsFromApi()
            .pipe(
              map((contents: Content[]) => {
                return new searchActions.LoadActionSuccess(contents);
              }),
              catchError(() => {
                // do something
              })
            );
        })
    )
  ;

  constructor(private someService: SomeService, private actions$: Actions) { }
}

SearchReducer will handle LoadSuccess when we successfully fetch contents from api and Search action that will filter our fetched contents to return only the ones containing our search string inside content's title parameter. We save first fetched contents in both of contents and searchedContents. Then, on search, we will update searchedContents to contain only contents having content.title including the searched string.

import { isEmpty } from 'lodash';

/** ngrx **/
import {createFeatureSelector} from '@ngrx/store';
import {createSelector} from '@ngrx/store';

/** App Models **/
import { Content } from '../models/content.model';

/** ngrx **/
import * as searchActions from '../actions/search.actions';

export type Action = searchActions.All;

export interface SearchsState {
  contents: Content[];
  searchedContents: Content[];
}

export const initialState: SearchsState = {
  contents: [],
  searchedContents: []
};

/ -------------------------------------------------------------------
// Selectors
// -------------------------------------------------------------------
export const selectContents      = createFeatureSelector<SearchsState>('search');
export const getSearchedContents = createSelector(selectContents, (state: searchedContents) => {
  return state.searchedContents;
});

export function contentsReducer(state: searchedContents = initialState, action: Action): searchedContents {
  switch (action.type) {
    case contentsActions.LOAD_SUCCESS:
      const loadContents = action.payload.map(content => new Content(content));
      return {
               contents: loadContents,
               searchedContents: loadContents
      };
    case contentsActions.SEARCH:
      const keywordContents = isEmpty(action.payload) ? state.contents :
          state.contents.filter(content => content.title.includes(action.payload));
      return {
               contents : state.contents,
               searchedContents : keywordContents
      };
    default: {
      return state;
    }
  }
}

So, updating searchedContents will automatically update the contents in our component.

like image 81
AndreaM16 Avatar answered Oct 17 '22 02:10

AndreaM16


ngrx store is the part of how you store the data. ngrx store is observable so your application flow is

Container -> Components

Container - wrapper component that will select data from store. example:

const contacts$: Observable<contact> = this.store.pluck('contacts');

//*contacts$ - the dollar since is convention for Observable *//

Component - data visualization component, the data will be as Input(). example:

Input() contacts: Array<contact>;

this convention is called sometime SmartComponent(Container) and DumbComponent(component)

now for a data transform/mapping you can use reactive approach(Rxjs) or functional programming or whatever you want but it not related for ngrx because in your contacts component the data as exist.

DEMO FOR YOUR SCENARIO:

contacts.container.ts

@Component({
    selector: 'contacts-container',
    template: `
    <contacts-list [contacts]="contacts$ | async"></contacts-list>
    `
})

export class ContactsContainer {
    contacts$: Observable<[]contact> = this.store.pluck('contacts');
    constructor(
        private store: Store<applicationState>
    ) { }
}

contact-list.component.ts

@Component({
    selector: 'contacts-list',
    template: `
        <input type="text" placeholder="write query" #query>
        <ul>
            <li *ngFor="contact of contacts | searchPipe: query.target.value">
            </li>
        </ul
    `
})

export class ContactsListComponent {
    contcats: Array<contact> = [];
    constructor() { }

}

i use searchPipe for data transform ( custom pipe ) but is only example for data transform you can do it else.

Good Luck!

like image 20
Lidor Avitan Avatar answered Oct 17 '22 04:10

Lidor Avitan