I've read in another stack overflow post that angular.js tries to be smart when the collection used in ng-repeat changes so that the DOM is not rebuilt completely (see Does ng-repeat retain DOM elements or create all new ones when the collection changes?). However I'm experiencing a problem: When I remove one item from my collection, all items following this one are rebuilt in the DOM. This is problematic for me because I am listing for the jQuery remove event and this event gets fired multiple times, although only one item got removed. Listening for scope.$on('remove') could solve my problem - this gets only triggered once - but here is my problem, that I want to access jQuery data before removing and when scope.$on('remove') is triggered, data got already removed.
What can I do to solve this issue? See this link for a demo: http://plnkr.co/edit/T1ZicU20JjWYk1J5ch7Z?p=preview.
element.bind('remove', function() {
alert("Element remove handler called.");
});
When I remove the third element, remove gets already triggered once, when I remove the second one, it gets triggered twice, when removing the third one, it gets triggered thrice.
Just add the track by to solve this issue :
ng-repeat="w in widgets track by $index"
Explanation:
When you add track by you basically tell angular to generate a single DOM element per data object in the given collection. This could be useful when paging and filtering, or any case where objects are added or removed from ng-repeat list.
Reference: http://www.bennadel.com/blog/2556-using-track-by-with-ngrepeat-in-angularjs-1-2.htm
Related
I have applied ng-click on an element and within the function, I want to access the DOM element itself. I could do that with :
var element = $document[1].getElementById('<id of the element>');
However, the problem I am facing is that when that element is clicked, it's class changes. But the element I get using the above method is the previous state of the element before the click. How can I get access to the new attributes of an element after the click is performed ?
Update
I am using AngularJS' smart-table for displaying data fetched from backend. The library offers sort functionality but it sorts the data which is already fetched from the DB and is present in front end. I wanted to tweak it so that when I click the sort button, I should be fetching data from the backend and update the rowCollection so that the table refreshes. Now, in order to trigger the API call, I was thinking of using ng-click event on table headers. Also, I need to know whether I need to sort in ascending order or descending order. So, for that, smart-table automatically appends a class sort-ascent or sort-descent to the table header when it is clicked. So, I thought maybe if I can access that, then using the combination of the header column (sort key) and the class (sort order), I can hit the backend API and fetch the appropriate data.
I understand the solution looks more of a hack then a proper way of doing things.
Maybe you should look at this answer : Accessing clicked element in angularjs
You can access by $event.target
<button ng-click="yourSubmit($event)"></button>
I'm wrestling with the way angular watches arrays. I have the following markup:
<map name="theMap" center="{{mapInfo.center.toUrlValue()}}" zoom="{{mapInfo.zoom}}" on-dragend="dragEnd()" on-zoom_changed="zoomChanged()">
<marker ng-repeat="pin in pins() track by pin.iconKey()" position="{{pin.latitude}}, {{pin.longitude}}" title="{{pin.streetAddress}}" pinindex="{{$index}}" on-click="click()"
icon="{{pin.icon()}}"></marker>
</map>
Each individual pin returned by pins() has a number of properties, sub-properties, etc. One of those sub-properties controls the marker color. When that subproperty changes I want the UI to update.
Because ng-repeat appears to $watch based on simply changes to the collection it's not obvious how to achieve that. I thought that my tracking function, iconKey(), would do it because it returns a different value depending upon the subproperty's value. But that didn't work.
One other thing: the subproperty gets changed in the callback from an $interval that runs under a directive. I mention this because, in an earlier post, someone thought that there might be a context/scope problem.
However, even when I make the change in an event listener within the UI's controller (where the event is raised within the "success" clause of the $interval callback) it still doesn't work.
That's why I think the problem is just angular not noticing the change in iconKey(); it's acting like all it $watches for ng-repeat is the array's length. Which doesn't change when the subproperty changes.
Update
I've created a plunker to demonstrate the issue I'm facing. You can find it at http://plnkr.co/edit/50idft4qaxqw1CduYkOd
It's a cut down version of the app I'm building, but it contains the essential elements (e.g., a data context service to hold information about the map pins and an $interval service to toggle the subproperty of one of the pin array elements).
You start the update cycle by clicking Start in the menu bar (you may want to drag the map down slightly to put both pins into full view). It should toggle the color of each pin, alternatively, 5 times each, once every 2 seconds. It does this by toggling the value of the isDirty property of the pin object in a listener defined in the controller. The event is raised by the $interval service.
If you break on line 22 during the test cycle you'll see the pin's icon being returned. So something within angular is calling for the information...but the pin color doesn't change.
I look forward to someone quickly pointing out the bone-headed mistake that has nothing to do with any of my theories :). Apologies in advance for whatever blinders I'm wearing.
Update 2
After checking out the code snippet in the response I simplified my plnkr and demonstrated that angular is, in fact, updating the UI when a subproperty changes. So this appears to be a limitation or bug in ng-map.
What you are missing here is the concept of array and function your function pins() passes an array and that array is been bound with ng-repeat. But the brutal fact here is that no matter what that array is never changed, because you do not have ANY reference to that array hence the rg-repeat will always remain as is...
I'll suggest to try get the array be referenced two ways to do that
ng-init="pinsArray = pins()"
and second inside controller
$scope.pinsArray = $scope.pins()
then make changes to $scope.pinsArray inside controller
ng-repeat will be changed to
ng-repeat="pin in pinsArray"
also read about filters I am guessing that's what you where trying to do with "track by"
hope this helps..
Edit: different story with ngMap markers
seems like it doesn't watch sub-property.
so here's a work around
add following statement to you update the pinsArray after making changes to its properties.
pinsArray = angular.copy(pinsArray);
the solved plnkr example:
http://plnkr.co/edit/EnW1RjE9v47nDpynAZLK?p=preview
I'm trying have an ng-repeat which contains divs that include a CKEDITOR instance. The list is sortable by a few different properties. However, when the list is resorted, the CKEDITORs break because they don't support being moved around in the DOM. The only solution I can think of is to destroy the CKEDITOR instance before sort, and recreate them after. Is there any events on ng-repeat that can accomplish this?
plunkr here: http://plnkr.co/edit/tGnTdzvRl7xhEX7zYq4c?p=preview
Was able to get this fixed eventually. Pretty easy now that I think about, but took me 3 days to realize it. Instead of trying to listen for events on ng-repeat, catch the event that is causing the data to be modified. For me, it was jquery UI sortable (angular directive.) In the controller add $scope.$broadcast('unbind-ckeditor') before the change, and then $scope.$broadcast('rebind-ckeditor') after it. In your angularjs directive for ckeditor, call scope.$on('unbind-ckeditor', function() {instance.destroy /* instance is your ckeditor instance*/}); and then reload it in the rebind. Hope this helps someone.
Edit:
Make sure the $on('unbind-ckeditor'... is only added to the scope once, or multiple sorts will throw exceptions.
The solution for me:
use divArea and not iframe, this will work with ng-repeat when the list changed.
using other textarea (hidden) and monitoring it as the ng-sortable+ckeditor broke the order of CKEditors.
in ng-sortable (who make the change in the list) I was added this code:
$rootScope.$broadcast('rebind-ckeditor'); console.log("rebind")
and in CKEDITOR directive:
scope.$on('rebind-ckeditor', function () {
if (jQuery(element).data("loaded")) {
console.log("rebind");
CKEDITOR.instances[element[0].id].destroy();
jQuery(element).data("loaded", null);
element[0].value = $("textarea", element.parent())[1].value; // update manualy from the hidden textarea
onLoad(); //recreate CKE after textarea changed.
}
})
I have a table on my page that is populated like this:
<tr data-ng-repeat="row in grid.view = (grid.data | filter:isProblemInRange )">
I have the following function:
$scope.gridReset = function () {
$scope.grid.data = angular.copy($scope.grid.backup);
$scope.gridForm.$setPristine();
};
The function replaces the data in $scope.grid.data with a backup.
When it runs the angular.copy seems to take no time to run but the data on the screen
takes about 5 seconds before I see it changing to the backup copy. Is there anything
I could do to speed this up?
Here's the function isProblemInRange:
$scope.isProblemInRange = function (row) {
return (row.problemId >= $scope.pidLower || $scope.pidLower == null || $scope.pidLower == "") &&
(row.problemId < $scope.pidUpper || $scope.pidUpper == null || $scope.pidUpper == "")
};
I have approximately 500 rows in the grid.data
1. Does your data has to be bound only once for each change?
ng-repeat sets a $watch for each item and within each $digest it dirty checks all registered $$watchers. If you have 2000+ of these $$watchers then your application will not be responsive (maybe until object.observe) and there is no way to fix it without reducing the amount of registered $$watchers.
You must read #Misko Hevery's answer: How does data binding work in AngularJS?
Check bindonce if it suites your requirements: https://github.com/Pasvaz/bindonce
2. If you have indexes then you can help ng-repeat.
ng-repeat without track by removes all DOM elements every time your array/object is overridden just to recreate all DOM elements again. If you have 1000+ elements that is slowwwwww.
angular.js 1.2 introduced track by syntax for ng-repeat to solve this issue.
Check this article : Using Track By With ng-Repeat In Angular.js 1.2
3. If you must have a huge amount of dynamic bindings?
You probably don't , use pagination whenever possible.
If not , then you must write advanced stuff (like custom repeaters and dynamic DOM insertions)
Check this article: angularjs-my-solution-to-the-ng-repeat-performance-problem
and this also: angularjs-1200ms-to-35ms
what ng-repeat watches?
ng-repeat uses $scope.$watchCollection, from the docs:
$watchCollection(obj, listener)
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays, this implies watching the array items; for object maps, this implies watching the properties). If a change is detected, the listener callback is fired.
That means if you have an array with 1000 objects ([1..1000]) then you end up with 1000 dirty checks with each $digest, one for each item(property).
ng-repeat and the DOM
If you override the collection object itself (grid.data in the question above), all elements are removed from the DOM (unless you use track by) because by default, ng-reapet uses $$hashkeys for tracking changes. Those $$hashkeys are lost whenever the collection is replaced.
If items are removed or added to the array without overriding it, then only these items are removed / added to the DOM.
I am using Jquery plugin http://timeago.yarp.com/ for showing time.
Issue is timeago will not take effect for dynamically generated items.
$(document).ready(function() {
$(".timeago").timeago(); // works perfectly fine for the items which are loaded on page load
//$(".timeago").live(timeago()); // gives me an error ie timeago is not defined
//$(".timeago").live($(".timeago").timeago()); // gives me an error too much recursion.
jQuery.timeago.settings.allowFuture = true;
});
From some google search I got to know something ie:
Using live is the same as using bind, except that it is limited only to the events click, dblclick, keydown, keypress, keyup, mousedown, mousemove, mouseout, mouseover, and mouseup.
Now how can do it cause I dont have any click event? How can I bind this?
.live() and .bind() assign callbacks to event. In your case, you don't have an event to assign the function to and so it fails.
You can, in theory, assign your callback to a custom event. You will however have to manually trigger the event (using .trigger()) whenever your item is generated. For example:
$("abbr.timeago").live("timeago", function() {
$(this).timeago();
});
// ... and in the bit that generates your item
$new_item.trigger("timeago")
Demo: http://jsfiddle.net/ZjuW4/9
Of course, using .live() in this situation is purely academic and does not really serve a good purpose.
If you do have access to the code that's generating the items, you can simply chain in the call to .timeago() as the items are generated, i.e. http://jsfiddle.net/ZjuW4/3/
take a look in this topic
here was discussed how to put timeago on dynamically loaded items
for example the results of an ajax request.
Activate timeago on newly added elements only
PS: allowFuture does not have anything to do with putting timeago on newly created items on your page. it just allows dates in the future (f.e. "in 3 days", "next week")