Push items to array when variable is done being totaled - javascript

Using Angular, I have created a component that fires an initial web request along with two others used in loops. I am looping through these items to set siteCount which is declared in the first loop's request. I am using setTimeout() to push the totaled amount to an array, however this is probably not best practice.
constructor(public http: HttpClient, public chart: ChartDetails) {
this.http.get(`https://siteurl.com/items?$filter=Hub/Title eq 'Project Hub' and Active eq 'Yes'`).subscribe(data => {
data['value'].forEach(item => {
let siteCount: number = 0
this.http.get(`${item.Site}/lists/?$filter=BaseTemplate eq 171`).subscribe(data => {
data['value'].forEach(list => {
this.http.get(`${item.Site}/lists(guid'${list.Id}')/items`).subscribe(data => {
this.myTasks = data['value']
this.myTasks.forEach(variable => {
variable.internalListName = list.EntityTypeName.replace("List", "")
});
siteCount += data['value'].length
})
});
})
setTimeout(() => {
if (siteCount) {
this.chart.goalChartLabels.push(item.Title)
this.chart.goalChartData.push(siteCount)
}
}, 500);
});
})
}
Without using setTimeout, how can I push item.Title and siteCount when siteCount is done being totaled and before being reset to 0 for the next item?

My initial version would look like this:
import { zip } from 'rxjs';
...
this.http.get(`https://siteurl.com/items?$filter=Active eq 'Yes'`).subscribe(data => {
data['value'].forEach(item => {
this.http.get(`${item.Site}/lists/?$filter=BaseTemplate eq 171`).subscribe(data => {
const itemListObservables = data['value']
.map(list => this.http.get(`${item.Site}/lists(guid'${list.Id}')/items`));
zip(...itemListObservables)
.subscribe(itemLists => {
const totalCount = itemLists
.map(l => l.length)
.reduce((sum, val) => sum + val, 0)
if (totalCount > 0) {
this.chart.goalChartLabels.push(item.Title)
this.chart.goalChartData.push(totalCount)
}
itemLists.forEach(variable => {
variable.internalListName = list.EntityTypeName.replace("List", "");
this.myTasks.push(variable);
});
});
})
});
})
zip function waits for all observables to emit a value and returns an array of them.
As for best practices:
HttpService calls should be done at least in ngOnInit. You will probably use inputs for running these requests and they won't be defined in constructor.
Making three-level nested loops with HTTP requests on each level is not pretty. Consider making this a single resource on server side.

You should use forkJoin() to make all the requests and then get the response.
The rxjs docs link.

Related

Updating count items with behaviorsubject cross side component communication

I have a angular 8 application. and I have two components. one component where you can edit a item. And one component where you can see all the items. The items are divided in four categories. And for each category there is a counter that counts the items in each category. If I add a item the counter increase, so that works correct. But if I delete a item. The counter doest'n decrease.
So I have this service:
_updateItemChanged = new BehaviorSubject<any>([]);
constructor() {}
}
and this is the
component where you can delete a item child component:
remove() {
this.dossierItemService.deleteDossierItem(this.data.dossierId, this.data.item.id)
.subscribe(() => {
this.dialogRef.close(true);
this.itemListService._updateItemChanged.next(this.data.item.title);
}, (error) => {
const processedErrors = this.errorProcessor.process(error);
this.globalErrors = processedErrors.getGlobalValidationErrors();
});
}
and this is the parent component.html:
<span i18n>Action steps</span>{{ dossierItemsCountString(itemTypes.ActionStep) }}
and this is parent.ts code
ngOnInit(): void {
this.itemlistService._updateItemChanged.subscribe(data => {
this.dossierItems = this.dossierItems.filter(d => d.title !== data);
});
}
and the dossierItemsCountString function:
dossierItemsCountBy(itemType: DossierItemTypeDto) {
return this.typeSearchMatches[itemType.toString()] || { total: 0, matches: 0 };
}
dossierItemsCountString(itemType: DossierItemTypeDto) {
const count = this.dossierItemsCountBy(itemType);
if (this.hasSearchQuery) {
return `(${count.matches}/${count.total})`;
} else {
return `(${count.total})`;
}
}
So what I have to change so that in the parent component also the counter decrease if you remove a item.
Thank you
After the discussion we found that the problem is in mixed pointers among components and the proper way to fix it is to manipulate the original array, instead of creating new one.
The solution:
this.itemlistService._updateItemRemoved.subscribe(data => {
const index = this.dossierItems.findIndex(a => a.id === data.id);
if (index !== -1) {
this.dossierItems.splice(index, 1);
}
});

Creating new array vs modifing the same array in react

Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.

I'm looking for a way to remove items from my select array based on values of other array

I'm trying to remove from select array the options who match with my other array, both come from api and are beeing displayed through ngFor, i'm already trying to do it using filter but with no success, code below as follow:
loadUnidadeUsuarios() {
return this.restApi.getUnidadeUsuarios(this.id).subscribe((data: UnidadeUsuario) => {
this.unidadeUsuarioData = data;
});
}
Then after load UnidadeUsuarioData i need to remove the users who match:
loadUsuarios() {
return this.restApi.getUsuarios().subscribe((data: Usuario) => {
this.Usuario = data;
this.Usuario = this.Usuario.filter(usuario => usuario.id !== this.unidadeUsuarioData.id )
});
}
But with no success
the filter it's looks like
this.Usuario.filter(usuario=>!this.unidadeUsuario.find(x=>x.id==usuario.id)
But you must take account if you need wait to the two calls finish. For this use switchMap
this.restApi.getUnidadeUsuarios(this.id).pipe(
switchMap((res: any[]) => {
//..here you has your getUnidadeUsuario...
//but you want the others usuarios
return this.restApi.getUsuarios().pipe(map((usuarios: any[]) => {
//in usuarios you has all the usuarios
//but you want filtered
return usuarios.filter(u => !res.find(x => x.id == u.id))
}))
})
).subscribe(usuarios => {
console.log(usuarios)
})
To achieve expected result, with just two loops, use below option
Convert unidadeUsuarioData as obj with id values
Filter Usuario to check whether unidadeUsuarioData has property id
loadUsuarios() {
let unidadeUsuarioDataObj = this.unidadeUsuarioData.reduce((acc, v) => {
acc[v.id] = v;
return acc
}, {})
this.Usuario = this.Usuario.filter(usuario => !unidadeUsuarioDataObj[usuario.id])
console.log(this.Usuario)
}
stackblitz for reference- https://stackblitz.com/edit/angular-dapycb?file=src/app/app.component.html

How to chain suscriptions

Hi I am trying to chain following subscriptions.
changeBranch() {
const bottomSheetRef: MatBottomSheetRef = this.bottomSheet.open(CCSBranchComponent, {
data: this.branches
});
this.subscription.add(bottomSheetRef.instance.change.subscribe((branch: Branch) => {
this.branchInfo = `Description : ${branch.author}\nAuthor : ${branch.id}\nCreated date :${branch.created}`;
this.blockpointBranchForm.get('branch').setValue(branch.id);
}));
this.subscription.add(bottomSheetRef.afterDismissed().subscribe(() => {
this.returnSelectedBranch.emit(this.blockpointBranchForm.get('branch').value);
}));
}
Here if bottomSheetRef.instance.change.subscribe is called before the sheet loads, it throws undefined. So i am trying the implement something that looks like this
this.subscription.add(this.bottomSheet.open(CCSBranchComponent, {
data: this.branches
}).instance.change.subscribe((branch: Branch) => {
this.branchInfo = `Description : ${branch.author}\nAuthor : ${branch.id}\nCreated date :${branch.created}`;
this.blockpointBranchForm.get('branch').setValue(branch.id);
}).afterDismissed().subscribe(() => {
this.returnSelectedBranch.emit(this.blockpointBranchForm.get('branch').value);
}));
Here the second subscribe is called on the subscription returns by first. How do I access the observable in the chain?
I guess what you want is to chain the the actions what are done when subscribing.
You can achieve this by
bottemsheetRef.instance.change.pipe(
switchmap(resultFromChange => bottomSheetRef.afterDismissed
).subsbribe(resultFromAfterDismissed => {// do whatever you like})

tranforming RxJS Observable

I use angularFirestore to query on firebase and I want join data from multiple documents using the DocumentReference.
The first operator map in the pipe return an array of IOrderModelTable, the second operator, i.e, the switchMap iterate over array an for each element use the id contained in each element to query data in other table.
The problem is that in the swithMap I obtain an array of observable due to anidated map operators. How I can obtain an array of IOrderModelTable and then return an observable of this array.
The code is:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
map(actions => {
return actions.map(a => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
});
}),
switchMap(data => {
const result = data.map(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
}));
});
// Result must be an IOrderModelTable[] but is a Observable<IOrderModelTable>[]
return of(result);
})
);
You can use to Array operator to transform a stream to an array, but make sure your stream will end.
The trick is to choose the right stream.
For you problem, the natural source would be the list received by your first call. In a schematic way I can put it , you get a list of ids, that you transform into a list of augmented information :
first input ...snapshopChanges():
----[A, B, C]------>
each element is transformed through ...valueChanges():
-------Call A -------------DataA-------->
-------Call B ------------------------DataB----->
-------Call C --------------------DataC----->
Then reduced using toArray() to :
----------------------------------------------[DataA, DataC, DataB]-------->
Code:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> { {
return this.tableOperatorsCollectionsRef.snapshotChanges()
.pipe(
map(actions => {
from(data).pipe(
map(action => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
}),
mergeMap(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(
map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
})
);
}),
toArray()
);
})
)
}
Important : I replaced switchMap by mergeMap, otherwise some information could be thrown away.
#madjaoue
You're right, mergeMap is the correct operator in this case because with switchMap for each event emitted the inner observable is destroyed so in the subscribe you only get the final event emitted, i.e, the last row. This observable is long lived, never complete, so also use the operator take with the length of the actions which is the array that contains the list of documents.
Thank you very much for the help. :D
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
switchMap(actions => {
return from(actions).pipe(
mergeMap(action => {
console.log(action);
const data = action.payload.doc.data() as IOrdersModelDatabase;
const otNumber = action.payload.doc.id;
return this.afs.collection('Orders/').doc(data.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: otNumber,
clientName: '',
clientReference: order.clientReference,
id: data.orderNumberReference,
};
}));
}),
mergeMap(order => {
console.log(order);
return this.afs.collection('Clients/').doc(order.clientReference.id).valueChanges().pipe(map(client => {
return {
otNumber: order.otNumber,
clientName: client.name,
clientReference: order.clientReference,
id: order.id,
};
}));
}),
take(actions.length),
toArray(),
tap(console.log),
);
}),

Categories

Resources