I have a number of components on a page, which all use observables to get API data. I pass these observables to a loading service, which I want to display a loader from when the first observable is passed until the last one has finalised.
Loading service:
private _loading = new BehaviorSubject<boolean>(false);
readonly loading$ = this._loading.asObservable();
showUntilLoadingComplete<T>(observable$: Observable<T>): Observable<T> {
return of(null).pipe(
tap(_ => this._loading.next(true)),
concatMap(_ => observable$),
finalize(() => this._loading.next(false))
);
}
My components then call loading service like so:
this.loadingService.showUntilLoadingComplete(someObservable$)
.subscribe(data=> {
// do stuff
});
However, due to the first observable finalising, the behaviour subject gets passed false and this in turn stops the loader from showing. I have considered creating another behaviour subject to store an array of the active observables, and remove them from here once finalised, and then subscribing to that and setting the loader off once the array has no length. But this doesn't seem like a great solution, so I am looking for others input.
Since you're depending on the same loading$ Observable in a singleton service, then you can add another property to reflect the active number of calls, then turn the loading off only if there is no other active call.
Try something like the following:
private _active: number = 0;
private _loading = new BehaviorSubject<boolean>(false);
readonly loading$ = this._loading.asObservable();
showUntilLoadingComplete<T>(observable$: Observable<T>): Observable<T> {
return of(null).pipe(
tap(() => {
this._loading.next(true);
this._active++;
}),
concatMap((_) => observable$),
finalize(() => {
this._active--;
if (!this._active) {
this._loading.next(false);
}
})
);
}
Related
In my Angular app , i'm working to set up a state management logic using BehaviourSubject
So , i ve in my store file this :
myStoreData = new BehaviourSubject([])
in my actions file i ve this :
export const SAVE_DATA_IN_CONTEXT = '[User] Save user the incoming data';
So that , in the component , when needed , the method to call is this:
click(newData){
this.reducerService('SAVE_DATA_IN_CONTEXT' , newData)
}
My purpose , is that in my reducer , i won't just send to the store (behaviourSubject) the new data , but i want that it appends it with the existing one ( i doosn't want that the new value replace the existing one) :
As a result it would look like this :
data to send to BehaviourSubject (array) = existing
data (array of objects) + new data (object)
my reducer looks like this , and i tried that :
public dispatchAction(actionTag: string, newDataPayload: any | null): void {
switch (actionTag) {
case ActionsTags.SAVE_DATA_IN_CONTEXT :
const newDataToSet = [...Stores.myStoreData.getValue() , ...newDataPayload ];
Stores.myStoreData.next(newDataPayload);
break;
}
Since i'm convinced that the method getValue() is a bad practise , and i won't pass by Stores.myStoreData.subscribe() because i can't handle the unsubscription and the user click method would be repetitive (possibly the subscribe would open a new subscription each time)
I'm looking fo a better manner to do it properly (maybe change the BehaviouSubject)
Suggestions ??
As explained in some comments under your question, there are libraries for this and you should probably use them instead of reinventing the wheel.
That said, lets assume this is for learning purpose and do it anyway.
I'd recommend to embrace reactive programming and build everything as streams. Then make a super tiny layer into a service to wrap this so you can provide it through dependency injection.
For the reactive bit, I'd just have a subject that I'd pass actions to. Based onto this, I'd have a stream maintaining the state. This would look like the following:
const action$ = new Subject();
const state$ = action$.pipe(
scan((state, action) => {
switch (action.type) {
case 'some_action':
// todo: return a new state
default:
state;
}
})
);
Then if you want to provide this into a service you could simply do:
#Injectable()
export class Store {
private action$ = new Subject();
public state$ = action$.pipe(
scan((state, action) => {
switch (action.type) {
case 'some_action':
// todo: return a new state
default:
state;
}
}),
shareReplay(1)
);
public dispatch(action): void {
this.action$.next(action)
}
}
I have a simple app on Angular/rxjs/Ngrx which requests list of default films from the api.
component.ts
export class MoviesComponent implements OnInit {
private movies$: Observable<{}> =
this.store.select(fromRoot.getMoviesState);
private films = [];
constructor(public store: Store<fromRoot.State>) {}
ngOnInit() {
this.store.dispatch(new MoviesApi.RequestMovies());
this.movies$.subscribe(film => this.films.push(film));
console.log(this.films)
}
effects.ts
#Effect()
requestMovies$: Observable<MoviesApi.MoviesApiAction> = this.actions$
.pipe(
ofType(MoviesApi.REQUEST_MOVIES),
switchMap(actions => this.MoviesApiServ.getDefaultMoviesList()
.pipe(
mergeMap(movies => of(new MoviesApi.RecieveMovies(movies))),
catchError(err => {
console.log('err', err);
return of(new MoviesApi.RequestFailed(err));
})
)
)
);
service.ts
export class MoviesApiService {
private moviesList = [];
constructor(private http: HttpClient) { }
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
return from(this.moviesList);
}
public getMovieByTitle(movieTitle: string): Observable<{}> {
return this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
);
}
}
DEFAULT_MOVIES is just array with titles.
So my getDefaultMoviesList method is not sending data. But if I replace this.moviesList to hardcoced array of values it works as expected.
What I'm doing wrong?
UPD
I wanted to loop over the default list of films, then call for each film getMovieByTitle and collect them in array and send as Observable. Is there any better solution?
1) You should probably move this line to the service contructor, otherwise you will push a second array of default movies every time you getDefaultMoviesList:
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
2) Actually you should probably merge the output of each http.get:
public getDefaultMoviesList(): Observable<{}> {
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
}
3) You should actually only do that once and store it in BehaviorSubject not to make new HTTP request on each getDefaultMoviesList
private movies$: BehaviorSubject<any> = new BehaviorSubject<any>();
public getMovies$() {
return this.movies$.mergeMap(movies => {
if (movies) return of(movies);
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
})
}
4) Your implementation shouldn't work at all since:
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item =>
this.moviesList.push(item))); // This line will happen after http get completes
return from(this.moviesList); // This line will happen BEFORE the line above
}
So you will always return an Observable of empty array.
5) You shouldn't use map if you don't want to map your array to another one. You should use forEach instead.
map is used like this:
const mappedArray = toMapArray.map(element => someFunction(element));
You can try creating the observable using of operator.
Ex: of(this.moviesList);
One intersting fact to note is that Observable.of([]) will be an empty array when you subscribe to it. Where as when you subscribe to Observable.from([]) you wont get any value.
Observable.of, is a static method on Observable. It creates an Observable for you, that emits value(s) that you specify as argument(s) immediately one after the other, and then emits a complete notification.
Im currently getting the new updated user value this way:
this.Service.user$.subscribe(data => {
this.userData = data;
this.userId = data._id;
});
but the updateUser is only executed every 5 secs.
So before its loaded the userData and UserId is empty.
is there a way i can get the stored user data from whats already in the service, instead of waiting 5 secs to it beeing executed again?
something like:
this.Service.user$().GET((data:any) => { // gets the value already stored
});
How would i accomplish this?
Service code:
user$: Observable<any>;
constructor(private http: HttpClient, private router: Router) {
this.user$ = this.userChangeSet.asObservable();
}
updateUser(object) {
this.userChangeSet.next(object);
}
Edit:
Also, how would i destory all subscribes on ngOnDestroy event?
What you can do in your service is internally use a BehaviourSubject to
store the values but expose this as an Observable.
Here is a quote from the docs detailing what a BehaviourSubject is
One of the variants of Subjects is the BehaviorSubject, which has a notion of "the current value".
It stores the latest value emitted to its consumers, and
whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject
See here for more.
Service code:
private _user$ = new BehaviourSubject<any>(null); // initially null
constructor(private http: HttpClient, private router: Router) {
this.userChangeSet.subscribe(val => this._user$.next(val))
}
get user$ () {
return this._user$.asObservable();
}
Then you can use it like normal in your component.
this.service.user$.subscribe(v => {
// do stuff here
})
Note that the first value
that the component will get will be null since this is the inital value of
the BehaviourSubject.
EDIT:
In the component
private _destroyed$ = new Subject();
public ngOnDestroy (): void {
this._destroyed$.next();
this._destroyed$.complete();
}
And then for the subscription
this.service.user$.pipe(
takeUntil(this._destroyed$)
).subscribe(v => {
// do stuff here
})
The way this works is that when the destroyed$ subject emits, the observables that have piped takeUntil(this._destroyed$) will unsubscribe from their respective sources.
Use BehaviorSubject for userChangeSet. It emits value immediately upon subscription.
Example:
userChangeSet = new BehaviorSubject<any>(this.currentData);
I have a subject that is subscribed to and fires when a user searches.
let searchView;
this.searchSubject
.switchMap((view: any) => {
searchView = view;
this.http.post(this.url, view);
})
.subscribe(page => {
this.searchHistoryService.addRecentSearch(searchView).subscribe();
})
searchHistoryService.addRecentSearch records this search so the user can see their recent searches.
I don't think this is good practice as the observable is subscribed to everytime, I would rather use a subject which I'm calling .next() on, or combine the history call with the search call itself.
If searchHistoryService.addRecentSearch returns a Subject I can call .next() but where would I subscribe to it?
I tried adding this in the searchHistoryService's constructor
this.searchHistorySubject.do(observableIWantToCall()).subscribe()
and then replacing the subscription to 'addRecentSearch' with this:
this.searchHistoryService.searchHistorySubject.next(searchView)
But it doesnt work.
The inner observable, observableIWantToCall() gets called but the observable returned isnt subscribed to.
What's wrong with this and what is best practice for subscribing to an observable when another is finished emitting?
I think you can do something like this:
let searchView;
private searchHistorySubject$: Subject<any> = new Subject<any>();
constructor(){
this.searchHistoryService.addRecentSearch(searchView).first().subscribe(
response => {
//It will entry when you send data through next
},
error => {
console.log(error);
}
);
}
...
addRecentSearch(searchView) {
...
return this._searchHistorySubject$.asObservable();
}
setSearchHistoryEvent(value: any) {
this._searchHistorySubject$.next(value);
}
this.searchSubject
.switchMap((view: any) => {
searchView = view;
this.http.post(this.url, view);
})
.subscribe(page => {
this.searchHistoryService.setSearchHistoryEvent(searchView);
}
)
I'm trying to learn Angular 2 and am rebuilding an Angular 1 app I've made with Angular 2 using the Angular CLI. I've setup a HTTP GET request, which fires successfully, and setup a subscriber to interpret the result, and console logging in the subscriber function shows the data I expect. However, no data is being updated on the template.
I tried setting the data to an initial value, to a value in the ngOnInit, and in the subscriber function, and the initial and ngOnInit update the template accordingly. For the life of me, I can't figure out why the template won't update on the subscribe.
events: any[] = ['asdf'];
constructor(private http: Http) {
}
ngOnInit() {
this.events = ['house'];
this.getEvents().subscribe(this.processEvents);
}
getEvents(): Observable<Event[]> {
let params: URLSearchParams = new URLSearchParams();
params.set('types', this.filters.types.join(','));
params.set('dates', this.filters.dates.join(','));
return this.http
.get('//api.dexcon.local/getEvents.php', { search: params })
.map((response: Response) => {
return response.json().events;
});
}
processEvents(data: Event[]) {
this.events = ['car','bike'];
console.log(this.events);
}
The data is being displayed via an ngFor, but car and bike never show. Where have I gone wrong?
You have gone wrong with not respecting the this context of TypeScript, if you do stuff like this:
.subscribe(this.processEvents);
the context get lost onto the processEvents function.
You have to either bind it:
.subscribe(this.processEvents.bind(this));
Use an anonymous function:
.subscribe((data: Events) => {this.processEvents(data)});
Or set your method to a class property:
processEvents: Function = (data: Event[]) => {
this.events = ['car','bike'];
console.log(this.events);
}
Pick your favourite, but I like the last option, because when you use eventListeners you can easily detach them with this method.
Not really sure with what's going on with that processEvents. If you want to subscribe to your response just do:
this.getEvents()
.subscribe(data => {
this.events = data;
});