Is there a better solution of this RxJS epic stream? - javascript

I have a redux state using redux-observable's epics.
I need to solve showing a message after a user deletes an object or more objects.
There are two ways how to delete an object:
by action deleteObject(id: string) which call deleteObjectFulfilled action
by action deleteObjects(ids: Array<string>) which call N * deleteObject(id: string) actions
I want to show only one message with a count of deleted messages after every success "deleting action".
My final solution of this epic is:
export const showDeleteInformationEpic = action$ =>
combineLatest(
action$.pipe(ofType(DELETE_OBJECT_FULFILLED)),
action$.pipe(
ofType(DELETE_OBJECTS),
switchMap(({ meta: { ids } }) =>
action$.pipe(
ofType(DELETE_OBJECT_FULFILLED),
skip(ids.length - 1),
map(() => ids.length),
startWith('BATCH_IN_PROGRESS'),
take(2),
),
),
startWith(1),
),
).pipe(
startWith([null, null]),
pairwise(),
map(([[, previousCount], [, currentCount]]) =>
(previousCount === 'BATCH_IN_PROGRESS')
? currentCount
: isNumber(currentCount) ? 1 : currentCount),
filter(isNumber),
map((count) => throwInfo('objectDeleted', { count })),
);
Can you see any better solution of this?

There is more simple solution if I use only deleteObjects(Array<string>) for both cases..

Instead of firing multiple actions, you can create and dispatch a single action DELETE_MULTIPLE and pass all the id(s) in the payload.
This way your effects will be a lot cleaner since you only have to subscribe to DELETE_MANY action and additionally, it will prevent multiple store dispatches.

Related

Subscribe to observable, async map result with input from dialog, use result from map to route

I am calling an API-service which returns an Observable - containing an array of elements.
apiMethod(input: Input): Observable<ResultElement[]>
From this I have been choosing the first element of the array, subscribing to that. Then used that element to route to another page like this:
this.apiService
.apiMethod(input)
.pipe(map((results) => results[0])
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
This works just fine.
Problem is, I do not want to just use the first element, I want a MatDialog, or other similar to pop up, and give the user option of which element to choose, and THEN route to the correct one.
If the list only contain one element though, the dialog should not show, and the user should be routed immediately.
I have tried to open a dialog in the .pipe(map()) function, but the subscribe() happens before I get answer from the user, causing it to fail. And I am not sure if that even is the correct approach. How would any of you solve this problem?
Edit
Ended up doing partly what #BizzyBob suggested:
Changing map to switchmap in the API-call, making it this way:
this.apiService
.apiMethod(input)
.pipe(switchMap((results) => this.mapToSingle(results)
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
With the mapToSingle(ResultElement[]) being like this:
private mapToSingle(results: ResultElement[]): Observable<ResultElement> {
if (result.length === 1){
return of(results[0]);
}
const dialogConfig = new MatDialogConfig<ResultElement[]>();
dialogConfig.data = results;
const dialogRef = this.dialog.open(ResultDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
I would create a DialogComponent that takes in the list of choices as an input, and emits the chosen item when it's closed.
Then, create a helper method (maybe call it promptUser) that simply returns an observable that emits the selected value:
this.apiService.apiMethod(input)
.pipe(
switchMap(results => results.length > 1
? this.promptUser(results)
: of(results[0])
)
)
.subscribe(
result => this.router.navigate('elements/', result.id)
);
Here we simply use switchMap to return an observable that emits the proper item. If the length is greater than 1, we return the helper method that displays the dialog and emits the chosen item, else just emit the first (only) item. Notice that we wrapped plain value with of since within switchMap, we need to return observable.
In either case, the desired item is emitted and received by your subscribe callback.
Two possible options:
Having a subject for the selected result that is "nexted" either by user input or a side effect of getting an api result with one element.
Keeping track of an overall state of the component and responding appropriately whenever a selectedResult is set in the state.
The example below is a sketch of using an Observable to keep track of the component's state.
There are two input streams into the state, the results from the api and the user input for the selected result.
Each stream is converted into a reducer function that will modify the overall state.
The UI should subscribe to this state via an async pipe, showing the modal when appropriate, and updating updating state from events via the Subjects.
The redirection should come as an effect to the change of the state when selectedResult has a value.
readonly getResultsSubject = new Subject<MyInput>();
readonly resultSelectedSubject = new Subject<ResultType>();
private readonly apiResults$ = this.getResultsSubjects.pipe(
switchMap((input) => this.apiMethod(input))
);
readonly state = combineLatest([
this.apiResults$.pipe(map(results => (s) => results.length === 1
? { ...s, results, selectedResult: x[0], showModal: false }
: { ...s, results, showModal: results.length > 1 })),
this.resultSelectedSubject.pipe(map(selectedResult => (s) => ({ ...s, selectedResult })))
]).pipe(
scan((s, reducer) => reducer(s), { }),
shareReplay(1)
);
ngOnInit() {
this.state.pipe(
filter(x => !!x.selectedResult)
).subscribe(x => this.router.navigate('elements/', x.selectedResult.id));
}
I've been using this pattern a lot lately. It makes it pretty easy the number of actions and properties of the state grow.
I would solve it using the following method:
Get the data with your subscribe (without the pipe). And save this data in the component variable
options: any;
this.apiService
.apiMethod(input)
.subscribe(
(result) => {
if (result.length === 1) {
this.router.navigate([result[0]]);
return;
}
options = result;
}
)
with an ngIf on the modal (conditional of the length of the array of options > 0 display the component with the different choices when the data is received
<modal-component *ngIf="options.length > 0"></modal-component>
when the user (click) on an option inside your modal, use the router to redirect.
html
<div (click)="redirect(value)">option 1</div>
ts
redirect(value) {
this.router.navigate([value]);
}
That would be the most straight forward

RxJS - Create Auto-Complete Observable That First Returns Data From Cache And Then From Server

I found this article that explains how I can use RxJs to create an observable for auto-complete:
https://blog.strongbrew.io/building-a-safe-autocomplete-operator-with-rxjs
const autocomplete = (time, selector) => (source$) =>
source$.pipe(
debounceTime(time),
switchMap((...args: any[]) =>
selector(...args)
.pipe(
takeUntil(
source$
.pipe(
skip(1)
)
)
)
)
)
term$ = new BehaviorSubject<string>('');
results$ = this.term$.pipe(
autocomplete(1000, (term => this.fetch(term)))
)
I want to improve this auto-complete observable by first returning data from local storage and display it to the user and then continue to the server to fetch data. The data that will be returned from the server will not replace the one the result from the local storage but will be added to it.
If I understand it correctly on each time the user types, there observable should emit twice.
How can I build it in the most efficient way?
Kind Regards,
Tal Humy
I think you can take advantage of startWith.
const term$ = new BehaviorSubject('');
const localStorageResults = localStorage.getItem(''); // Map it into the same shape as results$ but the observable unwrapped
const results$ = term$
.pipe(
startWith(localStorageResults),
debounceTime(1000),
switchMap(term =>
getAutocompleteSuggestions(term)
.pipe(
takeUntil(
//skip 1 value
term$.pipe(skip(1))
)
)
)
)
)
You may have to tinker with that, I am not sure if it will play nice with the debounceTime but it's an idea.
So after dealing with this for a few hours, I figured out that the solution was very straightforward:
autocomplete(1000, (term => new Observable(s => {
const storageValue = this.fetchFromStorage(term);
s.next(storageValue);
this.fetchFromServer(term)
.subscribe(r => s.next(r));
})))

How to branch off of an rxjs stream conditionally?

I am trying to simulate the "brush" feature like the one in any image editor.
I have the following streams:
$pointerDown: pointer pressed down
$pointerUp: pointer pressed up
$position: position of the brush
$escape: Escape key pressed
What I want to do
When the user is dragging the mouse, do temporary calculations. If the mouse is up, then commit those changes. If the escape key is pressed then do not commit those changes.
What I am currently handling is the first case:
$pointerDown.pipe(
r.switchMap(() =>
$position.pipe(
r.throttleTime(150),
r.map(getNodesUnderBrush),
r.tap(prepareChanges),
r.takeUntil($pointerUp),
r.finalize(commitBrushStroke))
)).subscribe()
How can I end the stream in two different ways? What is the idiomatic rxjs for this?
Thanks
Regarding your question I can see you need to have some kind of state over time. Here your state is the pointerdown/move/dragging observable, that needs to be accumulated or cleared and finally emitted. When I see such a state scenario I always like to use the scan operator:
Pre
For the sake of simple example i did not use your predefined observables. If you have issues adapting your specific pointer usecase to this very similar one, I can try to update it so it is closer to your question
1. What could represent my state
Here I am using an enum [status] to later on react on the event that happened before and an accumulation [acc] for the points over time
interface State {
acc: number[],
status: Status
}
enum Status {
init,
move,
finish,
escape
}
const DEFAULT_STATE: State = {
acc: [],
status: Status.init
}
2. Write functions that mutate the state
Your requirement can be split into: accumulate [pointerdown$ + position$], finish [pointerup$], escape [escape$]
const accumulate = (index: number) => (state: State): State =>
({status: Status.move, acc: [...state.acc, index]});
const finish = () => (state: State): State =>
({status: Status.finish, acc: state.acc})
const escape = () => (state: State): State =>
({status: Status.escape, acc: []})
3. Map your functions to your observables
merge(
move$.pipe(map(accumulate)),
finish$.pipe(map(finish)),
escape$.pipe(map(escape))
)
4. Use the functions in the scan where your state over time is placed
scan((acc: State, fn: (state: State) => State) => fn(acc), DEFAULT_STATE)
5. Process your mutated state
Here we only want to process if we have a finish, so we filter for it
filter(state => state.status === Status.finish),
Inner state sample
move$.next('1') = State: {status: move, acc: ['1']}
escape$.next() = State: {status: escape, acc: []}
move$.next('2') = State: {status: move, acc: ['2']}
finish$.next() = State: {status: finish, acc: ['2']}
Running stackblitz
FYI: It is pretty hard to get this mindset of solving state problems with rxjs. But once you understand the flow behind the idea, you can use it in nearly every statefull scenario. You will avoid sideeffects, stick to rxjs workflow and you can easily optimize/debugg your code.
Very interesting problem!
Here's my approach:
const down$ = fromEvent(div, 'mousedown');
const up$ = fromEvent(div, 'mouseup');
const calc$ = of('calculations');
const esc$ = fromEvent(document, 'keyup')
.pipe(
filter((e: KeyboardEvent) => e.code === 'Escape')
);
down$ // The source of the stream - mousedown events
.pipe(
switchMapTo(calc$.pipe( // Do some calculations
switchMapTo(
merge( // `merge()` - want to stop making the calculations when either `up$` or `esc$` emit
up$.pipe(mapTo({ shouldCommit: true })),
esc$.pipe(mapTo({ shouldCommit: false })),
).pipe(first()) // `first()` - either of these 2 can stop the calculations;
)
))
)
.subscribe(console.log)
StackBlitz.
I think there's pretty simple way to do that with toArray() and takeUntil(). I'm assuming that when you said you want "commit" changes you want to collect all changes and process them all at once. Otherwise, the same approach would work with buffer() as well.
$pointerDown.pipe(
switchMap(() => $position.pipe(
throttleTime(150),
map(getNodesUnderBrush),
tap(prepareChanges), // ?
takeUntil($pointerUp),
toArray(),
takeUntil($escape),
)
).subscribe(changes => {
...
commitBrushStroke(changes);
})
So the entire trick is whether you complete the inner chain before or after toArray. When you complete it before toArray() then toArray() will emits a single array of all changes it has collected so far. If you complete it after toArray() then the chain is disposed and toArray() will discard everything and just unsubscribe.
If I understand right, when the user presses the escape key than the entire stream should be unsubscribed so that, when the users starts again the dragging with the pointer mouse down, the stream starts again emitting.
If this is the case, you may want to try switchMap with $escape and merge, in other words something like this
const drag$ = $pointerDown.pipe(
r.switchMap(() =>
$position.pipe(
r.throttleTime(150),
r.map(getNodesUnderBrush),
r.tap(prepareChanges),
r.takeUntil($pointerUp),
r.finalize(commitBrushStroke))
))
const brush$ = $escape.pipe(
startWith({}), // emit any value at start just to allow the stream to start
switchMap(() => drag$)
);
const stopBrush$ = $escape.pipe(tap(() => // do stuff to cancel));
merge(brush$, stopBrush$).subscribe();
The whole idea is that, any time $escape emits, the previous subscription to drag$ is unsubscribed and a new one starts. At the same time any logic to cancel what needs to be cancelled can be performed.
I can not test this thing, so I hope I have not forgot something.
Here goes an other possible solution very much like your original:
const start$ = fromEvent(document, "mousedown").pipe(
tap((event: MouseEvent) => this.start = `x: ${event.clientX}, y: ${event.clientY}`)
);
const drag$ = fromEvent(document, "mousemove").pipe(
tap((event: MouseEvent) => (this.move = `x: ${event.clientX}, y: ${event.clientY}`) )
);
const stop$ = fromEvent(document, "mouseup").pipe(
tap((event: MouseEvent) => (this.stop = `x: ${event.clientX}, y: ${event.clientY}`))
);
const cancel$ = fromEvent(document, "keydown").pipe(
filter((e: KeyboardEvent) => e.code === "Escape"),
tap(() => this.stop = 'CANCELLED')
);
const source$ = start$
.pipe(
mergeMap(() =>
drag$.pipe(
takeUntil(merge(stop$, cancel$))
)
)
)
.subscribe();
The stream can end in two ways merging both takeUntil conditions:
takeUntil(merge(stop$, cancel$))
Here is the Stackblitz: https://stackblitz.com/edit/angular-gw7gyr

Angular async pipe don't refreshes result after input array filtering

In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations

Proper way to access store in ngrx/effect

I am using Angular 6, ngrx/store, ngrx/effects.
I have an effect that should be triggered when i press "Save" button. I am using withLatestFrom there to collect all data what i need for sending it to the server:
#Effect({dispatch: false})
saveAll$ = this.actions$.pipe(
ofType(ActionTypes.Save),
withLatestFrom(
this.store.select(fromReducers.getData1),
this.store.select(fromReducers.getData2),
this.store.select(fromReducers.getData3),
this.store.select(fromReducers.getData4)
),
switchMap(([action, data1, data2, data3, data4]: [ActionType, Data1[], Data2[], Data3[], Data4[]]) => {
// here is some operations with these data
return this.apiService.saveData({data1, data2, data3, data4})
})
)
Here is getData1 selector:
export const getData1= createSelector(
getItems,
getIndexes,
(items, indexes) => {
console.log('HI, I AM getData1');
return transformItems(items, indexes);
}
);
getItems, in turn, return state.items. The problem is that state.items can be modified in another effect:
#Effect()
handleItemsChanges$ = this.actions$.pipe(
ofType(ActionTypes.ChangesInItems),
withLatestFrom(
this.store.select(fromReducers.getItems),
this.store.select(fromReducers.getUsers),
),
switchMap(([action, items, users]: [ActionType, Item[], User[]]) => {
console.log('I AM handleItemsChanges');
const actions = [];
if (itemsShouldBeUpdated) {
actions.push(new UpdateData(changes))
}
})
)
So getData1 selector gets data from the store depend on another effect named handleItemsChanges. handleItemsChanges effect is triggered every time something is changed related to the items and recalc it again.
As a result, in saveAll i am getting not actual state.items.
What am i doing wrong? May be i should use another operator insted of withLatestFrom or what ca be the solution? Thank you
P.S. Btw i am using withLatestFrom every time when i want to get some data from the store. Is it correct?
you need to have action handleItemsChanges fired before saveAll gets fired. One way to do it is to create an effect on handleItemsChanges action and trigger the save action.
The framework will guarantee the order of execution (handleItemsChanges first then save), this way the withLatestFrom operation will work as you expected.
I've found discussion on ngrx github : https://github.com/ngrx/platform/issues/467
Looks like we have 2 ugly variants for accessing store from effects now.

Categories