I'm using Angular 10 and trying to collect window:keyup events for a certain amount of time using RXJS - but unfortunately not very successful at all. Basically I want to input data and if no key is pressed for a certain amount of time, the request should be submitted.
fromEvent(window, 'keyup').pipe(
map((ev: KeyboardEvent) => ev.key),
scan((acc, value) => acc + value),
debounceTime(500)
).subscribe(key => {
console.log(key);
});
That's how my basic approach looks like, which is basically doing what I want, but I'm unable to delete the scan result within the subscription result.
Is there a better way to achieve my desired behavior?
One approach I have found is to use a Subject alongside, and have it emit the results of an Observable which sets itself up and destroys itself for each produced string:
const result$ = new Subject<string>();
fromEvent(window, 'keyup').pipe(
exhaustMap(key => fromEvent(window, 'keyup').pipe(
takeUntil(result$),
startWith(key),
map((event: KeyboardEvent) => event.key),
scan((acc, curr) => acc + curr),
debounceTime(500),
tap(val => result$.next(val)),
)),
).subscribe();
result$.subscribe(console.log); // here you would do your request logic,
// and make sure to set it to unsubscribe
// (with a takeUntil and a destroy$ Subject, for example).
When a first key is pressed, it sets up the outer Observable. It then creates the inner Observable, watching every key pressed until the debounce time of 500ms passes, at which point it causes the Subject to emit. The emission of that very same Subject then kills the inner Observable, allowing the outer Observable to start up again. Don't forget to use an unsubscription method on BOTH the Subject and the outer Observable.
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.
}
I'm trying to read files by using bindNodeCallback and fs readdir, stat.
Here's my code:
import { readdir, stat, Stats } from "fs";
import { bindNodeCallback, Observable, of } from "rxjs";
import { catchError, filter, flatMap, map, mergeMap, switchMap, tap } from 'rxjs/operators';
const readDirBindCallback: (path: string) => Observable<string[]> = bindNodeCallback(readdir);
const fileStateBindCallback: (path: string) => Observable<Stats> = bindNodeCallback(stat);
readDirBindCallback('/Users/.../WebstormProjects/copy')
.pipe(
flatMap(x => x),
tap(console.log),
switchMap(status => {
console.log(status);
return fileStateBindCallback('/Users/.../WebstormProjects/copy/' + status);
})
)
.subscribe(result => {
console.log(result)
});
"switchMap" has been called multiple times correctly.
The problem is fileStateBindCallback has been called only once, the final subscibe logged only once.
Why is this happened?
If I subscribe the fileStateBindCallback manually in switchMap block. It would run as I expect. But that is not a good pratice and not fit in my request.
I think the problem is the switchMap operator.
switchMap can only have one active inner observable at a time. If an outer values comes in and there is an active inner obs., it will be unsubscribed and a new one will be created based on the newly arrived value and the provided function to switchMap.
readdir will return an array.
flatMap(arr => arr) will simply emit the array's items separately and synchronously.
Let's assume your directory has N items.
flatMap will emits these items one by one.
So item 1 is passed along and switchMap will create an inner obs (fileStateBindCallback), which involves asynchronous operation.
Then, item 2 is sent, but as this happens synchronously, the current inner observable(that one created due to item 1) handled by switchMap will be unsubscribed and a new one will be created for item 2.
And so forth, until item N finally arrives. Remember that the array's items are emitted synchronously. The item N-1's inner obs. will be unsubscribed and a new one will be created for item N. But since N is the last item in the array, it can't be interrupted by any subsequent emissions, meaning that its inner observable will have the time to emit & complete.
What you might be looking for is concatMap.
When Nth item arrives, it will wait for the current inner observable created due to the N-1th item to complete, before creating a new one based on the Nth item.
So, replacing switchMap with concatMap should do the job. If the order is not important, you can opt for mergeMap.
I'm facing a problem, and I've been trying to find a solution using RxJs, but can't seem to find one that fits it...
I have 3 different REST requests, that will be called sequentially, and each of them needs the response of the previous one as an argument
I want to implement a progress bar, which increments as the requests succeed
Here is what I thought :
I am going to use pipes and concatMap() to avoid nested subscriptions and subscribe to each request when the previous one is done.
Consider this very simplified version. Assume that each of represents a whole REST successful request (will handle errors later), and that I will do unshown work with the n parameter...
const request1 = of('success 1').pipe(
delay(500),
tap(n => console.log('received ' + n)),
);
const request2 = (n) => of('success 2').pipe(
delay(1000),
tap(n => console.log('received ' + n))
);
const request3 = (n) => of('success 3').pipe(
delay(400),
tap(n => console.log('received ' + n))
);
request1.pipe(
concatMap(n => request2(n).pipe(
concatMap(n => request3(n))
))
)
However, when I subscribe to the last piece of code, I will only get the response of the last request, which is expected as the pipe resolves to that.
So with concatMap(), I can chain my dependent REST calls correctly, but can't follow the progress.
Though I could follow the progress quite easily with nested subscriptions, but I am trying hard to avoid this and use the best practice way.
How can I chain my dependent REST calls, but still be able to do stuff each time a call succeeds ?
This is a generalized solution, though not as simple. But it does make progress observable while still avoiding the share operator, which can introduce unexpected statefulness if used incorrectly.
const chainRequests = (firstRequestFn, ...otherRequestFns) => (
initialParams
) => {
return otherRequestFns.reduce(
(chain, nextRequestFn) =>
chain.pipe(op.concatMap((response) => nextRequestFn(response))),
firstRequestFn(initialParams)
);
};
chainRequests takes a variable number of functions and returns a function that accepts initial parameters and returns an observable that concatMaps the functions together as shown manually in the question. It does this by reducing each function into an accumulation value that happens to be an observable.
Remember, RxJS leads us out of callback hell if we know the path.
const chainRequestsWithProgress = (...requestFns) => (initialParams) => {
const progress$ = new Rx.BehaviorSubject(0);
const wrappedFns = requestFns.map((fn, i) => (...args) =>
fn(...args).pipe(op.tap(() => progress$.next((i + 1) / requestFns.length)))
);
const chain$ = Rx.defer(() => {
progress$.next(0);
return chainRequests(...wrappedFns)(initialParams);
});
return [chain$, progress$];
};
chainRequestsWithProgress returns two observables - the one that eventually emits the last response, and one that emits progress values when the first observable is subscribed to. We do this by creating a BehaviorSubject to serve as our stream of progress values, and wrapping each of our request functions to return the same observable they normally would, but we also pipe it to tap so it can push a new progress value to the BehaviorSubject.
The progress is zeroed out upon each subscription to the first observable.
If you wanted to return a single observable that produced the progress state as well as the eventual result value, you could have chainRequestsWithProgress instead return:
chain$.pipe(
op.startWith(null),
op.combineLatest(progress$, (result, progress) => ({ result, progress }))
)
and you'll have an observable that emits an object representing the progress toward the eventual result, then that result itself. Food for thought - does progress$ have to emit just numbers?
Caveat
This assumes request observables emit exactly one value.
The simplest solution would be to have a progress counter variable that is updated from a tap when each response comes back.
let progressCounter = 0;
request1.pipe(
tap(_ => progressCounter = 0.33),
concatMap(n => request2(n).pipe(
tap(_ => progressCounter = 0.66),
concatMap(n => request3(n)
.pipe(tap(_ => progressCounter = 1)))
))
);
If you want the progress itself to be observable then you want to share the request observables as to not make duplicate requests) and then combine them to get the progress.
An example of how you may want to approach that can be found at: https://www.learnrxjs.io/recipes/progressbar.html
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!
I would like to to something like:
this._myService.doSomething().subscribe(result => {
doSomething()
});
.then( () => dosthelse() )
.then( () => dosanotherthing() )
So I would like to chain .then like in promise. How would I do that in Rxjs?
this._myService.getLoginScreen().subscribe( result => {
window.location.href = MyService.LOGIN_URL;
/// I would like to wait for the site to load and alert something from the url, when I do it here it alerts the old one
});
.then (alert(anotherService.partOfTheUrl())
getLoginScreen() {
return this.http.get(myService.LOGIN_URL)
.flatMap(result => this.changeBrowserUrl())
.subscribe( result => //i want to do sth when the page is loaded//);
}
changeBrowserUrl(): Observable<any> {
return Observable.create( observer => {
window.location.href = myService.LOGIN_URL;
observer.next();
});
}
The equivalent of then for observables would be flatMap. You can see some examples of use here :
RxJS Promise Composition (passing data)
Why we need to use flatMap?
RxJS sequence equvalent to promise.then()?
For your example, you could do something like :
this._myService.doSomething()
.flatMap(function(x){return functionReturningObservableOrPromise(x)})
.flatMap(...ad infinitum)
.subscribe(...final processing)
Pay attention to the types of what your functions return, as to chain observables with flatMap you will need to return a promise or an observable.
If dosthelse or dosanotherthing returns a raw value, the operator to use is map. If it's an observable, the operator is flatMap (or equivalent).
If you want to do something imperatively. I mean outside the asynchronous processing chain, you could leverage the do operator.
Assuming that dosthelse returns an observable and dosanotherthing a raw object, your code would be:
this._myService.doSomething()
.do(result => {
doSomething();
})
.flatMap( () => dosthelse() )
.map( () => dosanotherthing() );
Notice that if you return the return of the subcribe method, it will correspond to a subscription object and not an observable. A subscription object is mainly for being able to cancel the observable and can't take part of the asynchronous processing chain.
In fact, most of the time, you subscribe at the end of the chain.
So I would refactor your code this way:
this._myService.getLoginScreen().subscribe( result => {
window.location.href = MyService.LOGIN_URL;
/// I would like to wait for the site to load and alert something from the url, when I do it here it alerts the old one
alert(anotherService.partOfTheUrl()
});
getLoginScreen() {
return this.http.get(myService.LOGIN_URL)
.flatMap(result => this.changeBrowserUrl())
.do( result => //i want to do sth when the page is loaded//);
}
changeBrowserUrl(): Observable<any> {
return Observable.create( observer => {
window.location.href = myService.LOGIN_URL;
observer.next();
});
}
Updated rxjs solution
Rxjs has changed quite a bit since this was answered.
flatMap is now mergeMap
Or switchMap, they're mostly interchangeable but it's good to know the difference
.do() is now tap()
Chaining is now done inside of a .pipe(). All manipulation should be done inside this pipe
You can chain pipes if needed (Ex. one variable maps an array of Users. Another variable takes that first variable and maps it a second time)
Do something after the original call has been made
Scenario
Make an HTTP call (Ex. Authentication check)
When that call has finished, navigate to another page
this._myService.getAuthenticated()
.pipe(
tap(result => this._myService.navigateToHome())
)
.subscribe()
Chain multiple calls
Scenario
Make an HTTP call (Ex. Authentication check)
Make a 2nd call to pull more info
Navigate after both calls have finished
this._myService.getAuthenticated()
.pipe(
// The Authentication call returns an object with the User Id
switchMap(user => this._myService.getUserInfo(user.id))
// After the user has been loaded, navigate
tap(user => this._myService.navigateToHome())
)
.subscribe()
Note on the above examples: I am assuming these calls are HTTP which unsubscribe after being called once. If you use a live observable (ex. a stream of Users), make sure you either unsubscribe or use takeUntil/first operators.
Example for Clarification (April, 2022)
The top of this pipe can emit n values (this means the chain will be called everytime a new value enters into the top of the pipe). In this example, n equals 3. This is a key difference between observables and promises. Observables can emit multiple values over time, but a promise cannot.
The subsequent chained streams emit one value (hence mimicing promises).
// Emit three values into the top of this pipe.
const topOfPipe = of<string>('chaining', 'some', 'observables');
// If any of the chained observables emit more than 1 value
// then don't use this unless you understand what is going to happen.
const firstObservable = of(1);
const secondObservable = of(2);
const thirdObservable = of(3);
const fourthObservable = of(4);
const addToPreviousStream = (previous) => map(current => previous + current);
const first = (one) => firstObservable.pipe(addToPreviousStream(one));
const second = (two) => secondObservable.pipe(addToPreviousStream(two));
const third = (three) => thirdObservable.pipe(addToPreviousStream(three));
const fourth = (four) => fourthObservable.pipe(addToPreviousStream(four));
// Pipeline of mergeMap operators, used for chaining steams together.
topOfPipe.pipe(
mergeMap(first),
mergeMap(second),
mergeMap(third),
mergeMap(fourth),
).subscribe(console.log);
// Output: chaining1234 some1234 observables1234
You could also use concatMap or switchMap. They all have subtle differences. See rxjs docs to understand.
mergeMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/mergemap
concatMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/concatmap
switchMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/switchmap