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);
}
)
Related
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);
}
})
);
}
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);
So in normal javascript if I wanted to assign a value to a variable and then use that value outside of a function it would be done by declaring the variable first and then define it's value in the function. I'm brand new to typescript and angular so I am missing how to do this.
In the code below I am trying to get the value from a method in a service and then pass that value into my return. (I hope that makes sense). However I keep getting undefined on console.log(url) with no other errors.
emailsAPI() {
let url: any
this.apiUrlsService.urlsAPI().subscribe(
data => {
this.results = data
url = this.results.emails
}
);
console.log(url)
return this.http.get('assets/api/email_list.json')
}
api-urls service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
#Injectable()
export class ApiUrlsService {
constructor(
private http: HttpClient
) { }
urlsAPI () {
return this.http.get('assets/api/api_urls.json')
}
}
That's because you're calling async method subscribe and then trying to log the coming value before subscription is resolved. Put last two statements (console.log and return) inside the curly braces just after assigning this.results.emails to the url variable
emailsAPI(): Observable<any> {
let url: any
return this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
url = this.results.emails
// you can now access url variable
return this.http.get('assets/api/email_list.json')
});
}
As per reactive programming, this is the expected behaviour you are getting. As subscribe method is async due to which you are getting result later on when data is received. But your console log is called in sync thread so it called as soon as you are defining subscribe method. If you want the console to get printed. Put it inside your subscribe data block.
UPDATE:
As per your requirement, you should return Subject instead of Observable as Subject being data consumer as well as data producer. So it will consume data from httpget request from email and act as a producer in the method from where you called emailsAPI method.
emailsAPI(): Subject<any> {
let emailSubject:Subject = new Subject();
this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
return this.results.emails;
}).
subscribe(url=> {
this.http.get(your_email_url_from_url_received).subscribe(emailSubject);
});
return emailSubject;
}
The subject can be subscribed same as you will be doing with Observable in your calee method.
In a component, in ngOnInit() I've got two subscriptions to a data service. I want to do some processing once both subscriptions have returned. Whats the best way to do this? I can just process at the end of each, this just seems a little inefficient and won't work for which ever subscription activates first,
Thanks,
Component.TS
ngOnInit()
{
this.dataService.dataA().subscribe((dataAJSON) =>
{
this.dataA= dataAJSON
}
this.dataService.dataB().subscribe((dataBJSON) =>
{
this.dataB= dataBJSON
}
DataService
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class PMDataService
{
constructor(public http : Http)
{
}
dataA()
{
var dataA: any;
var json;
dataA= this.http.get("./assets/dataa.json")
.map(res => res.json());
return dataA
}
dataB()
{
var dataB: any;
var json;
dataB= this.http.get("./assets/datab.json")
.map(res => res.json());
return dataB
}
}
You can use Observable#forkJoin function on the Observables. It emits the last value from each when all observables complete,
Observable.forkJoin(this.dataService.dataA(), this.dataService.dataB())
.subscribe(val => /* val is an array */)
The method used depends on how you want to receive the data:
You can use the zip function. Emits once when all have emitted once. So similar to Promise.all except not on completion.
Observable.zip(obs1, obs2).subscribe((val) => { ... });
You can use the forkJoin function. Emits once when all have completed. So exactly like Promise.all.
Observable.forkJoin(obs1, obs2).subscribe((val) => { ... });
You can use the merge function. Emits in order of emission so could be 1st then 2nd or 2nd then 1st:
obs1.merge(obs2).subscribe((val) => { ... });
You can use concat function. Emits in order 1st then 2nd regardless if 2nd emits first:
obs1.concat(obs2).subscribe((val) => { ... });
It's best practice to split these up into a couple lines for clarity.
const obs1 = Rx.Observable.of(1,2,3);
const obs2 = Rx.Observable.of(1,2,3);
const example = Observable.zip(obs1, obs2);
//const example = Observable.forkJoin(obs1, obs2);
//const example = obs1.merge(obs2);
//const example = obs1.concat(obs2);
example.subscribe(val => { ... });
You could use the operator Zip or CombineLatest from rxjs.
See ReactiveX operators
You could do something like this:
Observable.zip(
this.http.get("./assets/dataa.json"),
this.http.get("./assets/dataa.json")
.take(1)
.map(values => [values[0].json(), values[1].json()])
.subscribe(values => {
// do something with my values
});
You could You can use concat to combine the observables and return a single observable.
Subscribe to observables in order as previous completes, emit values
changed service code
import 'rxjs/add/operator/concat';
export class PMDataService
{
data(){
return this.dataA().concat(this.dataB());
}
// methods dataA and dataB are unchanged, some of the constructor
}
Component code
ngOnInit(){
this.dataService.data().subscribe((dataJSON) =>
{
this.dataA= dataAJSON[0];
this.dataB= dataAJSON[1];
}
}
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;
});