AngularJS - Reducing the number of ng-repeats - javascript

I have a register that lists people alphabetically from A to Z... Each person, Mr A for example, has a set of corresponding data Histories.
I have a table with a simple ng-repeat that displays the data for 12 cells (representing 12months).
If 12 months/cells worth of data is supplied, i show all the data, if only 5 months of data (anything less than 12) is provided, i call a service srvEmptyCells that calculates the remaining cells and displays in a darker colour.
The problem with this is, that i notice i am repeating the ng-repeat:
emptyCell in getEmptyCells
many many times which is impacting page performance given i have over 100 users.
Is there a way i save the number of empty cells for each particular user? And remove the need for the extra ng-repeats? Would a directive improve things?
Heres a plunker: http://plnkr.co/edit/2UKDD1fvfYGMjX9oJqVu?p=preview
HTML:
<table ng-repeat="data in myData" class="my-table">
<caption>
{{ data.Name }}
</caption>
<tbody>
<tr>
<th>Rate</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Rate}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
<tr>
<th>Effort</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Effort}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
<tr>
<th>Advance</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Advance}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
<tr>
<th>Previous</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Previous}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
<tr>
<th>Current</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Current}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
<tr>
<th>Code</th>
<td ng-repeat="history in data.Histories.slice(0, 12)" class="my-table-cell">
{{history.Code}}
</td>
<td ng-repeat="emptyCell in getEmptyCells(data.Histories.slice(0, 12).length)" class="empty"></td>
</tr>
</tbody>
</table>
JS:
app.controller('MainCtrl', function($scope, $http, factoryGetJSONFile, srvEmptyCells) {
$scope.name = 'World';
factoryGetJSONFile.getMyData(function(data) {
$scope.myData = data.MyData.Entries;
});
$scope.getEmptyCells = srvEmptyCells.getEmptyCells;
});
app.factory('srvEmptyCells', function() {
return {
getEmptyCells: function(len) {
var emptyCells = [];
for(var i = 0; i < 12 - len; i++){
emptyCells.push(i);
}
return emptyCells;
}
};
});

You could modify $scope.myData by adding a cached version of data.Histories.slice(0, 12) and getEmptyCells(data.Histories.slice(0, 12).length) and using ng-repeat on those instead.
This way you'd save repeating those calls over and over again.
A better way, if you can modify the response, would be to do this service side rather than on the client. Why to return more than 12 histories if you won't need them?
It is hard to know if this will fix your performance issue, as there are many ng-repeats per row, but it will be faster than before at least.
Sample on caching client side;
factoryGetJSONFile.getMyData(function(data) {
$scope.myData = data.MyData.Entries;
for (int i=0; i < $scope.myData.length; i++) {
var current = $scope.myData[i];
current.nonEmptyCells = current.Histories.slice(0, 12);
current.emptyCells = srvEmptyCells.getEmptyCells(current.nonEmptyCells.length);
}
});
then change your ng-repeats in the html to:
<td ng-repeat="history in data.nonEmptyCells" class="my-table-cell">
and
<td ng-repeat="emptyCell in data.emptyCells" class="empty"></td>
updated plunkr: http://plnkr.co/edit/92S2rNNhBEyXZVsCI2M8?p=preview

I believe you can reduce the number of ng-repeat's to only 2.
You just need to change a little bit your approach. Instead of doing splice on every ng-repeat, use a list/array of indexes with a predefined length. Then, iterate over that list of indexes and for each index check if there is an item in the same position of the data.Histories array. If there is, then it's a 'my-table-cell' otherwise it's an 'empty' cell (ng-class helps you defining that).
Then use the same logic for the fields, instead of having 1 ng-repeat for each field, try to define a list/array of fields and then use only 1 ng-repeat to iterate over that list.
The final result should be something like this:
<table ng-repeat="data in myData" class="my-table">
<caption>
{{ data.Name }}
</caption>
<tbody>
<tr ng-repeat="field in fields">
<th>{{field}}</th>
<td ng-repeat="idx in indexes" ng-class="{ 'my-table-cell': data.Histories[idx][field], 'empty': !data.Histories[idx][field]}">
{{data.Histories[idx][field]}}
</td>
</tr>
</tbody>
</table>
And on the MainCtrl add the following lines:
// the list of indexes could be populated dynamically...
$scope.indexes = [0,1,2,3,4,5,6,7,8,9,10,11];
$scope.fields = ['Rate', 'Effort', 'Advance', 'Previous', 'Current', 'Code'];
Demo

Related

Create array of non hidden td rows

So I have a table where the users can filter out specific rows, by checking a checkbox. If a checkbox is selected then, some rows will get the hidden state.
I want to create a array with all the rows that isn't hidden, but I can't seem to get the state of the <td>.
The tables id is ftp_table and the rows I need the data from has the class name download. I tried to so something like this, to get the visibility value, but without any luck. The function is triggered after a hide row function has run.
function download_log() {
var rows = document.getElementsByClassName("download");
var log = [];
for (var i = 0; i < rows.length; i++) {
// Check if item is hidden or not (create a if and push into array)
console.log(getComputedStyle(rows[i]).visibility);
// append new value to the array IF NOT HIDDEN
log.push(rows.item(i).innerHTML);
}
}
The output I get when i hide something is everything is visible?:
Here is a example of the table, where all info rows has been hidden:
<table class="ftp_table" id="ftp_table">
<tbody>
<tr class="grey">
<th>Log</th>
</tr>
<tr class="info" hidden>
<td class="download">2021-10-06 12:38:15.946 INFO [conftest:101] -------------- Global Fixture Setup Started --------------</td>
</tr>
<tr class="debug">
<td class="download">2021-10-06 12:38:16.009 DEBUG [Geni:37] Initializing</td>
</tr>
<tr class="info" hidden>
<td class="download">2021-10-06 12:38:16.059 INFO [Wrapper:21] Downloading</td>
</tr>
<tr class="info grey" hidden>
<td class="download">2021-10-06 12:38:16.061 INFO [Handler:55] AV+</td>
</tr>
<tr class="debug grey">
<td class="download">2021-10-06 12:38:16.063 DEBUG [Session:84] GET'</td>
</tr>
</tbody>
</table>
You could use the following selector :
document.querySelectorAll("#ftp_table tr:not([hidden]) td.download");
It will select the td.download elements in tr that are not hidden in your table.
var visibleTds = document.querySelectorAll("#ftp_table tr:not([hidden]) td.download");
var arr = [];
for(let i = 0; i < visibleTds.length; i++){
arr.push(visibleTds[i].innerText);
}
console.log(arr);
<table class="ftp_table" id="ftp_table">
<tbody>
<tr class="grey">
<th>Log</th>
</tr>
<tr class="info" hidden>
<td class="download">2021-10-06 12:38:15.946 INFO [conftest:101] -------------- Global Fixture Setup Started --------------</td>
</tr>
<tr class="debug">
<td class="download">2021-10-06 12:38:16.009 DEBUG [Geni:37] Initializing</td>
</tr>
<tr class="info" hidden>
<td class="download">2021-10-06 12:38:16.059 INFO [Wrapper:21] Downloading</td>
</tr>
<tr class="info grey" hidden>
<td class="download">2021-10-06 12:38:16.061 INFO [Handler:55] AV+</td>
</tr>
<tr class="debug grey">
<td class="download">2021-10-06 12:38:16.063 DEBUG [Session:84] GET'</td>
</tr>
</tbody>
</table>
Why don't you use classList instead of style props ?
Like :
rows[i].classList.contains("hidden")

How can I implement dynamic rowspan

The code below is my latest attempt. There have been many others:
Javascript:
...
var issueArray = [];
_.each(issueGroups, function(i) {
var x = {
issue: i[0].issue,
comment: i[0].comment,
count: i.length,
new_row: 1
};
issueArray.push(x);
});
issueArray[0].new_row = 0;
var x = {
test: t[0].test,
issues: issueArray,
rowspan: issueArray.length
};
finalResult.push(x);
});
return finalResult;
The important thing to note here is that for each element of finalResult there is an array called issues that has a member called new_row which is true for all but the first row.
Template:
...
<tbody>
{{#each failuresByTest}}
<tr>
<td rowspan="{{rowspan}}">{{test}}</td>
{{#each issues}}
{{#if new_row}}
</tr>
<tr>
<td>{{issue}}</td>
<td>{{comment}}</td>
<td>{{count}}</td>
{{else}}
<td>{{issue}}</td>
<td>{{comment}}</td>
<td>{{count}}</td>
{{/if}}
{{/each}}
</tr>
{{/each}}
</tbody>
...
My data is such that the first element of finalResult has 3 issues. I expect it to look like this:
<tr>
<td rowspan="3">Test1</td>
<td>123</td>
<td>Bug 1</td>
<td>2</td>
</tr>
<tr>
<td>456</td>
<td>Bug 2</td>
<td>21</td>
</tr>
<tr>
<td>654</td>
<td>Bug 4</td>
<td>3</td>
</tr>
But it ends up looking like this:
<tr>
<td rowspan="3">Test1</td>
<td>123</td>
<td>Bug 1</td>
<td>2</td>
<td>
<table>
<tr>
<td>456</td>
<td>Bug 2</td>
<td>21</td>
</tr>
<tr>
<td>654</td>
<td>Bug 4</td>
<td>3</td>
</tr>
</table>
</td>
</tr>
However Meteor doesn't like this. I get:
=> Errors prevented startup:
While processing files with templating-compiler (for target web.browser):
client/templates/runs/run_page.html:128: Unexpected HTML close tag
</tr> <tr> <td><a h...
This side-tracked me so badly that I ended up putting the row tags wrongly to fix this and then it displayed wrongly. I have now edited the question accordingly since this is my real problem.
How do I solve this and persuade Meteor that actually I do know better than it!
Your template logic is flawed:
You are trying to insert a new <tr> inside a <td>. Since <tr> can only be contained inside tables, the browser will automatically add a table around it so that the html is valid.
<tbody>
{{#each failuresByTest}}
<tr>
<td rowspan="{{rowspan}}">{{test}}</td>
{{#each issues}}
{{#if new_row}}
<tr>
<td>
So depending on how each failureBytest should be rendered, you should either create a new table for each failure, or create a new row outside the previous row inseatd of inside it's cells.
I worked around Meteor's controlling behaviour with the following.
Javascript:
var issueArray = [];
_.each(issueGroups, function(i) {
var x = {
issue: i[0].issue,
comment: i[0].comment,
count: i.length,
new_row: 0
};
issueArray.push(x);
});
issueArray[0].new_row = 1;
_.extend(issueArray[0], {rowspan: issueArray.length});
_.extend(issueArray[0], {test: t[0].test});
var x = {
issues: issueArray
};
finalResult.push(x);
Note that the test name and rowspan have now moved into the first array element. I can probably remove new_row.
Template:
<tbody>
{{#each failuresByTest}}
{{#each issues}}
<tr>
{{#if new_row}}
<td rowspan="{{rowspan}}">{{test}}</td>
{{/if}}
<td>{{issue}}</td>
<td>{{comment}}</td>
<td>{{count}}</td>
</tr>
{{/each}}
{{/each}}
</tbody>
I really do not like putting "parent data" in the first array element. I prefer for all elements to be of exactly the same type/structure without duplicate/extra information added.

nested ng-repeat with open particular index with respect to repeated data

Every time the toggle is clicked, all payments are getting replaced with new payments. My problem is how to maintain the payments of a particular index of every click and show at respective index. please help me out
here is my html
<tbody data-ng-repeat="invoice in relatedInvoices>
<tr>
<td class="td-bottom-border">
{{invoice.PayableCurrencyCode}} {{invoice.PayablePaidAmount | number: 2}}<br />
<small>
<a data-ng-click="isOpenPayablePayments[$index] = !isOpenPayablePayments[$index]; togglePayablePayments(invoice.PayableInvoiceId)">Paid</a>
</small>
</td>
</tr>
<tr data-ng-show="isOpenPayablePayments[$index]">
<td>
<table>
<thead>
<tr>
<th>Transaction Id</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="payment in payablePayments">
<td>{{payment.TransactionId}}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
Here is my javascript
var getPayments = function (invoiceId) {
paymentService.getPayments(invoiceId).then(function (paymentsResponse) {
return paymentsResponse.data;
});
};
$scope.togglePayablePayments = function(invoiceId) {
$scope.payablePayments = getPayments(invoiceId);
};
If I understood correctly, you want to have "payablePayments" for every invoice.
This is working: http://plnkr.co/edit/cj3jxZ?p=info
Try something like
// init at beginning
$scope.payablePayments = [];
$scope.togglePayablePayments = function(invoiceId) {
$scope.payablePayments[invoiceId] = getPayments(invoiceId);
};
and then
<tr data-ng-repeat="payment in payablePayments[invoice.PayableInvoiceId]">
Otherwise you overwrite the object for the preceding invoice.

Ng-repeat is not working as expected in html table?

Why doesn't this simple table structure work in angularjs? None of the rows gets populated with the data. If I replace span with tr, it works fine but my third column of Percent doesn't fit in well.
<table class="table table-hover">
<tbody>
<tr>
<th>Grade</th>
<th>Point</th>
<th>Percent</th>
</tr>
<tr>
<span ng-repeat="gdata in gradepoints">
<td ng-repeat="(grade, gp) in gdata"> {{grade | uppercase}} </td>
<td ng-repeat="(grade, gp) in gdata"> {{gp}} </td>
</span>
<span ng-repeat="pdata in percents">
<td ng-repeat="(grade, perc) in pdata"> {{perc}} </td>
</span>
</tr>
</tbody>
</table>
Try this:
<tr ng-repeat="gdata in gradepoints">
<td>{{gdata.grade | uppercase}}</td>
<td>{{gdata.gp}}</td>
<td>{{pdata[$index].perc</td>
</tr>
You want one row for each element in the gdata array, so the ng-repeat needs to be on the <tr> element. If the two arrays aren't in sync, you can create a function in your controller to return the pdata element you need:
$scope.findPdata(gdata, index) {
// ... do your magic here to find the pdata element you want
return result;
}
<td>{{findPdata(gdata, $index)}}</td>
guessing from the below:
<tr>
<th>Grade</th>
<th>Point</th>
<th>Percent</th>
</tr>
i guess your table has 3 columns so your each row has 3 columns?
in that case use the following
<tr ng-repeat="gdata in gradepoints">
<td>{{gdata.grade | uppercase}}</td>
<td>{{gdata.gp}}</td>
<td>{{gdata.percent}}</td>
</tr>
for above to work your gradepoints must be an ARRAY containing objects with 3 properties each i.e. grade,gp,percent

how to merge 2 rows together WITH 2 rows in html table?

I am not so good in English, to express myself, so I show it with examples to you!
Here is my HTML table with code:
<table border="1">
<tr id="tr1_1">
<td rowspan="2">1</td>
<td>tr1.1 td2</td>
<td>tr1.1 td3</td>
<td rowspan="2">
up/
down
</td>
</tr>
<tr id="tr1_2">
<td>tr1.2 td2</td>
<td>td1.2 td3</td>
</tr>
<tr id="tr2_1">
<td rowspan="2">2</td>
<td>tr2.1 td2</td>
<td>tr2.1 td3</td>
<td rowspan="2">
up/
down
</td>
</tr>
<tr id="tr2_2">
<td>tr2.2 td2</td>
<td>td2.2 td3</td>
</tr>
</table>
(You can see the result here)
For example, if I click on the second two rows' "up" link, the result should be this:
<table border="1">
<tr id="tr2_1">
<td rowspan="2">2</td>
<td>tr2.1 td2</td>
<td>tr2.1 td3</td>
<td rowspan="2">
up/
down
</td>
</tr>
<tr id="tr2_2">
<td>tr2.2 td2</td>
<td>td2.2 td3</td>
</tr>
<tr id="tr1_1">
<td rowspan="2">1</td>
<td>tr1.1 td2</td>
<td>tr1.1 td3</td>
<td rowspan="2">
up/
down
</td>
</tr>
<tr id="tr1_2">
<td>tr1.2 td2</td>
<td>td1.2 td3</td>
</tr>
</table>
(You can see the final result here)
So how could I do this? I know the prev() and before() methods in javascript, but it merge only 1 row with 1 row, but I want to merge 2 rows WITH 2 rows!
I hope, someone can help me! Thank you!
Try this jQuery code :
$('.up').click(function() {
tr = $('tr[id^="'+$(this).closest('tr').attr('id').slice(0, 3)+'"]')
tr.insertBefore(tr.eq(0).prev().prev())
return false;
})
$('.down').click(function() {
tr = $('tr[id^="'+$(this).closest('tr').attr('id').slice(0, 3)+'"]')
tr.insertAfter(tr.eq(1).next().next())
return false;
})
And have a look to your fiddle edited : http://jsfiddle.net/lulu3030/UQz8u/6/
Just some explanations :
closest('tr') method find the nearest parent which has a tr tag
slice(0, 3) get the first 3 characters of a string
=> variable tr selects all elements which has the same 3 first id characters
insertBefore and insertAfter methods allows to move selected elements
Could be done like that too:
DEMO
$(function () {
$('.up, .down').on('click', function () {
var $tr = $(this).closest('tr'),
$flag = $('<tr/>').insertBefore($tr),
$rows = $tr.add($tr.next('tr')).detach()
methods = $(this).is('.up') ? ['insertBefore', 'prevAll'] : ['insertAfter', 'nextAll'];
if ($flag[methods[1]]('tr').eq(1).length) {
$rows[methods[0]]($flag[methods[1]]('tr').eq(1));
$flag.remove();
} else $flag.replaceWith($rows);
});
});

Categories

Resources