ngFor showing the last element only from array - javascript

Trying to loop over an array and display the results, but only the last element showing multiple times.
Here is my code.
Making a get request.
showItems(id: any): Observable<any> {
return this.http.get(`${this.url}${id}`)
}
Console logging works fine here, I can see the expected results.
ngAfterViewInit(): void {
this.showItems()
}
showItems(): void {
const id: any = []
this.idFromList.forEach((val: any) => id.push(val.nativeElement.innerText))
for(let i = 0; id.length > i; i++) {
this.ItemService.showItems(id[i]).subscribe(data => {this.api_items = data.item.current
console.log(this.api_items)});
}
}
HTML
<table>
<thead>
<tr>
<td>Name</td>
<td>Price</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items | filter: searchQuery" >
<td >
<a href="" #btn>{{item.id}}</a>
{{ item.name }}
</td>
<td *ngFor="let api_item of api_items | keyvalue">
{{ api_item.value }}
</td>
</tr>
</tbody>
</table>
Tried using fake JSON server, same results.

1) Quick answer
Push items in an array instead of replacing its value :
api_items = [];
this.ItemService.showItems(id[i]).subscribe(data => {
this.api_items.push(data.item.current);
});
2) Complete answer
As #MoxxiManagarm said, you're replacing api_items value at each iteration of your for loop.
You might want to check & use RxJS to handle multiple observables in a cleaner way: for example, by generating an array of Observable to resolve them together.
3) Ressources & useful links you might want to check for more information
https://www.learnrxjs.io/

Related

Functions as Element Attributes and the Angular ExpressionChangedAfterItHasBeenCheckedError

I'm creating a summary row on a tree table that also has filtering (I'm using the ng Prime component library, but I don't think that's germane to the error or question). I want my summary row to only summarize the information currently in view, thus the summary should change when the tree is expanded or when a filter is applied.
I achieved this goal by adding row data to a map via a function I set as an element attribute [attr.summaryFunction]="summaryFunction(i,rowData[col.field])", and reinitializing the map with ngDoCheck. Without this step, reinitializing the map in ngDoCheck, totals accumulate with every repaint or change and result in incorrect values. (I tried reinitializing the map with several other lifecycle hooks ngOnInit, ngOnViewInit, and ngOnChanges, but the only lifecycle hook that effectively reinitialized the map was ngDoCheck).
My approach works perfectly (yay me!), but I get an ExpressionChangedAfterItHasBeenCheckedError in the browser, three times, for each column. To address this error, I tried all 4 approaches to fixing the error recommended in the documentation:
using different lifecycle hooks --> totals are incorrect
I added this.cd.detectChanges() to summary function --> infinite loop
I wrapped the if statement in setTimeout(() => if(statement){}) --> infinite loop
Promise.resolve().then(() => if(statement){}) --> infinite loop
Again, my approach currently works exactly as I want it to, I just want these annoying errors to go away. I'd also like to understand why the usual fixes cause an infinite loop.
Here's my TS layer;
export class PrimetreeComponent implements OnInit, DoCheck {
sampleData: TreeNode[] = []
columns: any[] = [];
summaryMap = new Map();
columnIndex = 0;
constructor(private nodeService: NodeService, private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.nodeService.getFilesystem().subscribe(data => {this.sampleData = data});
this.columns = [
{field: 'name', header: 'name'},
{field: 'size', header: 'size'},
{field: 'type', header: 'type'}]
}
ngDoCheck(): void {
this.summaryMap = new Map();
}
summaryFunction(columnIndex:number, rowData:any){
if(this.summaryMap.has(columnIndex)){
//if row data is a non-number string we just count the rows
if(Number(rowData)){
this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex)+Number(rowData));
} else {
this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex)+1)
}
} else {
if(Number(rowData)){
this.summaryMap.set(columnIndex, Number(rowData));
} else {
this.summaryMap.set(columnIndex, 1)
}
}
}
}
And here's my HTML Template:
<p-treeTable #tt [value]="sampleData" [columns]="columns">
<ng-template pTemplate="caption">
<div style="text-align: right">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input
pInputText
type="text"
size="50"
placeholder="Global Filter"
(input)="tt.filterGlobal($any($event.target).value, 'contains')"
style="width:auto"
/>
</span>
</div>
</ng-template>
<ng-template pTemplate="header" let-columns>
<tr>
<th *ngFor="let col of columns">
{{ col.header }}
</th>
</tr>
<tr>
<th *ngFor="let col of columns">
<input
pInputText
type="text"
class="w-full"
(input)="
tt.filter($any($event.target).value, col.field, col.filterMatchMode)
"
/>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
<tr>
<td *ngFor="let col of columns; let i = index;" [attr.summaryFunction]="summaryFunction(i,rowData[col.field])">
<p-treeTableToggler
[rowNode]="rowNode"
*ngIf="i == 0"
(columnIndex)="i"
></p-treeTableToggler>
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="columns.length">No data found.</td>
</tr>
</ng-template>
<ng-template pTemplate="summary">
<tr>
<td *ngFor="let col of columns; let i = index;">
{{ col.header }}--sumMap--{{summaryMap.get(i) }}
</td>
</tr>
</ng-template>
I am not familiar with the approach of binding a function to an elements attribute, but it seems kind of hacky to me. Correct me if I'm wrong, but the reason for this is to have a way to trigger the function when the node is added to the dom?
I did not test this myself, but I think you could make use of the #ContentChildren decorator see docs. It exposes an observable that you can subscribe to to react to changes of the content dom. Ideally, you could run the summary creation and updating the summaryMap in one step.
Update:
I played around with the idea a bit. One solution for what you are trying to achieve is to use #ViewChildren decorator to get a QueryList of all the rows currently rendered. This can be achieved in the following way:
<ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
<tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size">
<td>
<p-treeTableToggler [rowNode]="rowNode"></p-treeTableToggler>
{{ rowData.name }}
</td>
<td>{{ rowData.size }}</td>
<td>{{ rowData.type }}</td>
</tr>
</ng-template>
Note the template ref on <tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size"> along with binding the rowData.size to an attribute of the element.
You can then grab that list of rows rows that are currently mounted to the dom like so:
// inside your component
#ViewChildren('tableRow') view_children!: QueryList<ElementRef>;
// get the list, calculate initial sum and subscribe to changes.
// We do that in ngAfterViewInit lifecycle hook as the
// list is guaranteed to be available at that point of time:
ngAfterViewInit() {
this.calculateSum(this.view_children);
this.view_children.changes.subscribe(
(list: QueryList<ElementRef>) =>
this.calculateSum(list)
);
}
You can find a running example of this here.

foreach loop does not update to next item properly, but runs proper number of times

I have passed a list of Models from my controller to my View, and I am trying to use a foreach loop to display elements of each item in the list. The same item is being displayed each time, though my foreach loop runs the correct number of times (i.e. if my list has 3 items, it will create a table with 3 rows, but every row will be the same).
I have confirmed that my list is populated correctly by changing the order in which the elements are passed to the View. The table always displays elements of the first item in every row, once for every time the for loop runs. Here is my code in the view to iterate through my list:
<tbody>
#foreach (var item in Model)
{
<tr>
<td>
<b>#Html.DisplayFor(Modelitem => item.Name)</b>
</td>
<td>
<b>#Html.DisplayFor(Modelitem => item.IPAddress)</b>
</td>
</tr>
}
</tbody>
This is my code from the controller to populate the list. I have verified that by changing the order of the elements, a different element will be displayed multiple times in the View.
public ActionResult Index()
{
List<IPWhitelist> model = new List<IPWhitelist>();
long companyID = Convert.ToInt64(Session["CompanyID"].ToString());
long branchID = Convert.ToInt64(Session["BranchID"].ToString());
model = db.IPWhitelists.Where(w => w.CompanyID == companyID && w.BranchID == branchID && w.IsActive).OrderBy(w => w.Name).ToList();
return View(model);
}
I am very stumped so any help is appreciated!
I didn't use MVC in a while, I think you have a problem in the syntax
If you want to iterate you can either use simply #item.Name and #item.IPAdress, and if you want to use the Html.DisplayFor, you have to use it as so:
<tbody>
#for (var i=0; i < Model.Count; i++)
{
<tr>
<td>
<b>#Html.DisplayFor(Modelitem => Modelitem[i].Name)</b>
</td>
<td>
<b>#Html.DisplayFor(Modelitem => Modelitem[i].IPAddress)</b>
</td>
</tr>
}
</tbody>
DisplayFor helper can be used in this way:
#foreach (var item in Model)
{
<tr>
<td>
<b>#Html.DisplayFor(model => item.Name)</b>
</td>
</tr>
}

Angular 2 set boolean at end of iteration using *ngFor

I am loading some data via rest-endport to safe it in an array. In an *ngFor directive i am currently iterating through all that data. So everthing is working.
Is there any possible way in Angular to e.g. set a boolean whenever the iteration of the array is finished (so that i can fire another function on complete)?
-- Edit--
On the ngOnInit lifecycle method i am retreiving the data:
ngOnInit() {
this.restService.getSomeBooks(5000).subscribe(buch => {
this.buecher = buch;
this.fetched = true;
})
}
After that - in HTML - i am iterating over that data:
<table>
<thead>some table-heads</thead>
<tbody>
<tr style="text-align: center" *ngFor="let buchforTable of buecher">
<td>{{buchforTable.author}}</td>
<td>{{buchforTable.erscheinungsdatum}}</td>
<td>{{buchforTable.isbn.toString()}}</td>
<td>{{buchforTable.verlag}}</td>
<td>{{buchforTable.uuid}}</td>
</tr>
</tbody>
</table>
The *ngFor-iteration should set a boolean variable whenever it is done.
As Alex Po already mentioned, trackBy is working. As you can see in the following snippet, with trackBy it is possible to handle events based on the current index of the iteration.
<tbody>
<tr style="text-align: center" *ngFor="let buchforTable of buecher; trackBy: trackByFn">
<td>{{buchforTable.author}}</td>
<td>{{buchforTable.erscheinungsdatum}}</td>
<td>{{buchforTable.isbn.toString()}}</td>
<td>{{buchforTable.verlag}}</td>
<td>{{buchforTable.uuid}}</td>
</tr>
</tbody>
To measure the time of the rendering process of all array-objects the trackBy-function would look like this (array contains 5000 objects here -> index 0 to 4999):
trackByFn(index){
if(index == 0)
this.renderStart = performance.now();
if(index == 4999) {
var renderStopp = performance.now();
var timeToRender = renderStopp - this.renderStart;
}
}

orderBy not working with pagination and filters

<table>
<thead>
<tr>
<th class="col-md-3" ng-click="sortDirection = !sortDirection">Created At</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="food in foods | filter:foodFilter | itemsPerPage:pageSize | orderBy:'created_at_date'">
<td class="col-md-"> {{food.created_at_date}} </td>
</tbody>
</table>
<dir-pagination-controls
max-size= 7
boundary-links="true">
</dir-pagination-controls>
This is only a snippet of my code but its too large to put up. Everything is working except only some of the created_at_date is in order. When I click on a different filter to add in or remove data depending on that filter, only some of it is entered into the correct place. My main question is: is there someway to sort all of the dates properly while still allowing the everything else function as well? All help is welcome, Thanks
(function () {
"use strict";
App.controller('foodsController', ['$scope'],
function($scope) {
$scope.sortDirection = true;
In your controller you can add the method to order the array before you loop over them.
Assuming your foods array has an array of objects, each with a key of created_at_date and a value:
App.controller('foodsController', function($scope) {
$scope.foods = [{
created_at_date: 6791234
}, {
created_at_date: 9837245
}, {
created_at_date: 1234755
}];
// create a method exposed to the scope for your template.
$scope.orderBy = function(key, array) {
// now you've received the array, you can sort it on the key in question.
var sorted = array.sort(function(a, b) {
return a[key] - b[key];
});
return sorted;
}
});
Now on your template, you have a method available to sort your values for you:
<table>
<thead>
<tr>
<th class="col-md-3" ng-click="sortDirection = !sortDirection">Created At</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="food in orderBy('created_at_date', foods) | filter:foodFilter | itemsPerPage:pageSize">
<td class="col-md-"> {{food.created_at_date}} </td>
</tr>
</tbody>
</table>
The orderBy method which we've created on your controller returns an array, but it's just sorted by the key that's sent in as the first argument of the function. The second argument is the original array you're trying to sort.
At least this way you can check if you remove all your other filters to see if it's ordered correctly, if then after you add them back in it changes it's because those filters are also changing the order.

Formatting data before render it

I am displaying some data in the view, but I need to formatted first, I was doing something like
val.toFixed(2) and that is OK, it works but the problem is that val sometimes comes with letters, and toFixed(2) is not taking that into account so is not displaying the letters.
So I need something that takes into account letters and numbers, the letters don't have to change, only the numbers which comes like 234235.345345435, and obviously I need it like this 234235.34.
Here is some of the code I am using
<table>
<tr>
<th ng-repeat='header in headers'>{{header.th}}</th>
</tr>
<tr>
<td ng-repeat='data in headers'>
<div ng-repeat='inner in data.td'>
<span ng-repeat='(prop, val) in inner'>{{val.toFixed(2)}}</span>
</div>
</td>
</tr>
</table>
and in the controller
$scope.LoadMyJson = function() {
for (var s in myJson){
$scope.data.push(s);
if ($scope.headers.length < 1)
for (var prop in myJson[s]){
prop.data = [];
$scope.headers.push({th:prop, td: []});
}
}
for (var s in $scope.data){
for (var prop in $scope.headers){
var header = $scope.headers[prop].th;
var data = myJson[$scope.data[s]][header];
$scope.headers[prop].td.push(data);
console.log($scope.headers[prop].td);
}
}
};
and I prepared this Fiddle
the way it is right now, is displaying the table properly, but as you see, the table is missing the name, it is because of the toFixed method.
So, what can I do ?
Create a custom filter to use on your template.
<table>
<tr>
<th ng-repeat='header in headers'>{{header.th}}</th>
</tr>
<tr>
<td ng-repeat='data in headers'>
<div ng-repeat='inner in data.td'>
<span ng-repeat='(prop, val) in inner'>{{val|formatValue}}</span>
</div>
</td>
</tr>
</table>
angular.module('whatever').filter('formatValue', function () {
return function (value) {
if (isNaN(parseFloat(value))) {
return value;
}
return parseFloat(value).toFixed(2);
}
});
You can try this :
That is a clean way to render formated data in view using angularjs as MVC
frontend framework :
Create a filter in your angular application.
Include your filter in your index.html.
use your filter like this : {{somedata | filterName}}
That is a simple angular filter to solve your problem, hope it will help you :
angular.module('app')
.filter('formatHeader', function() {
return function(data) {
if(angular.isNumber(data)) {
return data.toFixed(2);
}
return data;
}
});
And us it like this :
<table>
<tr>
<th ng-repeat='header in headers'>{{header.th}}</th>
</tr>
<tr>
<td ng-repeat='data in headers'>
<div ng-repeat='inner in data.td'>
<span ng-repeat='(prop, val) in inner'>{{val | formatHeader}}</span>
</div>
</td>
</tr>
You can take a look about these references :
angular functions
filter doc.
angular tutorials

Categories

Resources