I have an Angular template in the DOM. When my controller gets new data from a service, it updates the model in the $scope, and re-renders the template. All good so far.
The issue is that I need to also do some extra work after the template has been re-rendered and is in the DOM (in this case a jQuery plugin).
It seems like there should be an event to listen to, such as AfterRender, but I can't find any such thing. Maybe a directive would be a way to go, but it seemed to fire too early as well.
Here is a jsFiddle outlining my problem: Fiddle-AngularIssue
== UPDATE ==
Based on helpful comments, I've accordingly switched to a directive to handle DOM manipulation, and implemented a model $watch inside the directive. However, I still am having the same base issue; the code inside of the $watch event fires before the template has been compiled and inserted into the DOM, therefore, the jquery plugin is always evaluating an empty table.
Interestingly, if I remove the async call the whole thing works fine, so that's a step in the right direction.
Here is my updated Fiddle to reflect these changes: http://jsfiddle.net/uNREn/12/
This post is old, but I change your code to:
scope.$watch("assignments", function (value) {//I change here
var val = value || null;
if (val)
element.dataTable({"bDestroy": true});
});
}
see: http://jsfiddle.net/dnzY9/
I hope it help you
Following Misko's advice, if you want async operation, then instead of $timeout() (which doesn't work)
$timeout(function () { $scope.assignmentsLoaded(data); }, 1000);
use $evalAsync() (which does work)
$scope.$evalAsync(function() { $scope.assignmentsLoaded(data); } );
Fiddle. I also added a "remove row of data" link that will modify $scope.assignments, simulating a change to the data/model -- to show that changing the data works.
The Runtime section of the Conceptual Overview page explains that evalAsync should be used when you need something to occur outside the current stack frame, but before the browser renders. (Guessing here... "current stack frame" probably includes Angular DOM updates.) Use $timeout if you need something to occur after the browser renders.
However, as you already found out, I don't think there is any need for async operation here.
First, the right place to mess with rendering are directives. My advice would be to wrap DOM manipulating jQuery plugins by directives like this one.
I had the same problem and came up with this snippet. It uses $watch
and $evalAsync
to ensure your code runs after directives like ng-repeat
have been resolved and templates like {{ value }}
got rendered.
app.directive('name', function() {
return {
link: function($scope, element, attrs) {
// Trigger when number of children changes,
// including by directives like ng-repeat
var watch = $scope.$watch(function() {
return element.children().length;
}, function() {
// Wait for templates to render
$scope.$evalAsync(function() {
// Finally, directives are evaluated
// and templates are renderer here
var children = element.children();
console.log(children);
});
});
},
};
});
Hope this can help you prevent some struggle.
I think you are looking for $evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync
Finally i found the solution, i was using a REST service to update my collection. In order to convert datatable jquery is the follow code:
$scope.$watchCollection( 'conferences', function( old, nuew ) {
if( old === nuew ) return;
$( '#dataTablex' ).dataTable().fnDestroy();
$timeout(function () {
$( '#dataTablex' ).dataTable();
});
});
i've had to do this quite often. i have a directive and need to do some jquery stuff after model stuff is fully loaded into the DOM. so i put my logic in the link: function of the directive and wrap the code in a setTimeout(function() { ..... }, 1); the setTimout will fire after the DOM is loaded and 1 milisecond is the shortest amount of time after DOM is loaded before code would execute. this seems to work for me but i do wish angular raised an event once a template was done loading so that directives used by that template could do jquery stuff and access DOM elements. hope this helps.
I have found the simplest (cheap and cheerful) solution is simply add an empty span with ng-show = "someFunctionThatAlwaysReturnsZeroOrNothing()" to the end of the last element rendered. This function will be run when to check if the span element should be displayed. Execute any other code in this function.
I realize this is not the most elegant way to do things, however, it works for me...
I had a similar situation, though slightly reversed where I needed to remove a loading indicator when an animation began, on mobile devices angular was initializing much faster than the animation to be displayed, and using an ng-cloak was insufficient as the loading indicator was removed well before any real data was displayed. In this case I just added the my return 0 function to the first rendered element, and in that function flipped the var that hides the loading indicator. (of course I added an ng-hide to the loading indicator triggered by this function.
You can also create a directive that runs your code in the link function.
You can use the 'jQuery Passthrough' module of the angular-ui utils. I successfully binded a jQuery touch carousel plugin to some images that I retrieve async from a web service and render them with ng-repeat.
In some scenarios where you update a service and redirect to a new view(page) and then your directive gets loaded before your services are updated then you can use $rootScope.$broadcast if your $watch or $timeout fails
View
<service-history log="log" data-ng-repeat="log in requiedData"></service-history>
Controller
app.controller("MyController",['$scope','$rootScope', function($scope, $rootScope) {
$scope.$on('$viewContentLoaded', function () {
SomeSerive.getHistory().then(function(data) {
$scope.requiedData = data;
$rootScope.$broadcast("history-updation");
});
});
}]);
Directive
app.directive("serviceHistory", function() {
return {
restrict: 'E',
replace: true,
scope: {
log: '='
},
link: function($scope, element, attrs) {
function updateHistory() {
if(log) {
//do something
}
}
$rootScope.$on("history-updation", updateHistory);
}
};
});