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>
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 am trying to change the code that every time a user edits the marks in English or Hindi and then the highest Hindi and/or English marks are updated accordingly
CONTROLLER CODE
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope)
{
$scope.names = [
{name:'Priya',age:'19',gender:'Female',English:x[0], Hindi:x[1]},
....
....
{name:'Dev', age:'18' ,gender:'Male',English:x[2] ,Hindi:x[3]},
];
$scope.sum = function(list)
{
var total=0;
angular.forEach(list , function(x){
total+= x[];
});
return total;
}
});
HTML CODE
This is the code I use currently for displaying the highest marks
<tr ng-repeat ="x in names | orderBy:'sortColumn'">
<td>{{x.name}}</td>
<td>{{x.age}}</td>
<td>{{x.gender}}</td>
<td><input type="text" ng-model="x.English"></td>
<td><input type="text" ng-model="x.Hindi"></td>
<td ng-bind="avg=(x.English+x.Hindi)/2">{{avg}}</td>
<td>
<button>Delete</button>
</td>
</tr>
</table>
<table ng-model="sum">
<tr><td>The total is: <input value="{{sum(x)}}"></td></tr>
**<tr><td>THIS IS WHERE I WANT TO DISPLAY HIGHEST MARKS IN ENGLISH</td></tr>**
**<tr><td>**<td ng-repeat="x in names |orderBy:'-Hindi' | limitTo:1"> {{x.name}}THIS IS WHERE I WANT TO DISPLAY HIGHEST MARKS IN HINDI</td></tr>**
</table>
You can put watch on array like below:
$scope.total = 0
$scope.$watch('names', function(newVal) {
var total = 0;
angular.forEach(newVal, function(x) {
total += parseInt(x.English) + parseInt(x.Hindi);
});
$scope.total = total
}, true);
and in template replace {{sum(x)}} with {{total}}.
Check working example here
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
I'm trying to compare an old value in a <td> with the new value entered by the end user. I'm using ng-blur to detect when there is focus out of the field rather than calling the function. The problem is I can't get this very simple logic to work and I can't figure out why.
This is my table:
<main ng-controller="mainCtrl">
<table class="table table-bordered table-hover" ng-controller="tableCtrl">
<p ng-model="old">{{old}}</p>
<thead>
<th>user name</th>
<th>script name</th>
<th>cron format<span class="glyphicon glyphicon-question-sign" data-toggle="tooltip" data-original-title="Min|Hour|Day Of Month|Month|Day Of Week"></span></th>
</thead>
<tbody ng-repeat="(user_id,script_id) in data">
<tr ng-repeat="(script_id, cron_format) in script_id">
<td class="userName">{{user(user_id)}}</td>
<td class="scriptName">{{script(script_id)}}</td>
<td class="cronFormat"><input type="text" ng-model="cron_format" ng-blur="saveCron(user_id,script_id,cron_format)"/></td>
</tr>
</tbody>
</table>
and this is the comparison :
$scope.old = $scope.cron_format = "";
$scope.saveCron(user_id, script_id, cron_format) {
if ($scope.old == $scope.cron_format) {
return; //value was unchanged
}
$.post("updateCronChange.php", "user_id=" + userId + "&script_id=" + scriptId + "&cron_format=" + cronFormat, function (data) {
alert('cron format changed to:' + cronFormat);
});
$scope.old = $scope.cron_format;
});
Currently, the function executes each time the field is out of focus. What am I missing here?
You can use ng-init for each iteration of the ng-repeat to store the old value, and then use it in the function to compare:
<tr ng-repeat="row in data" ng-init="oldCron = row.cron_format">
And in the function call:
ng-click="saveCron(row.user_id,row.script_id,row.cron_format,oldCron)"
And finally inside the function itself:
$scope.saveCron = function(userId,scriptId,cronFormat,oldCron){
console.log("old cron: " + oldCron);
console.log("new cron: " + cronFormat);
if (oldCron != cronFormat) {
//assign
}
}
Fiddle.
If I understand you correctly, you are trying to save an old value and print it out. You can try changing it to this -
if ($scope.old != $scope.cron_format) {
$.post("updateCronChange.php", "user_id=" + userId + "&script_id=" + scriptId + "&cron_format=" + cronFormat, function(data) {
alert('cron format changed to:' + cronFormat);
});
$scope.old = $scope.cron_format;
}
Here is a working example if you like -
var test = angular.module('test', []);
test.controller('TestCtrl', ['$scope',
function($scope) {
$scope.myModel = '';
$scope.old = '';
$scope.getNewValue = 'false'
$scope.saveModel = function(value) {
if ($scope.old != value) {
$scope.getNewValue = 'true';
$scope.old = value;
$scope.myModel = '';
// Here you can do your post request
} else {
$scope.getNewValue = 'false';
}
};
}
]);
.green {
color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
<div ng-app='test'>
<div ng-controller="TestCtrl">Enter Some Text
<input type="text" ng-model="myModel" ng-blur="saveModel(myModel)">
<p>Old Value - {{old}}</p>
<p>Current Value - {{myModel}}</p>
<p class="green">Get New Value - {{getNewValue}}</p>
</div>
</div>
Let's say we have an observable array of N+1 elements. What i would like to achive is to build 3 by X grid with buttons from these elements. Like phone keypad or something like this.
What i have done so far is created a table with foreach and if in it:
<table>
<tbody data-bind="foreach: tableList">
<!-- ko if: isEof($index()) -->
<tr>
<!-- /ko -->
<td><button data-bind="text: name"></button></td>
<!-- ko if: isEof($index()) -->
</tr>
<!-- /ko -->
</tbody>
</table>
isEof() function should determine by list index wether we have already 3 elements rendered. If yes, then it will render tags. Also if index is 0, then it also renders elements.
This is the code of the function:
function TableItem(data){
this.id=ko.observable(data.id);
this.name=ko.observable(data.name);
this.isEof = function(index){
if(index ==0){
return true;
}else{
if((index+3) % 3 === 0){
return true;
}else{
return false;
}
}
}
}
But i'm facing two problems with this setup.
1) With these if blocks enabled, button name binding does not work. When i remove ko if blocks, it will be rendered correctly.
2) ko if statements does not seem to work correctly. It will render out only those lines, where is also allowed to render.
I've made JSFiddle sample of my solution: http://jsfiddle.net/kpihus/3Lw7xjae/2/
I would create a ko.computed that converts your list of table items into an array of arrays:
var TableItem = function TableItem(data) {
this.id = ko.observable(data.id);
this.name = ko.observable(data.name);
};
var Vm = function Vm() {
this.tableItems = ko.observableArray([]);
this.columnCount = ko.observable(3);
this.columns = ko.computed(function() {
var columns = [],
itemCount = this.tableItems().length,
begin = 0;
// we use begin + 1 to compare to length, because slice
// uses zero-based index parameters
while (begin + 1 < itemCount) {
columns.push( this.tableItems.slice(begin, begin + this.columnCount()) );
begin += this.columnCount();
}
return columns;
// don't forget to set `this` inside the computed to our Vm
}, this);
};
vm = new Vm();
ko.applyBindings(vm);
for (var i = 1; i < 15; i++) {
vm.tableItems.push(new TableItem({
id: i,
name: "name: " + i
}));
}
This way, you can display your table by nesting two foreach bindings:
<table>
<tbody data-bind="foreach: { data: columns, as: 'column' }">
<tr data-bind="foreach: column">
<td>
<button data-bind="text: name">A</button>
</td>
</tr>
</tbody>
</table>
JSFiddle
If you haven't worked with ko.computed before, they track whatever observables are accessed inside of them — in this case, this.tableItems and this.columnCount —, and whenever one of them changes, they run again and produce a new result.
this.columns takes our array of table items
[TableItem, TableItem, TableItem, TableItem, TableItem, TableItem]
and groups them by this.columnCount into
[[TableItem, TableItem, TableItem], [TableItem, TableItem, TableItem]]
We can achieve this by simply passing $root which will have tableList and do the looping in simple way .
View Model:
function TableItem(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.pickarray = ko.observableArray();
self.isEof = function (index, root) {
if (index === 0) {
self.pickarray(root.tableList().slice(index, index + 3));
return true;
} else if ((index + 3) % 3 === 0) {
self.pickarray(root.tableList().slice(index, index + 3));
return true;
} else {
return false;
}
}
}
var vm = function () {
this.tableList = ko.observableArray();
for (var i = 1; i < 15; i++) {
this.tableList.push(new TableItem({
id: i,
name: "name: " + i
}));
}
}
ko.applyBindings(new vm());
View :
<table data-bind="foreach:tableList">
<tr data-bind="if:isEof($index(),$root)">
<!-- ko foreach: pickarray -->
<td>
<button data-bind="text: id"></button>
</td>
<!-- /ko -->
</tr>
</table>
Trick here is very simple we just need to conditionally fill pickarray which does the job for us .
Working Fiddle here