My question in short
how do i manipulate the isolated scope created by ng-repeat inside of my controller?
To read the full problem i am having continue reading below:
I have a collection on timelines and each of them further has a collection of captures (essentially images).
HTML
<ul>
<li ng-repeat="timeline in timelines | filter:search | orderBy:'event_date':sort">
<ul ng-if="timeline.captures">
<li ng-repeat="captures in timeline.captures | limitTo:imageLimit">
<img ng-src="<% captures.capture_location %>">
</li>
</ul>
<a ng-click="(imageLimit = imageLimit + 1)">Load more</a>
</li>
</ul>
I want to limit the number of images being loaded on page load.. which i am able to do using limitTo
I want to load more images when the user clicks on 'Load more'. I am also able to do this by ng-click="(imageLimit = imageLimit + 1)
What i want to do:
I want to extract this out as a function
so that the following
Load more
becomes
Load more
To achieve this, i tried the following:
inside my controller
$scope.imageLimit = 1; // number of images to show initially on page load
$scope.loadMore = function() {
$scope.imageLimit += 1;
};
Now what is happening is that this this not create the imageLimit model inside the repeated scope (if you understand what i mean) but instead creates the imageLimit model at the controller scope. This is also the expected behaviour but i want to know how do i manipulate the scope within the repeated timeline (isolated scope for that timeline) inside my controller?
If I understand correctly, you want to be able to loadMore() for each timeline, independantly of the other ones. The loadMore() function should thus take the timeline as argument, and the limit should be a property of the timeline:
angular.forEach($scope.timelines, function(timeline) {
timeline.imageLimit = 10; // default value
});
$scope.loadMore(timeline) {
timeline.imageLimit++;
}
And in your template:
<li ng-repeat="captures in timeline.captures | limitTo:timeline.imageLimit">
...
<a ng-click="loadMore(timeline)">Load more</a>
Related
I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.
I am trying to set the position property on an object that is inside an array being used by ng-repeat.
<div ng-repeat="item in items track by $index" ng-init="pos = $index + 1; item.position = pos">
<span>{{pos}}</span>
</div>
Initially it sets the property fine, however, anytime the HTML is recompiled after a model (I add/remove/move elements in the array) change the pos variable is set properly and correct position is displayed. However it will not update item.position !
Interesting hack:
<li ng-repeat="item in items track by $index">
{{item.position=$index+1}}
<span>{{item.position}}</span>
</li>
Wouldn't suggest it in production though
DEMO
The assignment of properties really doesn't belong in your template. You should ideally be assigning the position property in your controller. You could use something like this in your controller
$scope.$watchCollection('items', function() {
angular.forEach($scope.items, function(item, idx) {
item.position = idx + 1;
});
})
however I'd really question the need to do so. The item's position in the array provides all the information you need.
Demo ~ http://plnkr.co/edit/H5JbsABEdceSXENFFBBO?p=preview
I need to do app,which doing something like that:
One web page separated on 2 tabs.
In first tab we have list of items. If you click on item it should be transferred to another tab and should be hidden on first tab.
Tools to do this thing is AngularJS.
Any ideas about that? Sorry for this question. I'm noobie in AngularJS.
Here's a really basic example of how you could implement your idea with AngularJS:
Tabs
one easy way to create tabs is to create the markup and use your controller to hold a reference to which tab to show:
// in the controller
$scope.currentTab = 1;
// in the markup
<li class="tab" ng-show="currentTab === 1">
... tab contents
</li>
... repeat for as many tabs as you want
you could then change the tab inside an ng-click directive in your page markup:
// in the markup
<a ng-click="currentTab = 1">change to tab 1</a>
Items
to show items in your tab first create the items in your controller:
// in the controller
$scope.tab1Items = [1, 2, 3, 4, 5];
then display in your tab:
// in the markup
<li class="tab">
<div ng-repeat="item in tab1Items">{{ item }}</div>
...
</li>
Moving the items
This can be done with a function in ng-click that passes the item from one array of items to another:
// in the controller
$scope.moveItem = function(item) {
if ($scope.tab1Items.indexOf(item) > -1) {
$scope.tab1Items.splice($scope.tab1Items.indexOf(item), 1);
$scope.tab2Items.push(item);
}
else {
$scope.tab2Items.splice($scope.tab2Items.indexOf(item), 1);
$scope.tab1Items.push(item);
}
}
// in the markup
<div ng-repeat="item in tab1items" ng-click="moveItem(item)">
...
The concept here is to create a page with one controller that maintains current tab state, as well as the items to display in those tabs. Since you have access to all the items in the same scope it's easy to manipulate and pass those items from one tab to another.
Here's a link to a basic working example in plnkr: http://plnkr.co/edit/PjSXI0JH4uQ1OW8u3f2z?p=preview
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
I am trying to pull all items from my array called 'collections'. When I input the call in my html I see only the complete code for the first item in the array on my page. I am trying to only pull in the 'edm.preview' which is the image of the item. I am using Angular Firebase and an api called Europeana. My app allows the user to search and pick which images they like and save them to that specific user.
here is the js:
$scope.users = fbutil.syncArray('users');
$scope.users.currentUser.collections = fbutil.syncArray('collections');
$scope.addSomething = function (something) {
var ref = fbutil.ref('users');
ref.child($rootScope.currentUser.uid).child('collections').push(something);
}
$scope.addSomething = function(item) {
if( newColItem ) {
// push a message to the end of the array
$scope.collections.$add(newColItem)
// display any errors
.catch(alert);
}
};
and the html:
<ul id="collections" ng-repeat="item in collections">
<li ng-repeat="item in collections">{{item.edmPreview}}</li>
</ul>
First, remove the outer ng-repeat. You want to only add the ng-repeat directive to the element which is being repeated, in this case <li>.
Second, from your AngularJS code, it looks like you want to loop over users.currentUser.collections and not just collections:
<ul id="collections">
<li ng-repeat="item in users.currentUser.collections">{{item.edmPreview}}</li>
</ul>
And third, you're defining the function $scope.addSomething twice in your JavaScript code. Right now, the second function definition (which, incidentally, should be changed to update $scope.users.currentUser.collections as well) will completely replace the first.