I have an observable like this.
Execution can be checked here.
https://codesandbox.io
import { from, merge , } from 'rxjs';
import { filter, tap, share } from 'rxjs/operators';
const nextTrain$ =
from(['Yellow', 'Green', 'Neon', 'Amber'])
.pipe(share());
const yellowTrain$ =
nextTrain$
.pipe(
filter(color => color === 'Yellow'),
tap(() => console.log(`Yellow Color Train is comming`))
)
const greenTrain$ = nextTrain$
.pipe(
filter((color) => color === 'Green'),
tap(() => console.log(`Green Color Train is comming`))
)
const blueTrain$ = nextTrain$
.pipe(
filter((color) => color === 'Blue'),
tap(() => console.log(`Blue Color Train is comming`))
)
merge(yellowTrain$,
greenTrain$,
blueTrain$).subscribe();
Currently when I susbscribe to the merge statement what gets printed out is
Yellow Color Train is comming
Green Color Train is comming
Whereas what i want is to print the first matching condition and stop everything else.
So that would mean just print
Yellow Color Train is comming.
How do i do this in rxjs ?
Just add the first operator:
merge(....).pipe(first())
Related
I have implemented a logic in my angular component that will display the version number against the Agreement.pdf like Agreement_v1.pdf as well as set the editable flag to true to any document that is not Identification and also Agreement document that is not v1. As you can see the version number logic is applied in the first tap operator. Unfortunately I think my second tap operator does find such occurrence. Could somebody tell me why ?
var first: number = 1;
if (this.readOnly) {
this.columnsToDisplay = ['category', 'filename', 'uploadedOnDate'];
} else {
this.columnsToDisplay = ['category', 'filename', 'uploadedOnDate', 'action', 'delete'];
}
const investigationDocuments = this.caseChange$
.pipe(
map(investigationsCase => {
this.configureSendNewAgreement(investigationsCase.state.documents);
return investigationsCase.state.documents
? investigationsCase.state.documents.filter(doc => doc.isActive === true)
: [];
}),
tap(a => a.filter(d => d.type === TypeOfDocument.Agreement).sort((x, y) => +new Date(x.uploadedOnDate) - +new Date(y.uploadedOnDate)).forEach(b => b.name = 'Agreement_v' + (first++) +'.pdf')),
tap(a => a.filter(d => d.type !== TypeOfDocument.Identification && d.uploadedBy != null && d.name !== 'Agreement_v1.pdf').forEach(b => b.isEditable = true)),
);
The tap operator doesn't modify the incoming value. So anything you'd do to a within the tap is ignored.
From the docs:
Used when you want to affect outside state with a notification without altering the notification
In other words, only use tap if you want to do some debugging or a side effect that does not change the notification (incoming variable).
Try changing both tap operators to map and see if that helps.
The map allows changing the incoming variable and it emits that changed variable to the next operator in the pipeline.
I did a stackblitz to show the difference:
https://stackblitz.com/edit/angular-tap-vs-map-deborahk
Component
import { Component, VERSION } from '#angular/core';
import { of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular ' + VERSION.major;
result$ = of(SampleData).pipe(
tap(a => a.filter(d => !d.startsWith('t'))),
tap(a => a.filter(d => !d.startsWith('f')))
)
result2$ = of(SampleData).pipe(
map(a => a.filter(d => !d.startsWith('t'))),
map(a => a.filter(d => !d.startsWith('f')))
)
constructor() {
this.result$.subscribe(result => console.log('Result: ', JSON.stringify(result)))
this.result2$.subscribe(result => console.log('Result2: ', JSON.stringify(result)))
}
}
export const SampleData = [
"one",
"two",
"three",
"four",
"five"
]
Result
Result: ["one","two","three","four","five"]
Result2: ["one"]
Notice that the first statement uses tap and the result is unchanged from the original sample data array.
The second statement uses map and the filtered results are propagated through the pipeline and appear in the result.
This is basically what I am after. Just not sure the best way to return the combination of all three in the switchmap.
unsubscribes = combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
})
).subscribe(([apiCall1Res, apiCall2Res, apiCall3Res]) => {
///Do work
})
If apiCall3 should after 1 and 2:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
.pipe(map(apiCall3Res => [apiCall1Res, apiCall2Res, apiCall3Res]));
})
With ... you can save some space here:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(results => apiCall3(apiCall1Res.Id)
.pipe(map(apicallResult3 => [...result, apicallResult3])
)
)
The sequence is correct, you just need to adjust how you treat the return values. When you use switchMap you transform the output of the observable sequence from the type you are receiving to the type of output of the observable you provide on the switchMap return. So you just must create an observable that returns the 3 values. You can do it by mapping the flow of the apiCall3 joining with the other two.
I propose one solution that can be adjusted to match your specific scenario if you need more. I created mock objects in order to make the sample directly executable for testing.
You can see the sample running with mock objects on the following stackblitz I created for you:
Editor: https://stackblitz.com/edit/rxjs-hjyrvn?devtoolsheight=33&file=index.ts
App: https://rxjs-hjyrvn.stackblitz.io
import { combineLatest, of, timer } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
// Mock objects...
const apiCall1 = timer(1).pipe(map(() => ({id: 1})));
const apiCall2 = timer(2).pipe(map(() => 2));
// apiCall3 mock created bellow on the fly...
let r1, r2; // <-- to save partial results because they are cutted from the flow bellow...
const source =
combineLatest(
apiCall1,
apiCall2,
).pipe(
tap(([apiCall1Res, apiCall2Res]) => { r1 = apiCall1Res; r2 = apiCall2Res;}),
map(([apiCall1Res, apiCall2Res]) => apiCall1Res.id), // adjust flow to apiCall3
switchMap((apiCall1ResId) => of(apiCall1ResId).pipe(map(id => id+2))), // <-- apiCall3 mock on the fly
map(apiCall3Res => [r1, r2, apiCall3Res])
);
source.subscribe(console.log);
As you can check on the output you receive the 3 values at the subscription observer code.
How do I access the resultB in the tap operator after it was switchMapped ?
streamA$.pipe(
switchMap(resultA => {
const streamB$ = resultA ? streamB1$ : streamB2$;
return streamB$.pipe( // <- nesting
switchMap(resultB => loadData(resultB)),
tap(data => {
// how do I access resultB here?
})
);
})
);
bonus question:
Is it possible to avoid the nesting here, and chain the whole flow under single pipe?
Please consider the following example:
streamA$.pipe(
switchMap(resultA => resultA ? streamB1$ : streamB2$),
switchMap(resultB => loadData(resultB).pipe(map(x => [resultB, x]))),
tap([resultB, data] => {})
);
This is how you can write your observable to get access of resultB and flat the observable operators chain -
streamA$.pipe(
switchMap(resultA => iif(() => resultA ? streamB1$ : streamB2$),
switchMap(resultB => forkJoin([loadData(resultB), of(resultB)])),
tap(([loadDataResponse, resultB]) => {
//loadDataResponse - This will have response of observable returned by loadData(resultB) method
//resultB - This is resultB
})
);
The basic problem here is that switchMap is being used to transform values to emit a certain shape into the stream. If your resultB value isn't part of that shape, then operators further down the chain won't have access to it, because they only receive the emitted shape.
So, there are basically 2 options:
pass an intermediate shape, that contains your value
use a nested pipe to bring both pieces of data into the same operator scope
The solutions suggested so far involve mapping to an intermediate object. My preference is to use the nested pipe, so the data flowing through the stream is a meaningful shape. But, it really comes down to preference.
Using the nested pipe, your code would look something like this:
streamA$.pipe(
switchMap(resultA => {
const streamB$ = resultA ? streamB1$ : streamB2$;
return streamB$.pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
);
})
);
Note: you can use iif to conditionally choose a source stream:
streamA$.pipe(
switchMap(resultA => iif(()=>resultA, streamB1$, streamB2$).pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
))
);
It can be helpful to break out some of the logic into separate functions:
streamA$.pipe(
switchMap(resultA => doSomeWork(resultA)),
miscOperator1(...),
miscOperator2(...)
);
doSomeWork(result) {
return iif(()=>result, streamB1$, streamB2$).pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
))
}
I have a long chain of operations within a pipe. Sub-parts of this chain represent some sort of high level operation. So, for instance, the code could look something like
firstObservable().pipe(
// FIRST high level operation
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
// SECOND high level operation
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
To improve readability of the code, I can refactor the example above as follows
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
)
}
performSecondOperation() {
return pipe(
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
}
Now, the whole thing works and I personally find the code in the second version more readable. What I loose though is the information that performFirstOperation() returns a parameter, param_2_1, which is then used by performSecondOperation().
Is there any different strategy to break a long pipe chain without actually loosing the information of the parameters passed from sub-pipe to sub-pipe?
setting aside the improper usage of forkJoin here, if you want to preserve that data, you should set things up a little differently:
firstObservable().pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1); // run this sync operation inside
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4])) // add inner map to gather data
);
})
)
this way you've built your second pipeline inside of your higher order operator, so that you can preserve the data from the first set of operations, and gather it with an inner map once the second set of operations has concluded.
for readability concerns, you could do something like what you had:
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
)
}
performSecondOperation() {
return pipe(
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1);
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4]))
);
})
)
}
an alternative solution would involve multiple subscribers:
const pipe1$ = firstObservable().pipe(
performFirstOperation(),
share() // don't repeat this part for all subscribers
);
const pipe2$ = pipe1$.pipe(performSecondOperation());
then you could subscribe to each pipeline independently.
I broke one complex operation into two like this:
Main Code
dataForUser$ = this.userSelectedAction$
.pipe(
// Handle the case of no selection
filter(userName => Boolean(userName)),
// Get the user given the user name
switchMap(userName =>
this.performFirstOperation(userName)
.pipe(
switchMap(user => this.performSecondOperation(user))
))
);
First Operation
// Maps the data to the desired format
performFirstOperation(userName: string): Observable<User> {
return this.http.get<User[]>(`${this.userUrl}?username=${userName}`)
.pipe(
// The query returns an array of users, we only want the first one
map(users => users[0])
);
}
Second Operation
// Merges with the other two streams
performSecondOperation(user: User) {
return forkJoin([
this.http.get<ToDo[]>(`${this.todoUrl}?userId=${user.id}`),
this.http.get<Post[]>(`${this.postUrl}?userId=${user.id}`)
])
.pipe(
// Map the data into the desired format for display
map(([todos, posts]) => ({
name: user.name,
todos: todos,
posts: posts
}) as UserData)
);
}
Notice that I used another operator (switchMap in this case), to pass the value from one operator method to another.
I have a blitz here: https://stackblitz.com/edit/angular-rxjs-passdata-deborahk
If my typeahead gets an empty search result, any subsequent query with a norrowed down search query should be prevented. E.g. if the search for 'red' returns empty, a search for 'redcar' makes no sense.
I tried using pairwise() and scan() operator. Code snippet:
import { tap, switchMap, filter, pairwise, scan, map } from 'rxjs/operators';
this.searchForm.get('search').valueChanges
.pipe(
switchMap( queryString => this.backend.search(queryString))
)
.subscribe()
Update
Given a simplified scenario: There is only the term 'apple' in the backend. The user is typing the search string (the request is not aborted by the switchMap()):
'a' -------> backend call returns 'apple'
'ap' ------> backend call returns 'apple'
'app' -----> backend call returns 'apple'
'appl' ----> backend call returns 'apple'
'apple' ---> backend call returns 'apple'
'apple p' -----> backend call returns EMPTY
'apple pi' ----> backend call returns EMPTY
'apple pie' ---> backend call returns EMPTY
The backend calls for 7. and 8. are unnecessary, because 6. already returns EMPTY. Therfore any subsequent call could be omitted. In my opinion some memoization is needed.
I would like to prevent unnecessary backend calls (http). Is there any way to achieve this in rxjs?
This is an interesting use-case and one of a very few situations where mergeScan is useful.
Basically, you want to remember the previous search term and the previous remote call result and based on their combination you'll decide whether you should make another remote call or just return EMPTY.
import { of, EMPTY, Subject, forkJoin } from 'rxjs';
import { mergeScan, tap, filter, map } from 'rxjs/operators';
const source$ = new Subject();
// Returns ['apple'] only when the entire search string is contained inside the word "apple".
// 'apple'.indexOf('app') returns 0
// 'apple'.indexOf('apple ap') returns -1
const makeRemoteCall = (str: string) =>
of('apple'.indexOf(str) === 0 ? ['apple'] : []).pipe(
tap(results => console.log(`remote returns`, results)),
);
source$
.pipe(
tap(value => console.log(`searching "${value}""`)),
mergeScan(([acc, previousValue], value: string) => {
// console.log(acc, previousValue, value);
return (acc === null || acc.length > 0 || previousValue.length > value.length)
? forkJoin([makeRemoteCall(value), of(value)]) // Make remote call and remember the previous search term
: EMPTY;
}, [null, '']),
map(acc => acc[0]), // Get only the array of responses without the previous search term
filter(results => results.length > 0), // Ignore responses that didn't find any results
)
.subscribe(results => console.log('results', results));
source$.next('a');
source$.next('ap');
source$.next('app');
source$.next('appl');
source$.next('apple');
source$.next('apple ');
source$.next('apple p');
source$.next('apple pi');
source$.next('apple pie');
setTimeout(() => source$.next('app'), 3000);
setTimeout(() => source$.next('appl'), 4000);
Live demo: https://stackblitz.com/edit/rxjs-do457
Notice that after searching for "apple " there are no more remote calls. Also, after 3s when you try searching a different term "'app'" it does make a remote call again.
You can use the filter operator:
this.searchForm.get('search').valueChanges.pipe(
filter(query => query)
switchMap(query => this.backend.search(queryString))
)
You can try out this mechanism here: RxJS-Editor
Code-share did not work so you get the code here:
const { of } = Rx;
const { filter } = RxOperators;
of('foo1', 'foo2', undefined, undefined, 'foo3').pipe(
filter(value => value)
)
Sounds like you want to keep all failed searches and check whether current search would fail also if HTTP is called. I cant think of any elegant way of having this in one stream, but with two streams:
_failedStreams = new Subject();
failedStreams$ = _failedStreams.asObservable().pipe(
scan((acc, curr) => [...acc, curr], []),
startWith('')
);
this.searchForm.get('search').valueChanges
.pipe(
withLatestFrom(failedStreams$),
switchMap([queryString, failedQueries] => {
return iif(() => failedQueries.find(failed => failed.startsWith(queryString)) ?
of('Not found') :
callBackend(queryString);
)
}
)
.subscribe()
callBackend(queryString) {
this.backend.search(queryString)).pipe(
.catchError(err => if(error.status===404) {
this._failedStreams.next(queryString);
// do something with error stream, for ex:
throwError(error.status)
}
)
}
Code is not tested, but you get the idea