ngxs state operator to insert or replace item in array (upsert) - ngxs

I'm trying to replace or insert an array item (upsert) with existing ngxs state operators. I am currently using the following iif-statement. Is there an easier way to do this?
setState(
patch({
contractList: iif<Contract[]>(
contractList=>
contractList.some(
contract =>
contract.id === contractId
),
updateItem<Contract>(
contract =>
contract.id === contractId,
patch(loadedContract)
),
insertItem<Contract>(loadedContract)
)
})
)

State operators exist but it doesn't mean you're forced to use them, right. There is a recipe called Immutability Helpers. It's going to be published to the main site in 1-2 weeks.
If we're talking about state operators then it's possible to create custom operators, that's some kind of decomposition:
function insertOrUpdateContract(id: string, loadedContract?: Contract) {
return iif<Contract[]>(
contracts => contracts.some(contract => contract.id === id),
updateItem(contract => contract.id === id, patch(loadedContract)),
insertItem(loadedContract)
);
}
Thus there will be less code in your action handler and it will be more self-descriptive:
ctx.setState(
patch({ contractList: insertOrUpdateContract(contractId, loadedContract) })
);
Otherwise you could have used immer library or something else preferred, that would save more code:
import { produce } from 'immer';
const state = produce(ctx.getState(), draft => {
const index = draft.contractList.findIndex(contract => contract.id === contractId);
// It exists then let's update
if (index > -1) {
draft.contractList[index] = loadedContract;
} else {
draft.contractList.push(loadedContract);
}
});
ctx.setState(state);
For sure immer is an additional package for the project but it's very lightweight. It provides more declarative way to do such immutable updates. Choose what suits you best!
I would stop on some immutability helper when it comes to such complex state updates.

Related

How to create a method that returns Observable that emits result of 2 Promises that need to be executed one after another?

I asked a question
Is Observable from chained promises equivalent of observables created with from and chained with concatMap?
on totally false premises. It seems that neither of my solutions had nothing to do with my intention.
I created a method that returns Observable and calls 2 methods returning Promise. I tried 2 ways:
public setItemInfos(itemInfos: IItemInfo[]): Observable<number> {
return from(this.db.selectionItemInfos.clear().then(() => {
return this.db.selectionItemInfos.bulkAdd(itemInfos);
}));
}
public setItemInfos(itemInfos: IItemInfo[]): Observable<number> {
const clear$ = from(this.db.selectionItemInfos.clear());
const bulkAdd$ = from(this.db.selectionItemInfos.bulkAdd(itemInfos));
return clear$.pipe(concatMap(() => bulkAdd$))
}
the use will be:
myService.setItemInfos(itemInfos).subsribe(count => {
console.log(`Cleared the table 1st and then added ${count} new items`);
});
I thought from both versions that:
table clear is execution is finished when bulkAdd starts
when bulkAdd is finished i get the count from that in subscribe
How this should really be done? Or can it be done?
This is (from what I can tell here), how I would do it.
In general, defer (or any higher-order operator) is a better way to create an observable from a promise. Defer lets you take the eager evaluation semantics of promises and turn them into the lazy evaluation semantics of observables.
Then all the usual observable operators and such will function as expected.
public setItemInfos(itemInfos: IItemInfo[]): Observable<number> {
const clear$ = defer(() => this.db.selectionItemInfos.clear());
const bulkAdd$ = defer(() => this.db.selectionItemInfos.bulkAdd(itemInfos));
return concat(clear$, bulkAdd$);
}
Update 1:
So I think I might know what you're after. This isn't really idiomatic RxJS since it's such an interleaving mix of declarative, imperative style of code. Even so, this should work? I haven't tested it fully, but some tinkering and I think this should do what you're after.
There's most assuredly a better way to accomplish the same thing, but without seeing the bigger picture of what you're after, it's hard to say.
interface Tagged<T> {
payload: T,
tag: number
}
class abitraryClass{
private setItemInfoSub: Subject<Tagged<IItemInfo[]>>;
private processItemInfo: Observable<Tagged<number>>;
private itemInfoTag = 0;
constructor(){
this.setItemInfoSub = new Subject<Tagged<IItemInfo[]>>();
this.processItemInfo = this.setItemInfoSub.pipe(
concatMap(({tag, payload: itemInfos}) => this.db.selectionItemInfos.clear().pipe(
ignoreElements(),
concatWith(defer(() => this.db.selectionItemInfos.bulkAdd(itemInfos))),
map(response => ({
payload: response,
tag
}))
)),
shareReplay(1)
);
// Make the processing pipeline live at all times.
this.processItemInfo.subscribe();
}
public setItemInfos(itemInfos: IItemInfo[]): Observable<number> {
const myTag = this.itemInfoTag++;
this.setItemInfoSub.next({
payload: itemInfos,
tag: myTag
});
return this.processItemInfo.pipe(
filter(({tag}) => tag == myTag),
map(({payload}) => payload)
);
}
}

rxjs - how can you create another observable from an observable but ignore its output in the parent observable?

I have a situation where I have an observable, and for each emitted item, I want to create another observable, but ignore that observable's value and instead return the result of the first observable.
For example, if I click a button, I want to track something that happens in another button, only when the first button is toggled on.
I can do this now, sort of, with a hack, by taking the output of the child observable and piping it to a mapTo with the parent's value. You can see it in this code, which can be played with in a code sandbox:
import { fromEvent, from } from "rxjs";
import { mapTo, switchMap, tap, scan } from "rxjs/operators";
const buttonA = document.getElementById("a");
const buttonB = document.getElementById("b");
const textA = document.querySelector('#texta');
const textB = document.querySelector('#textb');
fromEvent(buttonA, 'click').pipe(
// this toggles active or not.
scan((active) => !active, false),
switchMap(active => {
if (active) {
const buttonBClicks$ = fromEvent(buttonB, 'click');
// here we can observe button b clicks, when button a is toggled on.
return buttonBClicks$.pipe(
// count the sum of button b clicks since button a was toggled on.
scan((count) => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
}),
// ignore the value of the button b count for the final observable output.
mapTo(active)
)
} else {
textB.value = ``;
return from([active]);
}
})
).subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
A couple issues here. In the case that the button is toggled on, the outer observable only receives a value once the button is clicked.
This mapTo use seems hacky.
Any better ways to do this?
It sounds like you don't want the inner observable to actually be a part of the process at all. Are you waiting on it or anything?
If not, you can just do it all as a side effect as follows:
fromEvent(buttonA, 'click').pipe(
scan((active) => !active, false),
tap(active => { if(active) {
fromEvent(buttonB, 'click').pipe(
scan(count => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
})
).subscribe()
}})
).subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
Nested subscriptions are considered bad voodoo, so you ca refactor like this to keep your separation of conserns more apparent:
const trackActiveFromButton$ = fromEvent(buttonA, 'click').pipe(
scan((active) => !active, false),
shareReplay(1)
);
trackActiveFromButton$.subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
trackActiveFromButton$.pipe(
switchMap(active => active ?
fromEvent(buttonB, 'click').pipe(
scan(count => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
})
) :
EMPTY
)
).subscribe();
Any better ways to do this?
The below may be better depending on your taste. It seems to me your sample code gets a little messy because you have a single observable that is trying to do too many things. And the side-effects are sort of mixed in with the stream behavior logic.
It's totally fine to be use tap() to do side-effect type things, but sometimes it can make it harder to follow. Especially in the above code, since there is a nested observable involved.
Creating separate observables that always emit specific data can make things easier to follow.
If we declare a stream to represent the isActive state and subscribe to that to update textA, and define a counter stream to represent the number of clicks that occurred while isActive = true, using that value to update textB, I think it makes it easier to follow what's going on:
const clicksA$ = fromEvent(buttonA, 'click');
const clicksB$ = fromEvent(buttonB, 'click');
const isActive$ = clicksA$.pipe(
scan(active => !active, false),
startWith(false)
);
const counterB$ = combineLatest([isActive$, clicksB$]).pipe(
scan((count, [isActive]) => isActive ? count + 1 : -1, 0)
);
counterB$.subscribe(
count => textB.value = count === -1 ? '' :`button b count ${count}`
);
isActive$.subscribe(
isActive => textA.value = `Button a active: ${isActive}`
);
To me, having the streams defined separately makes it easier to see the relationship between them, meaning, it's easier to tell when they will emit:
isActive derives from clicksA
counterB derives from clicksB & isActive
Here's a working StackBlitz
Also:
the outer observable only receives a value once the button is clicked
This can be solved using startWith() to emit a default value.

How to load a firebase-collection with all its subcollection in one promise?

I am currently trying to figure out how to load an angular/fire-collection including all of its subcollections with RxJS.
This is my current approach:
return this.collectionRef.valueChanges().pipe(
flatMap((entities: Entity[]) => entity),
mergeMap((entity: Entity) => this.setSubCollection1(entity)),
mergeMap((entity: Entity) => this.setSubCollection2(entity)),
scan((entities: Entity[], entity: Entity) => entities.filter(a => a.id !== entity.id).concat(entity), [])
);
and to load the documents in their subcollections
private setSubCollection1 = (entity: Entity): Observable<Entity> => {
return this.subCollectionRef.valueChanges(actor).pipe(
map((subEntities1: SubEntity1[]) => {
entity.subEntities1 = subEntities1;
return entity;
})
);
}
It works fine when having a full stream.
But now I wanted to get all of data in one single Promise: I already tried .first().toPromise() but this only gets the first entry, and does not finish if the collection has no entries. Using reduce in the query also does not work, because valueChanges() never finishes.
Am I using the wrong operators? Or any other ideas on how to solve that?
I hope to hear from you.

check store for object before calling api

You know how they say you don't need state management until you know you need it. Well turns out my project needs it. So I need some help wit best practice as I am adding ngxs to an existing angular project.
I have an action called getServiceDetail and my statemodel has a list of objects called DriverListsStopInfoViewModel. each of these objects have a unique ID. The html template of the consuming component uses a selector for the property currentStopDetail, which is a state property that gets set in my action.
GOAL:
in my action I want to check the list of objects in my store to see if an object with the same id exists and return that object, and if it does not exist, call and api to get it.
EXAMPLE:
The following code works, but I would like to hear if this is the right way to do it. do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
export interface SignServiceStateModel {
searchResults: ServiceSearchModel[];
driverStopsDetails: DriverListsStopInfoViewModel[];
driverStopsList: DriverListsStopsViewModel[];
driverStopsMarkers: DriverStopsMarkerViewModel[];
currentStopDetail: DriverListsStopInfoViewModel;
}
const SIGNSERVICE_STATE_TOKEN = new StateToken<SignServiceStateModel>(
'signservice'
);
#State<SignServiceStateModel>({
name: SIGNSERVICE_STATE_TOKEN,
defaults: {
searchResults: [],
driverStopsDetails: [],
driverStopsList: [],
driverStopsMarkers: [],
currentStopDetail: null
},
})
#Injectable()
export class SignServiceState {
constructor(private driverListsService: DriverListsService) {}
#Action(DriverList.GetServiceDetail)
getServiceDetail(
ctx: StateContext<SignServiceStateModel>,
action: DriverList.GetServiceDetail
) {
if (action.serviceId === undefined || action.serviceId <= 0) {
return;
}
// check if record already in list and return
const currentState = ctx.getState();
const existingStopDetail = currentState.driverStopsDetails.find(s => s.SignServiceId === action.serviceId);
if (existingStopDetail !== undefined) {
const currentStopDetail = existingStopDetail;
ctx.patchState({ currentStopDetail });
return currentStopDetail;
}
// else get new record, add it to list and return
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((currentStopDetail) => {
ctx.patchState({ currentStopDetail });
ctx.setState(
patch({
driverStopsDetails: append([currentStopDetail])
})
);
})
);
}
#Selector()
static currentStopDetail(state: SignServiceStateModel) {
return state.currentStopDetail;
}
}
I only included the relevant code from my state class
QUESTION:
is this the best way to check the store for an item and call api if it does not exist?
Thanks in advance
Short answer is yes, what you have done here is a typical way of handling this scenario (in my experience). There's a couple of improvements you could make:
do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
No, you don't return anything from these action handlers, other than possibly an Observable that NGXS will handle (so in your case if there is no matching item found, you return the Observable that fetchs it from the API and patches the state).
Also when you do make the API call, you should only need a single update to the state:
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((result) => {
ctx.setState(
patch({
currentStopDetails: result
driverStopsDetails: append([result]),
})
);
})
);

Multiple Subscriptions on one observable

I have a read-write property on my ViewModel and need two separate actions to occur when it changes :
public decimal Paid {
get { return paid; }
set { this.RaiseAndSetIfChanged(ref paid, value); }
}
...
in the ctor:
this.WhenAnyValue(pb => pb.Paid)
.Select(amount => NumberToEnglish.ToSentence(amount))
.ToProperty(this, x => x.AmountInWords, out amountInWords);
this.WhenAnyValue(pb => pb.Paid)
.Subscribe(amount => SelectedPaymentBatch.Paid = amount );
Is there a way to do this in one statement or is this the correct way to do this?
It's very much feasible to do both in one stream, e.g using Do operator (see below), but I would recommend to keep your current approach, as it correctly separates both concerns, which are unrelated but the fact they trigger on the same property (but that could change).
this.WhenAnyValue(pb => pb.Paid)
.Do(amount => SelectedPaymentBatch.Paid = amount)
.Select(amount => NumberToEnglish.ToSentence(amount))
.ToProperty(this, x => x.AmountInWords, out amountInWords);

Resources