Angular 2 set boolean at end of iteration using *ngFor - javascript

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;
}
}

Related

ngFor showing the last element only from array

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/

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>
}

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.

angular performance: critical rendering path?

Im trying to optimice the page loading speed when rendering tables with many rows(columns min. 25x).
I am not experienced debugging/improving performance on angular apps so quite lost on what could be involved in this lack of speed.
Here is Chrome timeline report for 5 row query:
Here is Chrome timeline report for 100 row query:
The XHR load(api/list/json/Chemical...) increases in time as more rows are rendered on the table.
The server response with the data is returned fast(not the bottle neck):
Here is the template for the table:
<tbody ng-if="compressed">
<tr ng-if="dbos && (rows.length == 0)">
<td class="tableColumnsDocs"><div class="tableButtons"> </div></td>
<td class="tableColumnsDocs"><div>No results</div></td>
<td class="tableColumnsDocs" ng-repeat="attobj in columns track by $index" ng-if="$index > 0">
<p> </p>
</td>
</tr>
<tr class="tableRowsDocs" ng-repeat="dbo in rows track by $index">
<td class="tableColumnsDocs"><div ng-include="'link_as_eye_template'"></div></td>
<td class="tableColumnsDocs" ng-repeat="attobj in columns track by $index">
<div ng-init="values = dbo.get4(attobj.key); key = attobj.key; template = attobj.template || getAttributeTemplate(dbo.clazz + attobj.key);">
<div class="content" ng-include="template"></div>
<div class="contentFiller" ng-include="template"></div>
</div>
</td>
</tr>
</tbody>
And here templates the table will call:
<script type="text/ng-template" id="plain_values_template">
<p ng-repeat="v in values track by $index">{{ v }}</p>
</script>
<script type="text/ng-template" id="links_as_dns_template">
<div ng-repeat="dbo in values track by $index" ng-include="'link_as_dn_template'"></div>
</script>
<script type="text/ng-template" id="json_doc_template">
<textarea class="form-control" rows="{{values.length + 2}}" ng-trim="false" ng-readonly="true">{{ values | json }}</textarea>
</script>
<script type="text/ng-template" id="link_as_dn_template">
<p>{{ dbo.displayName() }}</p>
Relevant controller part:
$scope.getAttributeTemplate = function(str) {
//console.log("getAttributeTemplate"); console.log(str);
if ($templateCache.get(str + ".template")) {
return str + ".template";
}
var a = str.split(/(>|<)/);
//console.log(a);
if ((a.length - 1) % 4 == 0) {
return "links_as_dns_template";
}
var clsname = a[a.length - 3];
if (clsname == "*") {
return "plain_values_template";
}
var attname = a[a.length - 1];
var cls = datamodel.classes[clsname];
var att = cls.attribute[attname];
if (!att) {
return "plain_values_template";
}
if (att.type == "ref") {
return "links_as_dns_template";
}
return "plain_values_template";
};
I am new to angular and performance opt. so any tips on how to improove or bad practice highlight will be very helpful!
Long tables are angular's biggest evil, because of the hell-as-slow base directives such as ng-repeat
Some easy and obvious stuffs :
I see a lot of bindings in the row/cell templates without one-time binding (::). I dont think your row data is mutating. switching to one-time bindings will reduce the watchers count -> perf.
Some harder stuff :
Quick answer :
dont let angular handle the performance bottleneck
Long answer :
ng-repeat is supposed to compile it's transcluded content once. But using ng-include is killing this effet, causing every row to call compile on their ng-included contents. The key for good performance in big table is to be able to generates (yea, manually, which $compile, $interpolate and stuff) a unique compiled row linking function, with less as possible angular directives - ideally only one-time expression bindings, and to handle row addiction/removal manually (no ng-repeat, you own directive, your own logic)
You should AT LEAST find a way to avoid the second nested ng-repeat on' ng-repeat="attobj in columns track by $index"'. This is a dual repeated on each row, killing compilation &linking (rendering perf) and watcher count (lifecycle perf)
EDIT : as asked, a "naive" example of what can be done to handle the table rendering as manually (and fast) as possible. Note that the example does not handle generating the table header, but it's usually not the hardest thing.
function myCustomRowCompiler(columns) {
var getCellTemplate = function(attribute) {
// this is tricky as i dont know what your "getAttributeTemplate" method does, but it should be able to return
// the cell template AS HTML -> you maybe would need to load them before, as getting them from your server is async.
// but for example, the naive example to display given attribute would be
return $('<span>').text("{{::model."+ attribute +"}}"); // this is NOT interpolated yet
};
var myRowTemplate = $('<tr class="tableRowsDocs">');
// we construct, column per column, the cells of the template row
_.each(columns, function(colAttribute, cellIdx) {
var cell = $("<td>");
cell.html(getCellTemplate());
cell.appendTo(myRowTemplate);
})
return $compile(myRowTemplate); // this returns the linking function
}
and the naive usage :
function renderTableRows(dbos, columns) {
var $scope; // this would be the scope of your TABLE directive
var tableElement = $el; // this would be your table CONTENT
var rowLinker = myCustomRowCompiler(columns); // note : in real life, you would compile this ONCE, but every time you add rows.
for(var i=0; i<dbos; i++) {
var rowScope = $scope.$new(); // creating a scope for each row
rowScope.model = dbos[0]; // injecting the data model to the row scope
rowLinker(rowScope, function(rowClone) { // note : you HAVE to use the linking function second parameter, else it will not clone the element and always use the template
rowClone.appendTo(tableElement);
});
}
};
This is the approach i've been using to my own projects's table framework (well, more advanced, but this is really the global idea), allowing to use angular power to render the cell content ( 'getCellTemplate' implementation can return html with directive, which will be compiled), using filter even including directives in the cell, but keeping the table rendering logic to myself, to avoid useless ng-repeat watch, and minimizing the compilation overheat to it's minimum.

AngularJS - controller method doesn't get called on ngClick - no error

I try to call the method removePlayer(playerId) if a button gets clicked. But, the method doesn't get called, or at least the statements inside it aren't firing, because I put a console.log() statement at the top.
The console is empty, so I'm really clueless. Here is my code:
Controller:
function appController($scope) {
$scope.players = [];
var playercount = 0;
$scope.addPlayer = function(playername) {
$scope.players.push({name: playername, score: 0, id: playercount});
playercount++;
}
function getIndexOfPlayerWithId(playerId) {
for (var i = $scope.players.length - 1; i > -1; i--) {
if ($scope.players[i].id == playerId)
return i;
}
}
$scope.removePlayer = function(playerId) {
console.log("remove");
var index = getIndexOfPlayerWithId(playerId);
$scope.players.slice(index, 1);
}
}
appController.$inject = ['$scope'];
HTML:
...
<table id="players">
<tr ng-repeat="player in players">
<td>{{player.name}}</td>
<td>{{player.score}}</td>
<td><button ng-click="removePlayer({{player.id}})">Remove</button></td>
</tr>
</table>
...
You shouldn't be using curly braces ({{ }}) in the ng-click expression. You should write:
<button ng-click="removePlayer(player.id)">Remove</button>
ng-repeat creates a new scope, so it doesn't know what removePlayer is. You should be able to do something like this:
<table id="players">
<tr ng-repeat="player in players">
<td>{{player.name}}</td>
<td>{{player.score}}</td>
<td><button ng-click="$parent.removePlayer({{player.id}})">Remove</button></td>
</tr>
</table>
See https://groups.google.com/forum/?fromgroups=#!topic/angular/NXyZHRTAnLA
As stated, ng-repeat creates it's own scope, and the outer controller scope is not available. But since in JS you are using true objects write something like this:
<tr ng-repeat="player in players">
<td>{{player.name}}</td>
<td>{{player.score}}</td>
<td><button ng-click="player.removePlayer()">Remove</button></td>
</tr>
Beforehand, somewhere on your controller initialization you can assing the "removePlayer" function to each of your player object and naturally code in anything you want, thus accessing outer controller indirectly.

Categories

Resources