I have a shopping cart that shows the name of the flavor and quantity, then the total $ of each flavor. Then at the bottom of all the flavor, I have the calculation of the entire shopping cart.
HTML:
<div ng-repeat="flavor in cart track by $index">
<div>
<span class="flavor-name no-margin">{{flavor.name}} x <input type="text" name="{{flavor.name}}" size="1" ng-model="flavor.quantity"></span>
<span class="flavor-price no-margin">{{flavor.price * flavor.quantity | currency }}</span>
<div class="clearfix"></div>
</div>
</div>
<div ng-if="cart.length > 0">
<span class="cart-total-text no-margin">Total:</span>
<span class="cart-total no-margin">{{ total | currency}}</span>
<div class='clearfix'></div>
</div>
In my Controller:
function total() {
var total = 0;
for (var i = 0; i < $scope.cart.length; i++) {
var add = $scope.cart[i].price * $scope.cart[i].quantity;
total = total + add;
}
return total;
}
$scope.$watchCollection("cart", function() {
$scope.total = total();
});
And my cart looks like this:
{
'name': 'Strawberry',
'price': 4.5,
'quantity': 0,
},
{
'name': 'Mint',
'price': 3.5,
'quantity': 0,
}, etc
Right now, the total is working for when I add new things to the cart, but how do I make angular "listen" to when the quantity of the flavor is changed in my input box? Thanks!
You should be able to put the total() function on the scope and bind it to the span:
In the HTML:
<span class="cart-total no-margin">{{total() | currency}}</span>
In the controller:
$scope.total = function() {
var total = 0;
for (var i = 0; i < $scope.cart.length; i++) {
var add = $scope.cart[i].price * $scope.cart[i].quantity;
total = total + add;
}
return total;
}
Whenever something changes (like the quantity), Angular should trigger a digest cycle and reevaluate all the bound expressions including the total() method.
IMO that's the easiest way. If you really want to go with the $watch route (or understand why your current code is not working) you have to understand how Angular "watches" objects.
$scope.$watch by defaults only fires when the reference of the object changes.
$scope.$watchCollection goes a little further and fires when elements are added or removed from a collection (even though the collection reference stays the same).
In your case, changing the quantity of a flavor does not add or remove an item in the collection, which is why your code isn't triggered.
What you really want is a "deep watch" of the cart object. That can be achieved by passing a 3rd argument to the $watch method (a boolean called objectEquality):
$scope.$watch("cart", function() {
$scope.total = total();
}, true); // Tells Angular to test object equality.
This will make Angular look for change in the properties of the watched object (not just the reference), which should fire when the flavor.quantity changes.
You can watch a function if it is in the scope.
$scope.$watch('total()', function(){
});
Related
In my project, i am using ng-repeat with limitto filter and track by $index
<button ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">Previous</button>
<button ng-disabled="currentPage >= cil.cilChemicals.length/pageSize - 1" ng-click="currentPage=currentPage+1">Next</button>
<div ng-repeat="chemical in Chemicals | startFrom: currentPage * pageSize | limitTo:pageSize track by $index">
<span ng-init="initDetails(chemical)"> {{chemical.details}} </span>
</div>
Controller code :
$scope.currentPage = 0;
$scope.pageSize = 50;
Module.filter('startFrom', function() {
return function(input, start) {
start = +start;
return input.slice(start);
}
});
With this setup, initDetails(chemical) is called for all chemicals during first time ng-repeat rendering thus not called for each page which is rendered perfectly for the first time but when we repeat the page then ng-repeat starts re rendering from in between the array.
Please suggest why it starts from in between and when we use trackby chemical.chemicalIdthen angular js calls initDetails(chemical)for each page.
The way you have ng-init, runs in each iteration.
Except (enters track by):
when you use trackby $index the first element has always the $index 0 regardless of page number,
so angular doesn't re-render the html
but when you change the trackby to chemical.chemicalIdthen angular re-renders the html in evey iteration.
Actually angular will re-render only the items that have diff in the track by expression, either it is an index or id, than the previous items.
Also I agree with #ste2425's comment. that is not the intended use of ng-init
just a note:
LimitTo filter accepts 2 parameters, limit and begin,
limit is the page size, and begin is the index to start counting, so there is no need to create a custom filter for that.
You can essentially do this:
<button ng-disabled="currentPage == 0" ng-click="previousPage()">Previous</button>
<button ng-disabled="currentPage >= cil.cilChemicals.length/pageSize - 1" ng-click="nextPage()">Next</button>
<div ng-repeat="chemical in Chemicals | limitTo:pageSize:startFrom track by $index">
<span ng-init="initDetails(chemical)"> {{chemical.details}} </span>
</div>
And your controller
$scope.currentPage = 0;
$scope.pageSize = 50;
scope.startFrom = 0
$scope.nextPage = function() {
$scope.currentPage++;
$scope.startFrom = $scope.currentPage * $scope.pageSize;
};
$scope.previousPage = function() {
$scope.currentPage--;
$scope.startFrom = $scope.currentPage * $scope.pageSize;
};
I have a list of 1000+ items which I display using NgRepeat in Angular 1.3. The list populates with buttons. I have noticed significant delay on the click event int he list once it grows in size. When the list is only 5-10 items the clicks are instant. When the list is 1000 there is about 2-5 second delay before the button clicks are actually processed.
Now I cannot tell if this is a browser issue, but I suspect it has to do with too many listeners being used somewhere, causing the browser to check for them.
Here is sample of code in case there is a culprit hiding in there:
<div id="side" class="animated" style="min-height: 250px;"
data-ng-class="{'fadeInRight':documentDone}" data-ng-style="settings.listCss">
<div class="col-md-12 text-center" data-ng-style="settings.listCss"><h4>{{label}}</h4> {{inSide}} </div>
<div data-ng-repeat="doc in ::documents track by $index" id="{{ ::doc.id }}"
class="document ng-hide" data-ng-show="doc.show"
data-ng-init="docSettings = (settingslist[doc.companyid] || settings)" data-ng-style="::docSettings.listCss">
<div class="col-md-12" data-ng-style="docSettings.listCss">
<h4>
<span>{{ ::$index + 1 }}</span>
<span class="title-in-clusters">
{{ ::doc.title }}
<button type="button"
class="btn btn-primary btn-xs"
data-ng-click="viewJob(doc, docSettings)"
data-ng-style="docSettings.buttonCss">
<strong>VIEW</strong>
</button>
<a href="{{ ::doc.joburl }}" class="apply" target="_blank">
<button type="button" class="btn btn-primary btn-xs" data-ng-click="apply(doc.jobid, doc.companyid)"
data-ng-style="docSettings.buttonCss">
<strong>APPLY</strong>
</button>
</a>
</span>
</h4>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<span class=""><strong>ID: </strong>{{ ::doc.jobid }}</span>
<img data-ng-if="docSettings.heading.logourl && docSettings.heading.logourl != ''"
data-ng-src="{{docSettings.heading.logourl}}" class="side-logo inline-block" id="">
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<strong>Location: </strong><span class="">{{ ::doc.location }}</span>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<strong>Updated Date: </strong><span class="">{{ ::doc.updateddate }}</span>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<hr data-ng-style="docSettings.listCss">
</div>
</div>
</div>
There is nothing offensive about the other functions that are called when the button is pressed.
var modalInstance;
$scope.viewJob = function(modalDoc, docSettings) {
$scope.modalDoc = modalDoc;
$scope.docSettings = docSettings;
//the trusAsHtml takes string creates an object, so this will in essence convert string to object
//make sure you check if it is a string since it could be called multiple times by user (close and reopen same modal)
if (modalDoc.overview && typeof modalDoc.overview === 'string') {
$scope.modalDoc.overview = $sce.trustAsHtml(modalDoc.overview);
}
if (modalDoc.qualifications && typeof modalDoc.qualifications === 'string') {
$scope.modalDoc.qualifications = $sce.trustAsHtml(modalDoc.qualifications);
}
if (modalDoc.responsibilities && typeof modalDoc.responsibilities === 'string') {
$scope.modalDoc.responsibilities = $sce.trustAsHtml(modalDoc.responsibilities);
}
modalInstance = $modal.open({
templateUrl: 'app/modal/job_preview.html',
//templateUrl: 'myModalContent.html',
scope: $scope
});
};
I want to optimize this code so it can sever a list of up to 1500, but I cannot for the life of me find the culprit.
I will also take any solutions to reduce the load instead. Like for now I am thinking I may limit the number of DOM elements to 10 to so, and have angular rotate what is being viewed as user scrolls if it will result in better UX.
UPDATE:
Many things have been tried, from use of bind-once to more convoluted solutions that retard some of the watchers Which are enat but require a lot of Math to estimate which items are visible etc.
I finally decided on one solution that was easiest to do: I made a list of only items I wish shown and on mouse scroll up or down I edit the list.
First part of the solution is use of two directives:
.directive('ngMouseWheelUp', function() {
return function($scope, $element, $attrs) {
$element.bind("DOMMouseScroll mousewheel onmousewheel",
function(event) {
// cross-browser wheel delta
var event = window.event || event; // old IE support
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
if(delta > 0) {
$scope.$apply(function(){
$scope.$eval($attrs.ngMouseWheelUp);
});
// for IE
event.returnValue = false;
// for Chrome and Firefox
if(event.preventDefault) {
event.preventDefault();
}
}
});
};
})
.directive('ngMouseWheelDown', function() {
return function($scope, $element, $attrs) {
$element.bind("DOMMouseScroll mousewheel onmousewheel", function(event) {
// cross-browser wheel delta
var event = window.event || event; // old IE support
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
//console.log(event);
if(delta < 0) {
$scope.$apply(function(){
$scope.$eval($attrs.ngMouseWheelDown);
});
// for IE
event.returnValue = false;
// for Chrome and Firefox
if(event.preventDefault) {
event.preventDefault();
}
}
});
};
})
These two enable me to disable scrolling in the list on the right side. Then I would create two additional arrays from the documents in routeScope. First list would be generated whenever the documents were updated (which was an event listener for event emitted by the UI of the right hand side graph), this filter would only return array members that had the show property set to true:
var showFilter = function(object) {
return object.show;
}
This would be my array of visible items. From this array I created another Array of shown items. I defined a constant for max size of 7, so at most there are 7 items shown. And of course I set overflow of the parent container to none to disable scrollbar. (I may add a scroll graphic so the user knows he can scroll this field later)
Then I added the following directives to the side div:
data-ng-mouse-wheel-up="listUp()" data-ng-mouse-wheel-down="listDown()"
And inside the controller I defined listUp and listDown to work off an index and the max size constant to figure out which elements from the visible list I should add to the front or the back of the shown list.
/**
* Simulate scrolling up of list by removing bottom element and adding to top
*/
$scope.listUp = function() {
$rootScope.shownDocuments.unshift(getPrev());
$rootScope.shownDocuments.pop();
}
/**
* Simulate scrolling down of list by removing top element and adding to bottom
*/
$scope.listDown = function() {
$rootScope.shownDocuments.push(getNext());
$rootScope.shownDocuments.shift();
}
/**
* return next item in visibleDocuments array
*/
var getNext = function() {
$rootScope.topIndex++;
if ($rootScope.topIndex > $rootScope.visibleDocuments.length) {
$rootScope.topIndex -= $rootScope.visibleDocuments.length;
}
return ($rootScope.visibleDocuments[($rootScope.topIndex+max_shown_size)%$rootScope.visibleDocuments.length]);
}
/**
* Return previous item in visibleDocuments array
*/
var getPrev = function() {
$rootScope.topIndex--;
if ($rootScope.topIndex < 0) {
$rootScope.topIndex += $rootScope.visibleDocuments.length;
}
return ($rootScope.visibleDocuments[$scope.topIndex]);
}
Use of rootScope vs scope is mostly because modals would cause some undesirable behaviors if they were dismissed improperly.
Finally a reset function for the view:
/**
* Resets the list of documents in the visibleList (IE which are visible to client)
*/
var updateVisibleDocuments = function() {
$rootScope.topIndex = 0;
$rootScope.visibleDocuments = $rootScope.documents.filter(showFilter);
//clear view
$rootScope.shownDocuments = [];
$rootScope.topIndex = 0;
for (var i = 0; i < max_shown_size; i++) {
$rootScope.shownDocuments.push(getNext());
}
$rootScope.topIndex = 0;
}
This solution works really well because I only render 7 items even if my list has 100k items. This limits number of watchers tremendously.
You may want to try paginating to reduce the amount of things angular and the browser need to deal with on screen at any one time.
I am newbie on AngularJS.
Search result can be displayed in one page but why the next and previous button is showing ?
http://jsfiddle.net/2ZzZB/1473/
<input type="text" id="txtNotessearch" ng-model="search_notes" class="form-control input-sm" placeholder="SEARCH">
...
<ul>
<li ng-repeat="item in data | filter:search_notes | startFrom:currentPage*pageSize | limitTo:pageSize">
{{item}}
</li>
</ul>
Correct me, if I am wrong.
Because NumberOfPages is not filtered.
Instead of using $scope.data.length, you should use the length after the filter.
function MyCtrl($scope, $filter) { //Do not forget to inject $filter
$scope.currentPage = 0;
$scope.pageSize = 10;
$scope.data = [];
$scope.numberOfPages=function(){
var myFilteredData = $filter('filter')($scope.data,$scope.search_notes); //Filter the data
return Math.ceil(myFilteredData.length/$scope.pageSize);
}
for (var i=0; i<45; i++) {
$scope.data.push("Item "+i);
}
}
In addition, I would modify the ng-disable next button to
button "ng-disabled="(currentPage + 1) == numberOfPages()"
And I would add to search_notes onchange currentPage=1.
Here you have the fiddle
http://jsfiddle.net/rLots5zd/
In case, you want to hide the next and previous button when the result can be displayed in one page, please see the fiddle here: http://jsfiddle.net/rLots5zd/3/
I want to flow different data through a user clickable ul but I can't reset the state of the li's which have the isactive style set. Stripping down to bare minimum to demonstrate the input box takes two numbers separated by '-', the first is the number of clickable boxes, the second is the number of unclickable boxes at the beginning.
Note when new input is sent the li's that are currently active remain active. I want to reset the li's to inactive. [ note: trying to do this without jQuery to learn "The Angular Way". I have a pure jQuery version of this ]. angular.copy has not worked (though that could be ignorance)
I'm starting to think this might have to go but I'm keeping the graphic representation exclusively in the .html:
html
<div ng-controller="BoxScreen">
<input type="text" ng-model="inbox" />
<button ng-click="getBox()" /></button>
<div>
<br />
<h2>{{dys}}, {{dst}}</h2>
<div>
<ul class="smallbox">
<li data-ng-repeat="s in skip"> </li>
<li data-ng-repeat="d in ar" ng-class="{'button': !isActive, 'button active': isActive}" ng-init="isActive = false" ng-click="isActive = !isActive; clickMe(d)">{{d}}</li>
</ul>
</div>
</div>
</div>
javascript
angular.module('myApp', [])
.controller('BoxScreen', ['$scope', function($scope) {
$scope.getBox = function() {
indat = $scope.inbox.split('-');
$scope.dys = indat[0];
$scope.dst = indat[1];
$scope.ar = [];
$scope.skip = [];
for(var s=0; s < $scope.dst; s++) {
$scope.skip.push(s);
}
for(var d=1; d <= $scope.dys; d++) {
$scope.ar.push(d);
}
}
$scope.clickMe = function(did) {
//
}
}]);
I believe your problem is related to ng-repeat creating new child scopes for the child elements it attaches to the DOM. When you expand the list with new elements, ng-repeat doesn't actually destroy the old elements (as long as they're unchanged, as is true in your case), but reuse them. See more here.
The way you have designed your structures on the scope seems very messy to me. A better approach is to create all the data beforehand, and not introduce all the logic in the HTML.
Example:
<li data-ng-repeat="d in ar.items" ng-class="{'button': !d.isActive, 'button active': d.isActive}" ng-click="ar.toggle(d)">{{d.text}}</li>
where ar here is an object:
$scope.ar = {
items: [
{
text: '1',
isActive: false
},
more items...
],
toggle: function(d) {
d.isActive = !d.isActive;
}
}
This way you have access to the data in other places as well, and not some hidden away variables set on the child scope.
I'm trying to build a simple page to group record and then add a button to eliminate some records.
The problem is that the record eliminated that has the same name is deleted from the wrong grouped list. And also if a list have no grouped records should disappear, and instead is always there.
Fiddle: http://jsfiddle.net/Tropicalista/qyb6N/15/
// create a deferred object to be resolved later
var teamsDeferred = $q.defer();
// return a promise. The promise says, "I promise that I'll give you your
// data as soon as I have it (which is when I am resolved)".
$scope.teams = teamsDeferred.promise;
// create a list of unique teams
var uniqueTeams = unique($scope.players, 'team');
// resolve the deferred object with the unique teams
// this will trigger an update on the view
teamsDeferred.resolve(uniqueTeams);
// function that takes an array of objects
// and returns an array of unique valued in the object
// array for a given key.
// this really belongs in a service, not the global window scope
function unique(data, key) {
var result = [];
for (var i = 0; i < data.length; i++) {
var value = data[i][key];
if (result.indexOf(value) == -1) {
result.push(value);
}
}
console.log(result)
console.log(Math.ceil(result.length / 10))
$scope.noOfPages = Math.ceil(result.length / 10);
return result;
}
$scope.currentPage = 1;
$scope.pageSize = 5;
$scope.maxSize = 2;
$scope.deleteItem = function(item){
//code to delete here
var index=$scope.players.indexOf(item)
$scope.players.splice(index,1);
};
Here is a sample of something expanding on the tip from SpykeBytes
<div ng-repeat="location in journey.locations">
<div id="location_div_{{ $index }}">
<label class="journey-label">Location name</label>
<input class="journey-input" id="location_{{ $index }}" type="text" ng-model="location.location_name" />
<button ng-show="editable" tabindex="-1" class="journey-button remove" ng-click="removeItem(journey.locations, $index)">
Remove location
</button>
Then in my controller I set up an action that takes deletes the individual item
$scope.removeItem = function(itemArray, index) {
return itemArray.splice(index, 1);
};
To hide the group when nothing is listed, you need to get the filtered list and then use ng-show to drive the display. This is a bit tricky:
<div ng-show="currentList.length>0" ng-repeat="team in teams| startFrom:(currentPage - 1)*pageSize | limitTo:pageSize | filter:searchInput"> <b>{{team}}</b>
<li ng-repeat="player in (currentList = (players | filter: {team: team}))">{{player.name}}
<button class="btn btn-small" type="button" ng-click="deleteItem(player)">Delete</button>
</li>
</div>
However I am not seeing the problem you said about removing from wrong group. Can you let me know how to reproduce it?
Index won't help you here because the {{$index}} that ng-repeat provides is within the groupings. That is, each grouping restarts the $index variable. You are going to need a unique identifier for each record though. Without that there is no way to be sure that the record you want to remove is the right one.
As far as the groupings, you can recreate the model whenever you delete something. This wouldn't work with the sample data in the Fiddle, but it works when you're dealing with a real datasource.
You can instead pass the index of the object if it is within ng-repeat.