Rendering issue using Ng-repeat, track by and limitto together - angularjs

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;
};

Related

How to set a default value for AngularJS ng-repeat?

<li ng-repeat="item in quantityList track by $index" ng-model="isPageValid='true'">
<input value="item.quantity" ng-blur="validateQuantity(item)" ng-model="item.quantity">
</li>
I'm trying to set a default value for a status variable each time an ng-repeat loop occurs. I've tried ng-model, but that doesn't work. For instance,
I'd like to set isPageValid="true" before each time the ng-repeat loop runs. 'True' is will be the default value, and the validation function will test whether isPageValid should be set to 'false'.
I'd like the ng-repeat loop to run each time the ng-blur is exercised.
NOTE: I understand the way I'm using ng-model is incorrect, but this is just to illustrate the issue.
HTML:
<li ng-repeat="item in quantityList track by $index" ng-model="isPageValid='true'">
<input value="item.quantity" ng-blur="validateQuantity(item)" ng-model="item.quantity">
</li>
JS:
scope.validateQuantity = function(item){
var qty = item.quantity;
if(parseInt(qty) >=1 && parseInt(qty) <= 200){
item.isQuantityValid = true;
}else{
item.isQuantityValid = false;
scope.isPageValid = false;
}
}
The loop creates a list of input boxes. The objective is to create a global validation value called isPageValid which is 'false' if the validation by the JS fails for any input box. Note, when ng-blur is exercised, the JS validation runs and loop should re-run.
I believe ng-init could help...
<ul ng-init="isPageValid='true'">
<li ng-repeat="item in quantityList track by $index" >
<input ng-blur="validateQuantity(item)" ng-model="item.quantity">
</li>
</ul>
Note that it would be better practice to initialise isPageValid in the controller that is in the same scope as your validateQuantity function.
Here is an example of how you could initialise isPageValid in your controller and update the value after each call to validateQuantity...
In your controller:
scope.isPageValid = true;
function updatePageValid() {
scope.isPageValid = scope.quantityList.every(item => item.isQuantityValid);
}
scope.validateQuantity = function(item) {
var qty = parseInt(item.quantity);
item.isQuantityValid = (qty >= 1 && qty <= 200);
updatePageValid();
});

connecting angular bootstrap ui paginate to table

I am working on setting up angular pagination on a table that I am creating.
I have worked through the docs. However, I am not seeing how to connect the pagination to the table. My pagination size is matching the array size. Their are the right number of button links to page the page length. However, my table is still at full length making it impossible for me to test my pagination.
Here is my table body
<tbody>
<tr ng-repeat="drink in editableDrinkList | orderBy:'-date'">
<td>[[drink.name]]</td>
<td>[[drink.date |date: format :timezone]]</td>
<td>[[drink.caffeineLevel]]</td>
<td>
<a ng-click="delete(drink._id)">
<i style="font-size:18px; margin-right:5%" class="fa fa-times"></i>
</a>
<a href="" ng-click="update(drink)">
<i style="font-size:22px;" class="fa fa-pencil-square-o"></i>
</a>
</td>
</tr>
</tbody>
I have set my table body outside of the table tag I tried it inside but my pagination controls did not appear.
Here is my pagination
<pagination
total-items="totalItems"
ng-model="currentPage"
items-per-page="itemsPerPage"
ng-change="pageChanged()"
ng-click="setPage(currentPage)">
</pagination>
I also set up a p tag to make sure the tabs are responding
<div>
<p>total Items [[totalItems]]</p>
<p>itemsPerPage [[itemsPerPage]]</p>
<p>current page [[currentPage]]</p>
</div>
The total items, items per page and current page inside of the paragraphare all responding to each click on the pagination controller. However the table is not changing.
Here is my controller
var editabledrinkSet = function(){
editableArray = [];
DrinkLibrary.getAllDrinks().success(function(data){
for (var i = 0; i < data.length; i++) {
if(data[i].editable) {
editableArray.push(data[i])
};
};
$scope.currentPage = 1;
$scope.totalItems = editableArray.length;
$scope.itemsPerPage =10;
$scope.maxSize = 3;
$scope.setPage = function(pageNo) {
$scope.currentPage = pageNo;
}
$scope.pageChanged = function() {
$log.log('Page changed to: ' + $scope.currentPage);
};
$scope.editableDrinkList = editableArray;
});
};
When I get it working I will move most of it out into a directive. I just want to get it set up first.
You can do this with a custom filter.
Add a Custom Filter to ngRepeat
You need to be able to tell the repeat which index is going to be the starting point for each page and how many items to include on the page. You can do this by creating a simple filter (I called it pages). This filter is going to use the currentPage and itemsPerPage values that you already set up in your controller.
<tr ng-repeat="drink in editableDrinkList | orderBy:'-date' | pages: currentPage : itemsPerPage">
Create the Custom Filter
To create a custom filter you pass in your name and then a factory function that returns a worker function. The worker function automatically receives the input array as its first value. The subsequent values are the ones you specified in your filter.
So the pages filter gets the input array and the two values passed to it (currentPage and pageSize). Then you'll just use Array.slice to return the new array to the repeat. Remember, filters are non-destructive. They aren't changing the actual scoped array. They are creating a new array for the repeat to use.
app.filter('pages', function() {
return function(input, currentPage, pageSize) {
//check if there is an array to work with so you don't get an error on the first digest
if(angular.isArray(input)) {
//arrays are 0-base, so subtract 1 from the currentPage value to calculate the slice start
var start = (currentPage-1)*pageSize;
//slice extracts up to, but not including, the element indexed at the end parameter,
//so just multiply the currentPage by the pageSize to get the end parameter
var end = currentPage*pageSize;
return input.slice(start, end);
}
};
});
Add the Pagination Directive to the Page
Here's the cleaned up version of the directive you'll add to your html. Don't use an kind of ngClick or ngChange on the pagination directive. ngModel is set to the currentPage variable so there's nothing else you have to do. Since currentPage is two-way bound via ngModel, when it changes the pagination directive will update as well as the pages filter, which will in turn update your repeated table rows.
<pagination
total-items="totalItems"
ng-model="currentPage"
items-per-page="itemsPerPage"
max-size="maxSize">
</pagination>
It's that simple. If you want to see a working demo, check the Plunker.
Plunker

Angularjs - Pagination appear after search filter

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/

Binding Input to Angular controller function

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(){
});

AngularJs delete record on nested grouped rows

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.

Resources