I have a UI like this:
Where I can
Enter something in search box
Drag users from table to chart
The logic required is:
Initially chart shows some subset of all users (e.g., first 10)
When users are dragged on chart they are added to the users that already there
When filter is applied all users are removed from chart and then it is repopulated with some subset of matching users
I am trying to implement such logic with RxJs.
I have filteredUsers$ and addedUsers$ stream that produce users matching filter and dragged users correspondingly.
I need to combine them in such way:
Observable
.<OPERATOR>(filteredUsers$, addedUsers$)
.subscribe(([filteredUsers, addedUsers]) => {
// When filteredUsers$ fires:
// filteredUsers is value from stream
// addedUsers == null
// When addedUsers$ fires:
// filteredUsers is latest available value
// addedUsers is value from stream
redrawChart(/* combining users */)
});
Any ideas how I can achieve this?
Time sequence:
Filtered: - a - - - - a - ->
Added : - - b - b - - - ->
Result : - a ab - ab - a - ->
If you want the final stream to be populated only when addUsers$ fires with latest from could be a solution:
So, in your case addUsers$ could be the first stream.
You can try out the following code:
let firstObservable$ = Observable.from([1, 2, 3, 4])
.zip(Observable.interval(50), (a, b) => {
return a;
});
let secondObservable$ = Observable.from([5, 6])
.zip(
Observable.interval(70), (a, b) => {
return a;
});
firstObservable$
.withLatestFrom(secondObservable$, (f, s) => ({ a: f, b: s }))
.subscribe(x => {
console.log('result: ', x);
});
The first observable emits every 50 ms a value from the array.
The second observable every 75 ms.
The values printed are {a: 2, b: 5} {a: 3, b: 6} {a: 4, b: 6}
Because 1 was emitted before 5 we lose the pair (1,5)!
I am not clear but missing a pair from addUsers$ if the other stream has not emitted may be non-desired behavior for you.
You could overcome that if you start the second stream with an initial value and then filter out any results you don't want.
You have the combineLatest operator which basically does what you are describing. It combines two observables and gives you the latest value of both streams.
So:
--a--b-----c---
-x-----d-----p-
-combineLatest-
--a--b-b---c-c
x x d d p
This should allow you to do what you want if I understand correctly.
Here's the official doc link:
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/combinelatest.md
Eventually I have done it by adding additional subject:
var filteredUsers$ = ... // users from filter
var addedUsers$ = ... // users dragged on chart
var usersToDraw$ = new Subject();
subscriptions:
usersToDraw$
.subscribe(usersToDraw => {
redrawChart(usersToDraw);
});
filteredUsers$
.subscribe(filteredUsers => {
usersToDraw$.next(filteredUsers);
});
Observable
.combineLatest(filteredUsers$, addedUsers$)
.filter(([filteredUsers, addedUsers]) => addedUsers != null)
.subscribe(([filteredUsers, addedUsers]) => {
// we 'clear' stream so the same users won't be added twice
addedUsers$.next(null);
usersToDraw$.next(/* combining users */);
});
UPDATE
The solution can be improved with withLatestFrom (thanks #nova)
usersToDraw$
.subscribe(usersToDraw => {
redrawChart(usersToDraw);
});
filteredUsers$
.subscribe(filteredUsers => {
usersToDraw$.next(filteredUsers);
});
addedUsers$
.withLatestFrom(filteredUsers$)
.subscribe(([addedUsers, filteredUsers]) => {
usersToDraw$.next(/* combining users */);
});
Related
i'm kinda new to rxjs and can't get my head around this problem:
I have two streams:
one with incoming objects
---a----b----c----d----->
one with the selected object from a list
-------------------c---->
From the incoming objects stream make a stream of the list of objects (with scan operator)
incoming: ----a--------b-------c----------d----------------\>
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]---------\>
When a list object is selected (n), start a new stream
the first value of the new stream is the last value of the list sliced ( list.slice(n))
incoming: ----a--------b-------c----------d--------------------e-------->
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]--------->
selected object: ---------------------------------c------->
new stream of list: ------[c,d]-----[c,d,e]--->
i can't get the last value of the list stream when the object is selected,,,
made a marble diagram for better understanding,
selectedObject$ = new BehaviorSubject(0);
incomingObjects$ = new Subject();
list$ = incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
newList$ = selectedObject$.pipe(
withLastFrom(list$),
switchMap(([index,list])=> incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, list.slice(index))
))
)
A common pattern I use along with the scan operator is passing reducer functions instead of values to scan so that the current value can be used in the update operation. In this case you can link the two observables with a merge operator and map their values to functions that are appropriate - either adding to a list, or slicing the list after a selection.
// these are just timers for demonstration, any observable should be fine.
const incoming$ = timer(1000, 1000).pipe(map(x => String.fromCharCode(x + 65)), take(10));
const selected$ = timer(3000, 3000).pipe(map(x => String.fromCharCode(x * 2 + 66)), take(2));
merge(
incoming$.pipe(map(x => (s) => [...s, x])), // append to list
selected$.pipe(map(x => (s) => { // slice list starting from selection
const index = s.indexOf(x);
return (index !== -1) ? s.slice(index) : s;
}))
).pipe(
scan((list, reducer) => reducer(list), []) // run reducer
).subscribe(x => console.log(x)); // display list state as demonstration.
If I understand the problem right, you could follow the following approach.
The key point is to recognize that the list Observable (i.e. the Observable obtained with the use of scan) should be an hot Observable, i.e. an Observable that notifies independent on whether or not it is subscribed. The reason is that each new stream you want to create should have always the same source Observable as its upstream.
Then, as you already hint, the act of selecting a value should be modeled with a BehaviorSubject.
As soon as the select BehaviorSubject notifies a value selected, the previous stream has to complete and a new one has to be subscribed. This is the job of switchMap.
The rest is to slice the arrays of numbers in the right way.
This is the complete code of this approach
const selectedObject$ = new BehaviorSubject(1);
const incomingObjects$ = interval(1000).pipe(take(10));
const incomingObjectsHot$ = new ReplaySubject<number[]>(1);
incomingObjects$
.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
.subscribe(incomingObjectsHot$);
selectedObject$
.pipe(
switchMap((selected) =>
incomingObjectsHot$.pipe(
map((nums) => {
const selIndex = nums.indexOf(selected);
if (selIndex > 0) {
return nums.slice(selIndex);
}
})
)
),
filter(v => !!v)
)
.subscribe(console.log);
An example can be seen in this stackblitz.
I have a kind of web form which has 2 parts (master and optional child) and want to save the form values
A first call is for master data. The call result is used as a parameter for second call. But in some cases we don't need to save child part.
In the end of saving in the subscribe section I want to show "Success" message with one field from the first call result. I mean we can have one or two calls but show the success message when both of them completed
Master Data Call -> Child Data Call (optional) -> Success message
How to implement the behaviour using RxJS in a proper way?
Addition:
Based on comments I created an example here:
https://rxjs.rxplayground.com/
var master = from([1,2])
var child = of(10)
master.pipe(concatMap(m=>(m==1)?child:EMPTY)).subscribe(res=>console.log(res))
(shows 10 but should be 1,2)
Try to change "master" value. In any case log output should match the master. Also it would be nice to avoid "of(m)" inside of concatMap call
One more addition:
I tried this but not sure that it is the best solution
var master = from([1,2]);
var child = of("*");
master.pipe(
mergeMap(m => (m === 1 ? child.pipe(mapTo(m)) : of(m)))
).subscribe(console.log);
(shows 1,2)
I was saying this,just check once
import { from, of } from 'rxjs';
import { concatMap } from 'rxjs/operators';
const master$ = from([1, 2, 3, 4]);
const child$ = of(20);
// const child$ = () => console.log("child called");
master$
.pipe(
concatMap((masterData) => {
if (masterData % 2 === 0) {
child$;
// child$(); // if you want to check,child is being called or not
}
return of(masterData);
})
)
.subscribe(console.log);
This solution does exactly what I need
var master = from([1,2]);
var child = of("*");
master.pipe(
mergeMap(m => (m === 1 ? child.pipe(mapTo(m)) : of(m)))
).subscribe(console.log);
(shows 1,2)
I have two observable streams, each emitting a series of items over an infinite period of time (similar to how a DOM-based click Observable would behave). I know an item from Observable A (a$) will match an item from Observable B (b$), but need to do some custom logic to determine which items match.
I tried to make this work, but I could only get the first pair to match, and then subsequent items never emit again...
This is an extract from the code:
a$.pipe(
mergeMap(a => {
return b$.pipe(
filter(b => b.key.includes(a.subKey)), // custom matching logic goes here
take(1),
map(b => ({ a, b }))
);
})
)
.subscribe(({ a, b }) => {
console.log("do something with a and b", a, b);
});
Note that both Observables never complete, so if some item a from a$ emitted, its "pair" might not have been emitted from b yet. That's why I used filter and not find above. When I did find a matching item, I can complete the inner observable, since that pair has been matched & handled.
Please advise, what am I missing?
Have you looked at combine latest? It emits the latest value for both streams once both have emitted once.
combineLatest(a$, b$).pipe(filter(([a, b]) => b.key.includes(a.subKey)))
.subscribe(([a, b]) => {
// Do stuff with an and b here
});
I think one way to solve this is to first accumulate the values of each observable in a map, then to use the combineLatest operator so you can check for pairs on every emission.
const aMap$ = a$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
const bMap$ = b$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
combineLatest(aMap$, bMap$)
.pipe(
map(([aMap, bMap]) => {
let pair = null;
for (const bKey in bMap) {
const bVal = bMap[bKey];
const aPairKey = bVal.keys.find(k => !!aMap[k]);
if (aPairKey) {
pair = { a: aMap[aPairKey], b: bVal };
delete aMap[aPairKey];
delete bMap[bKey];
break;
}
}
return pair;
}),
filter(v => !!v)
)
I would accumulate the values from A and B to see which values happened yet. After you can create an intersection from those arrays
const keysOccuredInA$ = a$.pipe(
map(a => a.subKey),
scan((acc, curr) => ([...acc, curr]), []),
);
const keysOccuredInB$ = b$.pipe(
map(b => b.key), // b.key is an array, right?
scan((acc, curr) => ([...acc, ...curr]), []),
);
keysOccuredInBoth$ = combineLatest(keysOccuredInA$, keysOccuredInB$).pipe(
map(([keysOccuredInA, keysOccuredInB]) => _intersection(keysOccuredInA, keysOccuredInB)), // lodash intersection
)
I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';
JSBIN Sample
I have a changeable set of child components (POJO object) that each have its own state stream. Each time a user triggers addChild/removeChild/clearChildren, a new set of children state streams is emitted with #switchMap. So far so good! (And so amazed by RxJS!)
With Rx.Observable.from(arrayOfStateStreams).combineAll() I get a good result as long as the arrayOfStateStreams isn't an empty array.
Since this is a partial state that is combined(Latest) on a higher level, I need to get an empty array emitted or the global state tree will contain old state data that is no longer true!
I can emit some reserved token like ['EMPTY-ARRAY-PLACEHOLDER-TOKEN'], but that's just weird.
A better way would be to always append one last stream into the array so the last index can be considered trash. Still confusing code and state though.
Using [null] is not OK, since we could have a child state of 'null'.
Anyone who can solve this in a good way? Can't this be supported since there should be no other representation of an empty array after #combineAll?
Credits go to github user trxcllnt who provided the following answer:
combineAll won't emit unless the combined Observables emit at least
one value, but you could check to ensure the collection you're
combining is empty or not, and either combine or emit an empty Array:
var arrayOfStreamsStream = Rx.Observable
.of(
[], [
Rx.Observable.of('blah-1'), // component state.
Rx.Observable.of('blah-2'),
Rx.Observable.of('blah-3')
], [], [
Rx.Observable.of('foo-1'),
Rx.Observable.of('qux-2')
]
)
.switchMap(function onMap(coll) {
return coll.length === 0 ?
Observable.of(coll) :
Observable.combineLatest(...coll);
})
.subscribe(function onSubscribe(data) {
console.log('onSubscribe START')
console.dir(data)
console.log('onSubscribe END')
})
This has nothing to do with combineAll. The problem is that Observable.from results in nothing (an empty observable) when passed an empty array.
The only viable solution that I can think of if you have to get a result from an empty array is to return something else in that case.
Ann example to illustrate the problem and a possible solution.
var data = [1, 2, 3, 4, 5];
log('With data: ');
Rx.Observable.from(data)
.subscribe(function (d) { log('data: ' + d); });
// Prints:
// With data:
// data: 1
// data: 2
// data: 3
// data: 4
// data: 5
var data = [];
log('Without data: ');
var nullDataObject = { msg: 'my null data object' };
Rx.Observable.from(data.length == 0 ? [nullDataObject] : data)
.subscribe(function (d) { log('data: ' + d); });
// Prints:
// With data:
// data: [object Object]
Runnable example on jsfiddle.
When consuming this you simply filter away the object representing an empty array where appropriate.
a possible workaround is to just pipe it with startWith();
combineLatest(potentiallyEmptyArray).pipe(
startWith<any>([])
);
Note: Similar issues exist with combineLatest() (the static version) which can be solved using defaultIfEmpty() - which works, but it screws up the typing of the output.
// array of Observables
const animals: Observable<{ species: 'dog' | 'cat' }>[] = [];
// Type '{ species: "dog" | "cat"; }[]' is not assignable to type 'never[]'.
combineLatest(animals).pipe(defaultIfEmpty([]));
In TypeScript you need to either know the type of the object or use <any>[] which means you then lose typing completely.
If you have a concrete type you can use one of these:
defaultIfEmpty<Animal[]>([])
defaultIfEmpty([] as Animal[])
I often don't have a concrete type for the return value of an observable. So I came up with an operator:
export const emptyArrayIfEmpty = () => <T>(observable: Observable<T[]>) =>
observable.pipe(defaultIfEmpty([] as T[]));
Then I can add the following and get out an empty array if animals === [] without losing any typing information:
combineLatest(animals).pipe(emptyArrayIfEmpty());