I'm having an issue with a legacy app that's running Angular 1.5.9. The controller contains the following loop which is triggered by a "Select All" link on the page:
var len = $scope.payments.length, i;
for (i = 0; i < len; i++) {
$scope.payments[i].selected = true;
}
The selected property in the objects in the payments array is bound to checkboxes in the view:
<tr data-ng-repeat="payment in payments | orderBy: 'payDate'">
<td><input type="checkbox" data-ng-model="payment.selected" data-ng-change="setSelectedTotal()"/>...
There are up to 15000 items in the array/rows in the table, and the first time the Select All link is clicked after page load it takes up to 40 seconds for the view to refresh with all of the checkboxes checked. If I clear the checkboxes and then click the Select All link again, the checkboxes show as selected in about 1 second or less. This is true on all subsequent clicks of the Select All link-it's only slow the first time but takes a second or less every time after. I suspect this is related to something going on with model binding because when I surround the loop with console.time() and console.timeEnd(), the loop only takes a couple of milliseconds even on the first try. So the issue is with something that's happening after the loop completes. I've tried switching from ng-model to ng-checked just to see if it would speed things up but it gives me an error, and actually the app depends on the checkboxes being bound to the selected property. I also tried running a select all (followed by a clear all) on the first thousand checkboxes on page load but that didn't make any difference. Any insight into why it's so slow the first time and/or how to speed it up would be greatly appreciated.
Here is an example with some optimizations.
Note : I made use of document.querySelectorAll to select/unselect outside of controller as it's much faster than relying on $scope data.
angular.module('app', []);
angular.module('app')
.controller('ExampleController', ['$scope', function($scope) {
$scope.payments = [];
$scope.selected = false;
$scope.total = 0;
$scope.itemsCount = 7500;
// Populate with
populate($scope.itemsCount);
$scope.updateTotal = function() {
let total = 0;
for (let i = 0; i < $scope.payments.length; i++) {
if ($scope.payments[i].selected === true) {
total += $scope.payments[i].amount;
}
}
$scope.total = total;
}
$scope.toggleAll = function() {
// Toggle global selected state
$scope.selected = !$scope.selected;
for (let i = 0; i < $scope.payments.length; i++) {
$scope.payments[i].selected = $scope.selected;
}
$scope.updateTotal();
}
$scope.toggle = function(index) {
$scope.payments[index].selected = !$scope.payments[index].selected;
$scope.updateTotal();
}
function populate(count) {
for (let i = 0; i < count; i++) {
$scope.payments.push({
amount: i,
selected: false
});
}
}
}]);
// Toggle all checkbox
function vanillaToggleAll(event) {
var el = event.srcElement || event.target;
var checkboxes = document.querySelectorAll("input[type='checkbox']");
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = el.checked;
}
}
<!doctype html>
<html lang="en" ng-app="app">
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.4/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="ExampleController">
<h1>Items: {{itemsCount}}, Total: {{total}} USD</h1>
<table>
<thead>
<tr>
<td>
<input type="checkbox" ng-click="toggleAll()" onclick="vanillaToggleAll(event)">
<label>Select/Unselect All</label>
</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="payment in payments | orderBy: 'amount'">
<td>
<input type="checkbox" class="checkbox" ng-bind="payment.selected" ng-click="toggle($index)" />
<label ng-bind="::payment.amount"></label> USD
</td>
</tr>
</tbody>
</table>
</body>
</html>
A demo plunker with 7500 items to play with
And here are the results, I've used Chrome profiler to analyse time spent for loading, scripting, rendering...
1000 items
10000 items
15000 items
Related
Fiddle Example
I've a table in which each row has checkbox and another checkbox in to check-all rows (checkboxes) and send ID of selected/all row(s) as JSON object.
I've an object array from (GET) response (server-side pagination is enabled) and stored it in itemsList $scope variable.
Following is my code.
View
<table class="table">
<thead>
<tr>
<th><input type="checkbox" ng-model="allItemsSelected ng-change="selectAll()"></th>
<th>Date</th>
<th>ID</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in itemsList track by $index" ng-class="{selected: item.isChecked}">
<td>
<input type="checkbox" ng-model="item.isChecked" ng-change="selectItem(item)">
</td>
<td>{{item.date | date:'dd-MM-yyyy'}}</td>
<td>{{item.id}}</td>
</tr>
</tbody>
</table>
Controller
$scope.itemsList = [
{
id : 1,
date : '2019-04-04T07:50:56'
},
{
id : 2,
date : '2019-04-04T07:50:56'
},
{
id : 3,
date : '2019-04-04T07:50:56'
}
];
$scope.allItemsSelected = false;
$scope.selectedItems = [];
// This executes when entity in table is checked
$scope.selectItem = function (item) {
// If any entity is not checked, then uncheck the "allItemsSelected" checkbox
for (var i = 0; i < $scope.itemsList.length; i++) {
if (!this.isChecked) {
$scope.allItemsSelected = false;
// $scope.selectedItems.push($scope.itemsList[i].id);
$scope.selectedItems.push(item.id);
return
}
}
//If not the check the "allItemsSelected" checkbox
$scope.allItemsSelected = true;
};
// This executes when checkbox in table header is checked
$scope.selectAll = function() {
// Loop through all the entities and set their isChecked property
for (var i = 0; i < $scope.itemsList.length; i++) {
$scope.itemsList[i].isChecked = $scope.allItemsSelected;
$scope.selectedItems.push($scope.itemsList[i].id);
}
};
Below are the issues I'm facing...
If you check fiddle example than you can see that on checkAll() the array is updated with all available list. But if click again on checkAll() instead of remove list from array it again add another row on same object array.
Also i want to do same (add/remove from array) if click on any row's checkbox
If i manually check all checkboxes than the thead checkbox should also be checked.
I think that you are on the right path. I don't think is a good idea to have an array only for the selected items, instead you could use the isSelected property of the items. Here is a working fiddle: http://jsfiddle.net/MSclavi/95zvm8yc/2/.
If you have to send the selected items to the backend, you can filter the items if they are checked with
var selectedItems = $scope.itemsList.filter(function (item) {
return !item.isChecked;
});
Hope it helps
This will help you for one of the two doubts:
$scope.selectAll = function() {
if($scope.allItemsSelected){
for (var i = 0; i < $scope.itemsList.length; i++) {
$scope.itemsList[i].isChecked = $scope.allItemsSelected;
$scope.selectedItems.push($scope.itemsList[i].id);
}
}else{
for (var i = 0; i < $scope.itemsList.length; i++) {
scope.itemsList[i].isChecked = $scope.allItemsSelected;
}
$scope.selectedItems = [];
}
};
I'm looking for something to achieve solution to point 2.
ng-checked can be used but it is not good to use ng-checked with ng-model.
I have a function in my angular application that generates tables through directives. The fields in the tables need to have unique ng-model names so i can calculate each table seperatly.
I have solved the unique ng-model name with a counter that goes up for each table that is added and it adds the current count to the end of each ng-model name for each field.
(See my plunkr link for futher explanation).
I have a function in my app.js that will sum the fields. The function works very well when i have static ng-model names but i cant figure out how to concat the ng-model names with the current count so that the function can calculate each table seperatly when adding a number after each ng-model.
How do i fix my $scope.total function so that it works with the dynamic ng-model names?
My plunkr
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.count = 0;
$scope.total = function(count){
var total =
// I want to parseFloat the $scope.td1 + the current count.
// How does the syntax look when you concat ng-model.
parseFloat($scope.td1 + count || 0) +
parseFloat($scope.td2 + count || 0) +
parseFloat($scope.td3 + count || 0) +
parseFloat($scope.td4 + count || 0) +
parseFloat($scope.td5 + count || 0);
return total || 0;
}
});
Edit:
As a follow up question, i have added a new input to my plunkr that should display the sum of the "total" in the first two tables that are generated. This does not work as it is now and i cant figure out why.
I added a new function that should summerize the first two "total".
Use :
$scope['td1'+count]
In javascript you can access an object property either with pointed notation : object.property, or with named array notation : object['property'].
In your case, $scope.td1 + count will return the value of $scope.td1 + count.
$scope.td12 = 10;
$scope.td1 = 3;
count = 2;
$scope.td1 + count; // 5
$scope['td1'+count]; // $scope['td12'] == $scope.td12 == 10
update your total function to below code. It should work. Here is the updated plunk - http://plnkr.co/edit/vRSQOvRVkUKLZ1ihvBp4?p=preview
$scope.total = function(count){
// I want to parseFloat the $scope.td1 + the current count.
// How does the syntax look when you concat ng-model.
var total =
parseFloat($scope["td1" + count] || 0) +
parseFloat($scope["td2" + count] || 0) +
parseFloat($scope["td3" + count] || 0) +
parseFloat($scope["td4" + count] || 0) +
parseFloat($scope["td5" + count] || 0);
return total || 0;
}
A better approach is to create a self-contained table directive that keeps track of its own total and tells the parent scope whenever that changes.
This avoids having weird counters and cluttering up one scope with properties for every single input.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
// the totals for each of the tables
$scope.totals = [];
$scope.grandTotal = 0;
// whenever one of the table's total changes, it calls this function
// with its new total, so we can update our grand total
$scope.changeTotal = function(index, total) {
$scope.totals[index] = total;
$scope.grandTotal = sum($scope.totals);
}
});
app.directive('myTable', function() {
return {
restrict: 'E',
scope: {
onTotalChange: '&'
},
templateUrl: 'table.html',
link: function($scope) {
$scope.values = [];
// whenever one of the values the inputs are bound to changes,
// recalculate the total for this table and tell the parent scope
$scope.$watchCollection('values', function(values) {
$scope.onTotalChange({
total: sum(values)
});
});
}
}
});
// easy way to sum an array of values in modern browsers
function sum(values) {
return values.reduce(function(a, b) {
return a + b
}, 0);
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="plunker" ng-controller="MainCtrl">
<p>Grand total: {{grandTotal}}</p>
<!-- Whenever we click the button, add a new entry to our totals array
so a new table appears -->
<button ng-click="totals.push(0)">New table</button>
<!-- Render a new table for every entry in our totals array.
Whenever that table's total changes, update its total in the array. -->
<my-table ng-repeat="i in totals track by $index" on-total-change="changeTotal($index, total)">
</my-table>
<!-- This allows the directive to specify a templateUrl of 'table.html' but not
have to actually fetch it from the server. -->
<script type="text/ng-template" id="table.html">
<table>
<thead>
<tr>
<th>head 1</th>
<th>head 2</th>
<th>head 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="number" ng-model="values[0]">
</td>
<td>
<input type="number" ng-model="values[1]">
</td>
<td>
<input type="number" ng-model="values[2]">
</td>
</tr>
<tr>
<td>
<input type="number" ng-model="values[3]">
</td>
<td>
<input type="number" ng-model="values[4]">
</td>
<td>
<input type="number" ng-model="values[5]">
</td>
</tr>
</tbody>
</table>
</script>
</div>
So what I need to do is get only all of the shown (using ng-show) students checkboxes checked by clicking a toggleAll() button at the top of the page.
Here is my code:
<div ng-repeat="course in content" ng-show="course.show" >
<div ng-repeat="student in course.students" ng-show="student.show" ng-click="toggleStudent(student)">
<input type="checkbox">
........
I tried using:
$scope.toggleAll = function () {
for (var i = 0; i < $scope.course.students.length; i++) {
...
}
};
but length is undefined.
Any help would be greatly appreciated!
course is a local variable so it is not accessible on your main scope.
Assuming you want to check all for one course only, you should pass that course into the function.
<div ng-repeat="course in content" ng-show="course.show" >
<input type='checkbox' ng-click='toggleAll(course)'>
<div ng-repeat="student in course.students" ng-show="student.show" ng-click="toggleStudent(student)">
...
$scope.toggleAll = function (course) {
for (var i = 0; i < course.students.length; i++) {
...
}
};
I have two lists formed from an array containing objects.
I'm trying to move objects from one list to the other and vice versa.
Controller:
spApp.controller('userCtrl',
function userCtrl($scope,userService,groupService){
//Generate list of all users on the SiteCollection
$scope.users = userService.getUsers();
//array of objects selected through the dom
$scope.selectedAvailableGroups;
$scope.selectedAssignedGroups;
//array of objects the user actually belongs to
$scope.availableGroups;
$scope.assignedGroups;
//Generate all groups on the site
$scope.groups = groupService.getGroups();
//Boolean used to disable add/remove buttons
$scope.selectedUser = false;
//Take the selectedAvailableGroups, add user to those groups
//so push objects to "assignedGroups" array and remove from "avaiableGroups" array
$scope.addUserToGroup = function (){
userService.addUserToGroup($scope.selectedUser, $scope.selectedAvailableGroups, $scope.assignedGroups, $scope.availableGroups)
};
}
);
Service:
spApp.factory('userService', function(){
var addUserToGroup = function (selectedUser, selectedAvailableGroups, assignedGroups, availableGroups) {
var addPromise = [];
var selectLength = selectedAvailableGroups.length;
//Add user to selected groups on server
for (var i = 0; i < selectLength; i++) {
addPromise[i] = $().SPServices({
operation: "AddUserToGroup",
groupName: selectedAvailableGroups[i].name,
userLoginName: selectedUser.domain
});
};
//when all users added, update dom
$.when.apply($,addPromise).done(function (){
for (var i = 0; i < selectLength; i++) {
assignedGroups.push(selectedAvailableGroups[i]);
availableGroups.pop(selectedAvailableGroups[i]);
};
//alert(selectedUser.name + " added to: " + JSON.stringify(selectedAvailableGroups));
});
}
}
Object:
[{
id: 85,
name: Dev,
Description:,
owner: 70,
OwnerIsUser: True
}]
HTML:
<div>
<label for="entityAvailable">Available Groups</label>
<select id="entityAvailable" multiple
ng-model="selectedAvailableGroups"
ng-options="g.name for g in availableGroups | orderBy:'name'">
</select>
</div>
<div id="moveButtons" >
<button type="button" ng-disabled="!selectedUser" ng-click="addUserToGroup()">Add User</button>
<button type="button" ng-disabled="!selectedUser" ng-click="removeUserFromGroup()">Remove</button>
</div>
<div>
<label for="entityAssigned">Assigned Groups</label>
<select id="entityAssigned" multiple
ng-model="selectedAssignedGroups"
ng-options="g.name for g in assignedGroups | orderBy:'name'">
</select>
</div>
Right now, the push into assigned groups works but only updates when I click on something else or in the list, not really dynamically. But the biggest issue is the .pop() which I don't think works as intended.
$.when.apply($,addPromise).done() seems not to be angular api or synchronous. So angular is not aware of your changes. You must wrap your code inside a $scope.$apply call:
$scope.$apply(function(){
for (var i = 0; i < selectLength; i++) {
assignedGroups.push(selectedAvailableGroups[i]);
availableGroups.pop(selectedAvailableGroups[i]);
};
});
If you click on something, a $digest loop will happen and you will see your changes.
Your pop did not work because Array.pop only removes the last element. I guess that is not what you want. If you want to remove a specific element you should use Array.splice(),
I got a table that has an entry that looks like this:
<tr>
<td><input type="checkbox" name="ids[]"/><a style="cursor: pointer;" onclick="addtopath('parameter1', 'parameter2')" class="btn_addpath"> Add</a></td>
</tr>
As you can see every table entry countains the function "addtopath('parameter1', 'paramter2');"
The parameters are generated via php; so each item is different. Also, every entry has a checkbox. This is where the trouble occurs.
I want to create a function that runs the "addtopath" function for every table item, that is checked, as if the user clicked the "Add" button.
Hope it makes sense.
Modern browsers...
function runChecked() {
var links = mytable.querySelectorAll("input[name='ids[]']:checked + a.btn_addpath");
[].forEach.call(links, function(link) {
link.onclick();
});
}
IE8+...
function runChecked() {
var inputs = mytable.querySelectorAll("input[name='ids[]']");
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].checked)
inputs[i].nextSibling.onclick();
}
}
IE6+...
function runChecked() {
var inputs = mytable.getElementsByTagName("input");
for (var i = 0, len = inputs.length; i < len; i++) {
if (inputs[i].name === "ids[]" && inputs[i].checked)
inputs[i].nextSibling.onclick();
}
}
I would add the parameters to data attributes in case you want to move to jQuery at some point. It's also good practice.
<td><input type="checkbox" data-one="one" data-two="two" class="btn_addpath"/>Add</td>
<td><input type="checkbox" data-one="three" data-two="four" class="btn_addpath"/>Add</td>
<td><input type="checkbox" data-one="five" data-two="six" class="btn_addpath"/>Add</td>
<td><input type="checkbox" data-one="seven" data-two="eight" class="btn_addpath"/>Add</td>
function addToPath(p1, p2) {
console.log(p1, p2);
}
var checkboxes = document.getElementsByClassName('btn_addpath');
var checkboxArr = [].slice.call(checkboxes);
checkboxArr.forEach(function (el) {
var p1 = el.getAttribute('data-one');
var p2 = el.getAttribute('data-two');
el.onchange = function () {
if (this.checked) { addToPath(p1, p2); }
};
});
If you use jQuery you can use the following code:
$("input[type=checkbox]:checked").siblings('a').click();
Test it at http://jsfiddle.net/tMe46/
This should emulate onClick event at all links in checked boxes
$("input[type=checkbox]:checked + a").click();