I might be off on the process, but here goes:
I have an angular2 service. The source for the data of this service is going to be localstorage... later optionally updated when a DB call using http returns. Because I'll be wanting to update the data returned as the various sources come back, it appears I want to use an observables. For now, I'm just trying to get the concept down, so I've skipped the localstorage aspect... but I'm including the 'backstory' so it makes (some) sense as to why I'm wanting to do this in multiple methods.
My thought was I would have a "getHTTPEvents()" method that would return an observable with the payload being the events from the DB. (the theory being that at some point in the future I'd also have a 'getLSEvents()' method that would piggy back in there)
To mock that up, I have this code:
private eventsUrl = 'app/mock-events.json';
getHTTPEvents() : Observable<Array<any>> {
return this._http.get(this.eventsUrl)
.map(response => response.json()['events'])
.catch(this.handleError); // handle error is a logging method
}
My goal would be to create a method that allows filtering on the returned events yet still returns an observable to users of the service. That is where my problem is. With that goal, I have a public method which will be called by users of the service. (attempted to use pattern from here https://coryrylan.com/blog/angular-2-observable-data-services)
public getEvents(key:string,value:string) : Observable<Array<any>> {
var allEventsObserve : Observable<Array<any>> = this.getHTTPEvents();
var filteredEventsObserve : Observable<Array<any>>;
allEventsObserve
.subscribe(
events => {
for(var i=0;i<events.length;i++) {
if(events[i][key]==value) {
console.log('MATCH!!!' + events[i][key]); // THIS WORKS!
return new Observable(observer => filteredEventsObserve = observer); // what do I need to return here? I want to return an observable so the service consumer can get updates
}
}
return allEventsObserve
},
error => console.error("Error retrieving all events for filtering: " + error));
}
The above doesn't work. I've watch lots of videos and read lots of tutorials about observables, but nothing I can find seems to go more indepth other than creating and using the http observable.
I further tried this method of making the new observable:
var newObs = Observable.create(function (observer) {
observer.next(events[i]);
observer.complete(events[i]);
});
And while at least that compiles, I'm not sure how to 'return' it at the right time... as I can't "Create" it outside the allEventsObserve.subscribe method (because 'events' doesn't exist) and can't (seem) to "return" it from within the subscribe. I'm also not entirely sure how I'd then "trigger" the 'next'...?
Do I need to modify the data within allEventsObserve and somehow simply still return that? Do I make a new observable (as attempted above) with the right payload - and if so, how do I trigger it? etc... I've checked here: How to declare an observable on angular2 but can't seem to follow how the 'second' observable gets triggered. Perhaps I have the entire paradigm wrong?
It appears that you're misunderstanding what an RxJS operator (like map, filter, etc) actually returns, and I think correcting that will make the solution clear.
Consider this short example:
allEventsObserve
.map(events => {
return 'this was an event';
})
Granted, it's a pretty useless example since all of the data from events is lost, but let's ignore that for now. The result of the code above is not an array of strings or anything else, it's actually another Observable. This Observable will just emit the string 'this was an event' for each array of events emitted by allEventsObserve This is what allows us to chain operators on observables -- each operator in the chain returns a new Observable that emits items that have been modified in some way be the previous operator.
allEventsObserve
.map(events => {
return 'this was an event';
})
.filter(events => typeof events !== 'undefined')
allEventsObserve is obviously an Observable, allEventsObserve.map() evaluates to an Observable, and so does allEventsObserve.map().filter().
So, since you're expecting your function to return an Observable, you don't want to call subscribe just yet, as doing so would return something that isn't really an Observable.
With that in mind, your code can be rewritten in the following way:
public getEvents(key:string,value:string) : Observable<Array<any>> {
var allEventsObserve : Observable<Array<any>> = this.getHTTPEvents();
return allEventsObserve
.map(events => {
var match = events.filter(event => event[key] == value);
if (match.length == 0) {
throw 'no matching event found';
} else {
return match[0];
}
})
.catch(e => {
console.log(e);
return e;
});
}
Since getEvents returns an Observable, somewhere else in your code you would do something like getEvents().subscribe(events => processEvents()) to interact with them. This code also assumes that this.getHTTPEvents() returns an Observable.
Also, notice that I changed your for loop to a call to filter, which operates on arrays. events in this case is a plain-old JavaScript Array, so the filter that is getting called is not the same filter as the RxJS operator filter.
Related
I have an observable that returns items, I need only one specific item and I want to add some extra fields to it from a http request which is also observable. Code below is how I'm trying to achieve that but it doesn't work. I need all observables to complete to get the full item data in ngOnInit. What am I missing?
ngOnInit() {
myItemsObservable$(this.store, items, items.data)
.map(items => items.find(
item => {
return item.id === id
}
))
.concatMap(item => {
return this.apiService.get(`/items/${item.id}/extradata`).map(extra => ({
...item,
extra
}))
})
.subscribe(item => {
// I expect item to have extra fields here.
this.item = item
})
// this.item here should already be complete.
}
Let's look at a simplified version of your code:
1 function ngOnInit() {
2 myItemsObservable$().subscribe(item => this.item = item);
3 console.log(this.item); // undefined
4 }
You are essentially calling two functions that get executed immediately one after the other.
Line 2 creates a subscription object which initiates the flow of data inside the observable. But... execution is not paused after line 2. So then, Line 3 is executed, before the asynchronous tasks within the observable have been completed. This is why this.item is still undefined on line 3.
Hopefully, you can see why your comment is not correct:
// this.item here should already be complete.
You are passing a function (item => this.item = item) to the subscribe() method that handles emissions from the observable when they occur. This is the place in your code where you actually have the emitted value.
So, if we move the console.log() inside the subscribe, this.item would no longer be undefined:
1 function ngOnInit() {
2 myItemsObservable$().subscribe(item => {
3 this.item = item;
4 console.log(this.item); // not undefined :-)
5 });
6 }
To address the two parts of your question:
How to add extra fields in RxJS observable
You are already doing this. You've used the map and concatMap operators to modify values emitted by the source observable into your desired value.
...and wait for it to complete?
Well, you don't "wait" for it per se. With RxJS, you are defining the behavior of how the data flows. The only place you have access to the actual data is inside the subscribe.
But... instead of subscribing, then copying the data from the observable to another variable, you can simply reference the observable directly in other parts of your code.
Let's break your code up into a few different parts so it's easier to see how we can reference different observable sources without subscribing:
id$ = this.route.paramMap.pipe( // This could come from a form control input
params => params.get('id') // or some other observable source.
);
allItems$ = myItemsObservable$(this.store, items, items.data);
getItem$(id) {
return this.allItems$.pipe(
map(items => items.find(i.id === id))
);
}
getExtraData$(id) {
return this.apiService.get(`/items/${id}/extradata`);
}
item$ = this.id$.pipe(
switchMap(id => getItem$(id)),
switchMap(item => this.getExtraData(item.id).pipe(
map(extra => ({ ...item, ...extra }))
))
);
}
See how the definition of item$ starts with the id$? This means that whenever id$ emits a new value, item$ will automatically call getItem$(), then getExtraData() then emit this new item. We didn't need to subscribe to make that happen.
We can simply define an observable to start with another observable then .pipe() the emissions and transform them to suit our needs.
We've essentially designed an observable that will emit any time that item in the store changes, or whenever our selected id$ emits a new value. In a sense, we've built up item$ to represent our item and it will always be up to date, including having its "extra data" appended. This is very powerful. Now we can just use it.
Notice the definition of item$ doesn't need to be in ngOnInit; it can actually go directly on your component.
It's true we could subscribe in our component... but we can usually just use the AsyncPipe in the template:
<div *ngIf="item$ | async as item">
<h1>{{ item.name }}</h1>
<ul>
<li>{{ item.description }}</li>
<li>{{ item.someProperty }}</li>
</ul>
</div>
If you find yourself often subscribing in your component, only to copy the data to a local variable, just to be consumed by your template; I would encourage you to pause and ask yourself it its really necessary. Most of the time you can define an observable that emits exactly the data your view needs without ever needing to subscribe.
RxJS provides many operators and static functions that make it easy to create observables with a variety of common behaviors.
Are you using an old version of angular and rxjs? In the current way, map is not a method of observables.
Instead, I think you would be looking for something like this;
function ngOnInit() {
myItemsObservable$(this.store, items, items.data)
.pipe(
filter(item => item.id === id), // assuming your observable emits each item separately. Otherwise, use your map statement above.
take(1), // do you need this? I'm guessing you wouldn't.
concatMap(item => this.apiService.get(`,/items/${item.id}/extradata`)
.pipe(
map(extra => Object.assign({}, item, {extra})) // if I understand what you are wanting here, otherwise replace "{extra}" with "extra", or go back to your original notation
)
)
)
.subscribe(
item => {
// I expect item to have extra fields here.
this.item = item
}
)
// this.item here should already be complete.
}
Given this method:
public logIn(data:any): Observable<any> {
this.http.get('https://api.myapp.com/csrf-cookie').subscribe(() => {
return this.http.post('https://api.myapp.com/login', data);
});
}
I would like it to return that nested observable, so that my calling code can use it like so:
this.apiService.logIn(credentials).subscribe(() => {
// redirect user to their dashboard
});
without needing to know about the first /csrf-cookie request. Obviously the above doesn't work - but I'm struggling to understand how to make the inner HTTP request wait for the outer one to finish AND be returned by the method.
you should use switchMap see the documentation on switch map
public logIn(data:any): Observable<any> {
return this.http.get('https://api.myapp.com/csrf-cookie').pipe(
switchMap(x => this.http.post('https://api.myapp.com/login', data))
);
}
with rxjs nested subscribes are generally not a good idea. There are many great operators within the library that will get you around it. In this case above where one call depends on another switchMap(...) is the best fit.
Also the code has been modified to return the observable not the subscription
I am currently struggling to wrap my head around angular (2+), the HttpClient and Observables.
I come from a promise async/await background, and what I would like to achieve in angular, is the equivalent of:
//(...) Some boilerplate to showcase how to avoid callback hell with promises and async/await
async function getDataFromRemoteServer() {
this.result = await httpGet(`/api/point/id`);
this.dependentKey = someComplexSyncTransformation(this.result);
this.dependentResult = await httpGet(`/api/point/id/dependent/keys/${this.dependentKey}`);
this.deeplyNestedResult = await httpGet(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`);
}
The best I could come op with in angular is:
import { HttpClient } from `#angular/common/http`;
//(...) boilerplate to set component up.
constructor(private http: HttpClient) {}
// somewhere in a component.
getDataFromRemoteServer() {
this.http.get(`/api/point/id`).subscribe( result => {
this.result = result;
this.dependentKey = someComplexSyncTransformation(this.result);
this.http.get(`/api/point/id/dependent/keys/${this.dependentKey}`).subscribe( dependentResult => {
this.dependentResult = dependentResult;
this.http.get(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`).subscribe( deeplyNestedResult => {
this.deeplyNestedResult = deeplyNestedResult;
});
})
});
}
//...
As you might have noticed, I am entering the Pyramid of Doom with this approach, which I would like to avoid.
So how could I write the angular snippet in a way as to avoid this?
Thx!
Ps: I am aware of the fact that you can call .toPromise on the result of the .get call.
But let's just assume I want to go the total Observable way, for now.
When working with observables, you won't call subscribe very often. Instead, you'll use the various operators to combine observables together, forming a pipeline of operations.
To take the output of one observable and turn it into another, the basic operator is map. This is similar to how you can .map an array to produce another array. For a simple example, here's doubling all the values of an observable:
const myObservable = of(1, 2, 3).pipe(
map(val => val * 2)
);
// myObservable is an observable which will emit 2, 4, 6
Mapping is also what you do to take an observable for one http request, and then make another http request. However, we will need one additional piece, so the following code is not quite right:
const myObservable = http.get('someUrl').pipe(
map(result => http.get('someOtherUrl?id=' + result.id)
)
The problem with this code is that it creates an observable that spits out other observables. A 2-dimensional observable if you like. We need to flatten this down so that we have an observable that spits out the results of the second http.get. There are a few different ways to do the flattening, depending on what order we want the results to be in if multiple observables are emitting multiple values. This is not much of an issue in your case since each of these http observables will only emit one item. But for reference, here are the options:
mergeMap will let all the observables run in whatever order, and outputs in whatever order the values arrive. This has its uses, but can also result in race conditions
switchMap will switch to the latest observable, and cancel old ones that may be in progress. This can eliminate race conditions and ensure you have only the latest data.
concatMap will finish the entirety of the first observable before moving on to the second. This can also eliminate race conditions, but won't cancel old work.
Like i said, it doesn't matter much in your case, but i'd recommend using switchMap. So my little example above would become:
const myObservable = http.get('someUrl').pipe(
switchMap(result => http.get('someOtherUrl?id=' + result.id)
)
Now here's how i can use those tools with your code. In this code example, i'm not saving all the this.result, this.dependentKey, etc:
getDataFromRemoteServer() {
return this.http.get(`/api/point/id`).pipe(
map(result => someComplexSyncTransformation(result)),
switchMap(dependentKey => this.http.get(`/api/point/id/dependent/keys/${dependentKey}`)),
switchMap(dependantResult => this.http.get(`/api/point/id/dependent/keys/${dependentResult.someValue}`)
});
}
// to be used like:
getDataFromRemoteServer()
.subscribe(deeplyNestedResult => {
// do whatever with deeplyNestedResult
});
If its important to you to save those values, then i'd recommend using the tap operator to highlight the fact that you're generating side effects. tap will run some code whenever the observable emits a value, but will not mess with the value:
getDataFromRemoteServer() {
return this.http.get(`/api/point/id`).pipe(
tap(result => this.result = result),
map(result => someComplexSyncTransformation(result)),
tap(dependentKey => this.dependentKey = dependentKey),
// ... etc
});
}
I have three observable sources in my code that emit values of the same type.
const setTitle$ = params$.do(
params => this.titleService.setTitle( `${params[1].appname} - ${this.pagename}` )
).switchMap(
() => Observable.of(true)
);
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
);
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
);
When i use them in race like this
setTitle$.race(
openDocument$,
saveDocument$
).subscribe();
works only setTitle and when i subscribe manually to another two sorces like
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
).subscribe();
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
).subscribe();
then they work too. Help me understand why it's going on and how to force to work all sources in race, merge, etc.
From the documentation, the .race() operator does this:
The observable to emit first is used.
That is why, you will only get ONE emission, because only one out of the three observables that emits first will get emitted.
What you are looking for is .forkJoin() or .combineLatest().
If you want all the observables to execute in parallel and wait for ALL of them to come back as one observables, use .forkJoin():
Observable
.forkJoin([...setTitle$, openDocument$, saveDocument$])
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
//all three observables must be completed. If any of it was not completed, the other 2 observables will wait for it
})
If you however wants to listen to every emission of all the observables regardless when they are emitted, use .combineLatest():
Observable
.combineLatest(setTitle$, openDocument$, saveDocument$)
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
// as long as any of the observables completed, it will be emitted here.
});
Problem was with shared params source.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).share();
I have shared it with share operator. But in this article from the first comment to my question i found this:
When using multiple async pipes on streams with default values, the .share() operator might cause problems:
The share() will publish the first value of the stream on the first subscription. The first async pipe will trigger that subscription and get that initial value. The second async pipe however will subscribe after that value has already been emitted and therefore miss that value.
The solution for this problem is the .shareReplay(1) operator, which will keep track of the previous value of the stream. That way all the async pipes will get the last value.
I replaced share() with shareReplay(1) and all sources began emitting values.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).shareReplay(1);
Thanks to everyone for help!
Just as the title says, in Angular 2, is there any way to check if source is already subscribed? Because I have to check it before using
this.subscription.unsubscribe();
This is my code:
this.Source = Rx.Observable.timer(startTime, 60000).timeInterval().pluck('interval');
this.Subscription = this.Source
.subscribe(data => { something }
and then I want to be sure that it is subscribed before calling unsubscribe()
It seems you can check whether the subscription is closed with
this.subscription.closed
indicate whether this Subscription has already been unsubscribed.
I had a similar case, I had a if condition with one optional subscribe:
if (languageIsSet) {
// do things...
} else {
this.langSub = this.langService.subscribe(lang => {
// do things...
});
}
If you want to call unsubscribe safely (you don't know if is subscribed or not), just initialize the subscription with EMPTY instance:
private langSub: Subscription = Subscription.EMPTY;
Now you can unsubscribe without errors.
You can check if Subject has observers because it has a public property observers.
With Observables you can't because they don't typically have arrays of observers. Only if you've multicasted them via a Subject with the multicast() operator for example.
Maybe if you could describe your use case in more detail I'll be able to give you better advice.
const source = ...;
let subscribed = false;
Rx.Observable.defer(() => {
subscribed = true;
return source.finally(() => { subscribed = false });
})
As I can see in your code you always create a subscription.
So if you created subscriptions object it means subscription exists and you can unsubscribe.
It Still a bit not clear why you need to check is any subsection exist
Btw. unsubscribe() method checking is subscription closed or not.
Subscription is closed if somebody called unsubscribe() or observable is compleated