Calculate summary with AngularJs on selected checkboxes - javascript

I'm learning the AngularJs and I've created a dynamic list with ng-repeat, but now I cannot find the solution for the next step...
I have the following code:
HTML:
<div ng-app="app">
<div class="list" ng-controller="ListController">
<div class="row" ng-repeat="prod in prodList">
<div class="prod-name cell">
<input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" value="{{ prod.id }}" data-cb="" /><label for="prod-{{ prod.id }}">{{ prod.name }}</label>
</div>
<div class="prod-size cell">
<label for="prodColor-#{{ prod.id }}">
<select id="prodColor-{{ prod.id }}" data-ng-model="orders.prods.color[prod.id]" data-ng-options="color.id as color.name for color in prod.colors">
<option value="">Select one</option>
</select>
</label>
</div>
<div class="prod-price cell">
{{ prod.price }}
</div>
</div>
<div class="sum">
{{ sum }}
</div>
</div>
</div>
JS:
var app = angular.module("app", [], function () {
});
app.controller('ListController', function ($scope) {
init();
function init () {
$scope.prodList = [{"id":"1","name":"window","colors":[{"id":"9","name":"white"},{"id":"11","name":"black"}],"price":"100"},{"id":"2","name":"door","colors":[{"id":"22","name":"blue"},{"id":"23","name":"red"}],"price":"356"},{"id":"3","name":"table","colors":[{"id":"37","name":"white"},{"id":"51","name":"black"}],"price":"505"}];
$scope.orders = {};
$scope.orders.prods = {};
$scope.orders.prods.prod = {};
$scope.orders.prods.color = {};
$scope.sum = 0;
}
});
Working demo:
http://jsfiddle.net/HDrzR/
Question
How can I calculate the summary of selected product's price into the $scope.sum?
Edited:
So if you select the "window" and the "table" from the list, the sum should contains 606. But if you unselect the "table" then the sum should be 100.
Thank you in advance!

You could either $watch over $scope.orders.prod and re-calculate the sum or (if performance is not a major concern and the list of objects is not huge) you could use a sum() function and reference it in the view:
Total: {{sum()}}
$scope.sum = function () {
return $scope.prodList.filter(function (prod) {
return $scope.orders.prods.prod[prod.id];
}).reduce(function (subtotal, selectedProd) {
return subtotal + parseInt(selectedProd.price);
}, 0);
};
/* If the above looks complicated, it is the equivalent of: */
$scope.sum = function () {
var sum = 0;
for (var i = 0; i < $scope.prodList.length, i++) {
var prod = $scope.prodList[i];
if ($scope.orders.prods.prod[prod.id]) {
sum += parseInt(prod.price);
}
}
return sum;
}
If you decide to go down the watching path, this is how the code should look like:
Total: {{watchedSum}}
$scope.sum = ...; // same as above
$scope.$watchCollection('orders.prods.prod', function (newValue, oldValue) {
if (newValue !== undefined) {
$scope.watchedSum = $scope.sum();
}
});
UPDATE:
"Stole" Quad's $watchCollection as this is more "cheap" than $watch(..., true).
As long as $scope.order.prods.prod contains only rimitive values (i.e. no objects), $watchCollection is enough to detect the changes.
See, also, this short demo illustrating both approaches.

here is an updated fiddle.
http://jsfiddle.net/HRQ9u/
what I did is add a $scope.$watch on the orders:
$scope.$watch('orders', function (newValue, oldValue) {
var findProdById = function (products, id) {
for (var i = 0; i < products.length; i++) {
if (parseInt(products[i].id, 10) === parseInt(id, 10)) {
return products[i];
}
}
return null;
};
var orders = newValue;
var usedProducts = newValue.prods.prod;
var sum = 0;
for ( var prop in usedProducts ){
var prodList = $scope.prodList;
var id = prop;
if ( usedProducts[id] ){
var product = findProdById(prodList, id);
if ( product !== null ){
sum += parseInt(product.price, 10);
}
}
}
$scope.sum = sum;
}, true);
Edit:
With that said, you can probably simplify your model a lot. And I mean A LOT.
Also, I have used only regular js as opposed to lodash or underscore.

Register an ng-click with checkbox and pass $event & prod.id references to a custom function that will be called on checkbox click event.
<input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" value="{{ prod.price }}" ng-click="calcSum($event, prod.id)" data-cb="" />
Within controller define custom function as:
$scope.calcSum = function($event, id) {
var checkbox = $event.target;
if (checkbox.checked ) {
$scope.sum += parseInt(checkbox.value);
} else {
$scope.sum -= parseInt(checkbox.value);
}
}
Change the Checkbox value attribute to store {{ prod.price }} instead of {{ prod.id }}, it makes more sense.

From my POV it looks more appropriate to use is ngChange from the input[checkbox] itself. Like:
ng-change="addMe(prod.price);"
We need a way to differentiate checked from not checked and in order to change less your app we will use the existence of the object by it doing this way:
ng-change="addMe(orders.prods.prod[prod.id],prod.price);"
ng-true-value="{{ prod.id }}"
ng-false-value=""
The full HTML will look like this:
<input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" ng-change="addMe(orders.prods.prod[prod.id],prod.price);" ng-true-value="{{ prod.id }}" ng-false-value="" />
From your Controller all you need is to add or substract when necessary:
$scope.addMe=function(checked,val){
val = 1*val
if (!checked){ $scope.sum = $scope.sum-val;}
else { $scope.sum = $scope.sum +val; }
}
Here is the Online Demo

Updated your fiddle FIDDLE
You can $scope.$watchCollection your scope to calcuate new sum. I chaged your prodList prices to number instead of string, and used underscore.js for easier looping in your objects/arrays.
$scope.$watchCollection("orders.prods.prod",function(){
var sum = 0;
for(var i = 0;i<$scope.prodList.length;i++){
if($scope.orders.prods.prod[$scope.prodList[i].id]){
sum += $scope.prodList[i].price;
}
}
$scope.sum = sum;
});
watchCollection docs are here
edit: changed with regular js answer

Related

Strange behavior when splicing array, javascript

I'm working with list of checkboxes and I have next logic behavior for it:
if all items selected, checkbox "select all" is checked
if one of all selected items has been unchecked, checkbox "select all" is unchecked as well
This logic is clear. Depends of what item is checked I extract its id to an additional array and then using this array for request that to get data.
For pushing everything works fine, but for slicing the logic is strange. So I can slice the array until first item is checked, however I unchecked the first item, pushed and sliced items no more related with checkboxes.
I have reproduced plunker with it, so I appreciate if anybody could help me to find what I'm missing.
$scope.modelClass = {
selectedAll: false
};
$scope.selectAllClass = function (array) {
angular.forEach(array, function (item) {
item.selected = $scope.modelClass.selectedAll;
$scope.param =''
});
};
$scope.checkIfAllClassSelected = function (array) {
$scope.modelClass.selectedAll = array.every(function (item) {
return item.selected == true
});
$scope.checked = array.filter(function (item) {
return item.selected == true
}).length;
angular.forEach(array, function (obj) {
if(obj.selected == true){
requestClass(obj)
}
});
};
var selectedClass = [];
var requestClass = function (obj) {
selectedClass.push(obj);
angular.forEach(selectedClass, function (val) {
if (val.selected != true) {
selectedClass.splice(selectedClass.indexOf(val.id), 1);
}
else {
selectedClass = selectedClass.filter(function (elem, index, self) {
return index == self.indexOf(elem);
})
}
});
$scope.param = _.map(selectedClass, 'id')
};
$scope.classes = [
{"id":4,"name":"Achievement","selected":false},
{"id":13,"name":"Information","selected":false},
{"id":6,"name":"Issue","selected":false},
{"id":5,"name":"Message","selected":false},
{"id":9,"name":"Request","selected":false}
]
The logic looks good for me, not sure what's wrong here. I've took the first solution from this post (it looks like you are using the second one) and slightly modified it for your needs.
$scope.model = {
selectedClass : []
}
$scope.isSelectAll = function(){
$scope.model.selectedClass = [];
if($scope.master){
$scope.master = true;
for(var i=0;i<$scope.classes.length;i++){
$scope.model.selectedClass.push($scope.classes[i].id);
}
}
else{
$scope.master = false;
}
angular.forEach($scope.classes, function (item) {
item.selected = $scope.master;
});
$scope.param = $scope.model.selectedClass
}
$scope.isChecked = function() {
var id = this.item.id;
if(this.item.selected){
$scope.model.selectedClass.push(id);
if($scope.model.selectedClass.length == $scope.classes.length ){$scope.master = true;
}
} else {
$scope.master = false;
var index = $scope.model.selectedClass.indexOf(id);
$scope.model.selectedClass.splice(index, 1);
}
$scope.param = $scope.model.selectedClass
}
$scope.classes = [
{"id":4,"name":"Achievement","selected":false},
{"id":13,"name":"Information","selected":false},
{"id":6,"name":"Issue","selected":false},
{"id":5,"name":"Message","selected":false},
{"id":9,"name":"Request","selected":false}
]
html
<div ng-class="{'selected': master, 'default': !master}">
<div>
<input type="checkbox" ng-model="master" ng-change="isSelectAll()" > Select all
</div>
</div>
<div ng-repeat="item in classes | orderBy : 'id'" ng-class="{'selected': item.selected, 'default': !item.selected}">
<div >
<input type="checkbox" ng-model="item.selected" ng-change="isChecked()">
{{ item.name }}
</div>
</div>
this is fixed plunker

Couldn't append span element to array object in Angularjs/Jquery

Am struggling hard to bind an array object with list of span values using watcher in Angularjs.
It is partially working, when i input span elements, an array automatically gets created for each span and when I remove any span element -> respective row from the existing array gets deleted and all the other rows gets realigned correctly(without disturbing the value and name).
The problem is when I remove a span element and reenter it using my input text, it is not getting added to my array. So, after removing one span element, and enter any new element - these new values are not getting appended to my array.
DemoCode fiddle link
What am I missing in my code?
How can I get reinserted spans to be appended to the existing array object without disturbing the values of leftover rows (name and values of array)?
Please note that values will get changed any time as per a chart.
This is the code am using:
<script>
function rdCtrl($scope) {
$scope.dataset_v1 = {};
$scope.dataset_wc = {};
$scope.$watch('dataset_wc', function (newVal) {
//alert('columns changed :: ' + JSON.stringify($scope.dataset_wc, null, 2));
$('#status').html(JSON.stringify($scope.dataset_wc));
}, true);
$(function () {
$('#tags input').on('focusout', function () {
var txt = this.value.replace(/[^a-zA-Z0-9\+\-\.\#]/g, ''); // allowed characters
if (txt) {
//alert(txt);
$(this).before('<span class="tag">' + txt.toLowerCase() + '</span>');
var div = $("#tags");
var spans = div.find("span");
spans.each(function (i, elem) { // loop over each spans
$scope.dataset_v1["d" + i] = { // add the key for each object results in "d0, d1..n"
id: i, // gives the id as "0,1,2.....n"
name: $(elem).text(), // push the text of the span in the loop
value: 3
}
});
$("#assign").click();
}
this.value = "";
}).on('keyup', function (e) {
// if: comma,enter (delimit more keyCodes with | pipe)
if (/(188|13)/.test(e.which)) $(this).focusout();
if ($('#tags span').length == 7) {
document.getElementById('inptags').style.display = 'none';
}
});
$('#tags').on('click', '.tag', function () {
var tagrm = this.innerHTML;
sk1 = $scope.dataset_wc;
removeparent(sk1);
filter($scope.dataset_v1, tagrm, 0);
$(this).remove();
document.getElementById('inptags').style.display = 'block';
$("#assign").click();
});
});
$scope.assign = function () {
$scope.dataset_wc = $scope.dataset_v1;
};
function filter(arr, m, i) {
if (i < arr.length) {
if (arr[i].name === m) {
arr.splice(i, 1);
arr.forEach(function (val, index) {
val.id = index
});
return arr
} else {
return filter(arr, m, i + 1)
}
} else {
return m + " not found in array"
}
}
function removeparent(d1)
{
dataset = d1;
d_sk = [];
Object.keys(dataset).forEach(function (key) {
// Get the value from the object
var value = dataset[key].value;
d_sk.push(dataset[key]);
});
$scope.dataset_v1 = d_sk;
}
}
</script>
Am giving another try, checking my luck on SO... I tried using another object to track the data while appending, but found difficult.
You should be using the scope as a way to bridge the full array and the tags. use ng-repeat to show the tags, and use the input model to push it into the main array that's showing the tags. I got it started for you here: http://jsfiddle.net/d5ah88mh/9/
function rdCtrl($scope){
$scope.dataset = [];
$scope.inputVal = "";
$scope.removeData = function(index){
$scope.dataset.splice(index, 1);
redoIndexes($scope.dataset);
}
$scope.addToData = function(){
$scope.dataset.push(
{"id": $scope.dataset.length+1,
"name": $scope.inputVal,
"value": 3}
);
$scope.inputVal = "";
redoIndexes($scope.dataset);
}
function redoIndexes(dataset){
for(i=0; i<dataset.length; i++){
$scope.dataset[i].id = i;
}
}
}
<div ng-app>
<div ng-controller="rdCtrl">
<div id="tags" style="border:none;width:370px;margin-left:300px;">
<span class="tag" style="padding:10px;background-color:#808080;margin-left:10px;margin-right:10px;" ng-repeat="data in dataset" id="4" ng-click="removeData($index)">{{data.name}}</span>
<div>
<input type="text" style="margin-left:-5px;" id="inptags" value="" placeholder="Add ur 5 main categories (enter ,)" ng-model="inputVal" />
<button type="submit" ng-click="addToData()">Submit</button>
<img src="../../../static/app/img/accept.png" ng-click="assign()" id="assign" style="cursor:pointer;display:none" />
</div>
</div>
<div id="status" style="margin-top:100px;"></div>
</div>
</div>

Set a property of scope object dynamically in Angular Js

I am writing a generic method to get some data from a service and populate in the dynamic property name passed in the function. the value does gets assigned to the text box using angular.element assignment but does not gets populated in the model. following is the code.
<div class="input-group">
<input class="form-control" id="ImgRollover" name="ImgRollover" ng-model="contentEntity.imgRollover" placeholder="" readonly="readonly" type="text">
<div class="input-group-btn">
<button class="btn" type="button" ng-click="pickImage('contentEntity.imgRollover')">
</button>
</div>
here is my controller method which internally uses a service which sends back a promise
$scope.pickImage = function (attrModel) {
ImageSelector.selectImage().then(
function (value) {
//angular.element("#" + attrModel).val(value);
$scope[attrModel] = value;
});
};
attrModel is a property name in the scope object contentEntity but the name of the property is known ONLY dynamically via the method parameter.
<button class="btn" type="button" ng-click="pickImage('contentEntity', 'imgRollover')"></button>
$scope.pickImage = function (attrModel1, attrModel2) {
ImageSelector.selectImage().then(function (value) {
$scope.[attrModel1][attrModel2] = value;
});
};
should work
I know this has already been well answered but I wanted to make a dynamic property creator.
It splits attrModel by '.' and then edits $scope and adds and/or returns each property if it either exists already or not, we preserve the last key outside of the while loop so that the value just has to be appended to it.
$scope.pickImage = function (attrModel) {
ImageSelector.selectImage().then(
function (value) {
var parent = $scope,
current,
attribute,
attributes = attrModel.split('.');
while(attributes.length > 1 &&
(attribute = attributes.shift()) &&
(current = parent[attribute] || (parent[attribute] = {}))) {
parent = current;
}
current[attributes[0]] = value;
});
};
Of course, if you want to do it the angular way you'd have to use a service in order to do that, it could look like this
jsfiddle here
angular.module('myApp').service('ObjectWalker', function () {
var getNodeData = function (object, path) {
var parent = object,
current,
attribute,
attributes = path.split('.');
while(attributes.length > 1 &&
(attribute = attributes.shift()) &&
(current = parent[attribute] || (parent[attribute] = {}))) {
parent = current;
}
return [current, attributes[0]];
};
return {
set: function(object, path, value) {
var nodeData = getNodeData(object, path);
nodeData[0][nodeData[1]] = value;
},
get: function(object, path) {
var nodeData = getNodeData(object, path);
return nodeData[0][nodeData[1]];
}
};
})
There is already an answer but, just like to post something for dynamic properties...
<!DOCTYPE html>
<html>
<head>
<script src="https://code.angularjs.org/1.2.23/angular.js"></script>
<script type="text/javascript">
var value = 0;
function mainCtrl($scope) {
value++;
$scope.pickImage = function (attrModel) {
value++;
alert(attrModel)
$scope[attrModel] = value;
};
$scope.getValue = function(attrModel) {
return $scope[attrModel];
}
}
</script>
</head>
<body ng-app ng-controller="mainCtrl">
<input type="text" ng-model="test.obj" />
<br/>
<button ng-click="pickImage(test.obj)">test</button>
<br/>
display the value afoter button click,
note there is no single quote
<br/>
value: {{ getValue(test.obj) }}
</body>
</html>

AngularJS - Passing Values To Service On Click

I have a simple ng-click, which at present, is passing a $index value from an ng-repeat via ng-init.
What i would like to know is how can i pass another defined value into my ng-click?
Current HTML:
<div ng-repeat="values in Categories" id="history-{{$index}}" ng-init="sectionIndex = $index">
{{ values.Name }}, {{ values.Amount }}
</div>
<div>
<div class="left" ng-click="scrollLeft(sectionIndex)"></div>
<div class="right" ng-click="scrollRight(sectionIndex)"></div>
</div>
JS:
$scope.scrollLeft = scrollLeftRight.moveLeft;
$scope.scrollRight = scrollLeftRight.moveRight;
app.factory('scrollLeftRight', function () {
return {
moveLeft: function (sectionIndex) {
var scrollViewport_width = $(window).width();
var pixelsToMove = 0;
$('#history-' + sectionIndex).scrollLeft(pixelsToMove - 100);
pixelsToMove = pixelsToMove - 100;
if (pixelsToMove <= 0) {
pixelsToMove = 0; // reset
} else {
pixelsToMove = pixelsToMove;
}
},
moveRight: function (sectionIndex) {
var scrollViewport_width = $(window).width();
var pixelsToMove = 0;
$('#history-' + sectionIndex).scrollLeft(pixelsToMove + 100);
pixelsToMove = pixelsToMove + 100;
if (pixelsToMove >= scrollViewport_width) {
pixelsToMove = scrollViewport_width;
} else {
pixelsToMove = pixelsToMove;
}
}
};
});
What i would like to know, is how can i pass another value in my ng-init (if possible), so something like:
<div ng-repeat="values in Categories" id="history-{{$index}}" ng-init="sectionIndex = $index, tableID='history'">
So that my second parameter tableID, can be passed into my function:
moveRight: function (sectionIndex, tableID) {
And so that i no longer have to directly outline my id via:
$('#history-' + sectionIndex).scrollLeft(pixelsToMove - 100);
But instead:
$(tableID + sectionIndex).scrollLeft(pixelsToMove - 100);
There are Some ways to do this : I know 2 of them :p
1- Dummy way you can add A data-yourVariable into your Tag like this :
<div ng-repeat="values in Categories" id="history-{{$index}}" ng-init="sectionIndex = $index" data-table="history">
And in your ng-click you can do like this :
ng-click(sectionIndex,this.parent().data('table'));
2- Simple way : you can pass an object in your ng-init like this :
ng-init="{sectionIndex: $index, tableID:'history}"
NOTE: You must use(:) instead of (=) in your ng-init because you want to use it as an object
So in your ng-click :
ng-click(sectionIndex,tableID);
Hope that helps

knockout binding after array updated

I have an array that I'm removing items from but I'm keeping count of the number of items to do UI formatting. I need to be able to have the bind update.
ko.applyBindings(viewModel);
getFoos();
var viewModel = {
foos: ko.observableArray([]),
reloadFoos: function () {
getFoos();
},
removeFoo: function () {
remove(this);
}
};
var foo = function () {
this.Id = ko.observable();
this.Name = ko.observable();
this.Count = ko.observable();
};
function remove(foo) {
viewModel.foos.splice(viewModel.foos.indexOf(foo), 1);
viewModel.foos.each(function(index) {
viewModel.foos[index].Count = index%10 == 0;
});
}
function getFoos() {
viewModel.foos([]);
$.get("/myroute/", "", function (data) {
for (var i = 0; i < data.length; i++) {
var f = new foo();
f.Id = data[i];
f.Name = data[i];
f.Count = i%10 == 0;
viewModel.foos.push(f);
}
});
}
<div data-bind="foreach: foos">
<div style="float: left">
<a href="javascript:void(0);" data-bind="click : $parent.removeFoo, attr: { id: Id }">
<label data-bind="value: Name"></label>
</a>
</div>
<!-- ko if: Count -->
<div style="clear: left"></div>
<!-- /ko -->
</div>
When the click event fires the item is removed from the array but the if bind doesn't get updated and the ui formatting is off. I'm trying to keep from reloading the data because the ui block bounces as it removes and reloads.
Your UI is not being updated because when you do your assignment to Count, you aren't assigning as an observable. You are replacing the observable with a straight boolean value. So, your assignment calls like this one:
viewModel.foos[index].Count = index%10 == 0;
Will cause viewModel.foos[index].Count to be equal to true or false and the value won't be stored in the observable.
That line should be this instead:
viewModel.foos[index].Count(index%10 == 0);
That will set the observable correctly. Note that you must change all of your assignments to observables to be set this way. See the "Reading and Writing Observables" section of this page: Knockout Observables.

Categories

Resources