Skip to content

Instantly share code, notes, and snippets.

@krismeister
Created March 18, 2018 18:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krismeister/db76922101aeaaf592bf6fed5099e323 to your computer and use it in GitHub Desktop.
Save krismeister/db76922101aeaaf592bf6fed5099e323 to your computer and use it in GitHub Desktop.
NGRX Effects are complicated
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Store} from '@ngrx/store';
import {Actions, Effect} from '@ngrx/effects';
import * as advancedSearchActions from './actions';
import {createLogger, LOG_LEVELS} from '../../../shared/logger';
import {TenantSearchService} from "../../api/tenantSearch/tenantSearch.service";
import {SearchQueryParams} from "../../ng-models/search/SearchQueryParams.interface";
import {ActivatedRoute, Router} from "@angular/router";
import {ADVANCED_SEARCH_TYPES} from "./reducers";
import {MiServiceSearchResults} from "../../models/SearchResults";
const log = createLogger(LOG_LEVELS.REDUX_ADVANCED_SEARCH);
@Injectable() export class AdvancedSearchEffects {
/**
* depeplinkChanged listens for AS_DEEP_LINK_CHANGED, this is when the URL changes.
*/
@Effect()
deepLinkChanged: Observable<advancedSearchActions.Actions> = this.actions$
// only listen to this type:
.ofType(advancedSearchActions.ActionTypes.AS_DEEP_LINK_CHANGED)
// switchMap, allows getCorrectEndpoint() to return an HTTP observable,
// but will wait until the flattened results before moving onto the next .map call
.switchMap( (action: advancedSearchActions.Actions ) => this.getCorrectEndpoint(action.payload))
// map modifies a flat response, here we are returning the AsDataLoadSuccess action
.map( (response) => {
// log('response is', response);
return new advancedSearchActions.AsDataLoadSuccess(response);
});
/**
* loadMore listens for AS_LOAD_MORE, this is when the user clicks the Load More button.
*/
@Effect()
loadMore: Observable<advancedSearchActions.Actions> = this.actions$
// only listen to this type:
.ofType(advancedSearchActions.ActionTypes.AS_LOAD_MORE)
// mergeMap takes the observable from mergeWithState and waits for a flattened object for the next .map
.mergeMap( (action) => this.mergeWithState(action))
// we now have both the full AdvancedSearchState and the original action
.map( ( {asState, action}) => {
//combined now has a shape with params(from current state) and action combined.
const newParams = {...asState.results.meta.params};
const qtyToAdd = (<any>action).payload.qty;
newParams.from = Number(newParams.from) + Number(newParams.size);
newParams.size = Number(qtyToAdd);
return newParams;
})
// this fixes an angular 2 bug in arrays
.map (query => this.convertArrayQueries(query))
// here we
.switchMap( (newParams ) => {
// switchMap, allows getCorrectEndpoint() to return an HTTP observable,
// but will wait until the flattened results before moving onto the next .map call
return this.getCorrectEndpoint(newParams);
})
.map( (response) => {
// map modifies a flat response, here we are returning the AsLoadMoreSuccess action
return new advancedSearchActions.AsLoadMoreSuccess(response);
});
/**
* filterAdd - Add or remove filters
*/
@Effect({ dispatch: false })
filterAdd: Observable<advancedSearchActions.Actions> = this.actions$
.ofType(advancedSearchActions.ActionTypes.AS_FILTER_UPDATE)
.mergeMap( (action) => this.mergeWithState(action))
.map( ({asState, action}) => {
//combined now has a shape with params(from current state) and action combined.
const newParams = {...asState.results.meta.params};
const filtersToAdd = (<any>action).payload;
//for each new filter key, we apply the value
for (let [filterKey, filterValue] of Object.entries(filtersToAdd)) {
newParams[filterKey] = filterValue;
}
return newParams;
})
// this fixes an angular 2 bug in arrays
.map (query => this.convertArrayQueries(query))
.map( (newParams) => {
this.navigate(newParams);
return null;
});
/**
* Returns an observable which when resolved will return an new combine object
* @param {any} action The original advancedSearchActions.Actions
* @returns {Observable<any>} Resolves to an object with shape {asState, action}
*/
mergeWithState(action: any):Observable<any> {
return this.store.first().map(
(state) => {
return {asState: state.advancedSearch, action}
}
);
}
/**
* Decides which search endpoint to hit
* @param newParams The new parameters to send to that endpoint
* @returns {Observable<MiServiceSearchResults>}
*/
getCorrectEndpoint(newParams): Observable<MiServiceSearchResults> {
return this.store.first()
.switchMap( (state) => {
switch(state.advancedSearch.searchType) {
case ADVANCED_SEARCH_TYPES.EVENT: {
// this is the search for events endpoint
return this.tenantSearchService.searchTenantEvents(newParams as SearchQueryParams)
}
default: {
// this is the default
// MI_SERVICE is the default:
return this.tenantSearchService.searchTenantMiServices(newParams as SearchQueryParams);
}
}
}
);
}
/**
* Will update the current page URL query
* @param newQuery {object} The object serialize for the query.
*/
navigate(newQuery: object): void {
// console.log('newParams', newQuery);
// remove params which should not be stored in the URL
['owner', 'from', 'size'].forEach(unsafeParam => {
delete newQuery[unsafeParam];
});
this.router.navigate([], { relativeTo: this.route, queryParams: newQuery});
}
/**
* Fixes and angular 4 bug where arrays are not serialized correctly
* @param {Object} query
* @returns {Object}
*/
convertArrayQueries(query: object): object {
const newQuery = {};
for (let [key, value] of Object.entries(query)) {
if (Array.isArray(value)) {
newQuery[key+'[]'] = value;
} else {
newQuery[key] = value;
}
}
return newQuery
}
constructor(
private actions$: Actions,
private tenantSearchService: TenantSearchService,
private router: Router,
private route: ActivatedRoute,
private store: Store<any>
) {}
}
// How to use the ASDeeplinkChanged Effect
this.activatedRoute.queryParams
.subscribe( (queryParams) => {
this.store.dispatch(new ASActions.AsDeeplinkChanged(...queryParams));
this.loaded = true;
});
@krismeister
Copy link
Author

The purpose of this effects file is to interface with a search backed, the API calls are structured similarly to this:
?time_range=30_DAY&notification_event_type%5B%5D=EVENT_TYPE_AUTO_RENEW&product_category%5B%5D=Support&mrc_min=50&mrc_max=90

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment