I have the directive, which transcludes an arbitrary content, which should contain some children, that may be created with ngRepeat. Within link or transcludeFn I have access to a template for children, not a finished DOM. Given that, how can I calculate e.g. the sum of .width() of each child?
See http://embed.plnkr.co/o492ObrHC65zbCMxIhxu/
This is an artifact of who ng-repeat and ng-if work - they create a comment and then $watch for a value to either insert or remove an element from the DOM.
So, you are getting the actual DOM of the transcluded element, but it is still in the process of being generated by ng-if/ng-repeat, which have scheduled the generation at the end of the digest cycle.
The quick fix seems to be to use $timeout with 0 seconds. This is not as hacky as it seems - there is no chance of a race condition. All it does is scheduling the function to run at the end of the digest cycle, ensuring that it will see the changes made by ng-repeat and ng-if.
link: function(scope, iElement){
scope.$watchCollection('items', function () {
// will not yet have the repeated elements
console.log("before timeout", iElement.html());
$timeout(function () {
// will have all the repeated elements
console.log("after timeout", iElement.html());
});
});
}
plunker
This illustrates, however, that you never know just what transcluded directives are or could be doing - some may change the DOM at a later point in time. The better way is probably to listen to DOM changes and react to that.
Related
Found some close questions , but not exactly the one I need to ask.
I have multiple elements with ng-click events.
For a majority of them (of a specific class), I don't need to actually run an angular digest cycle after click. The result of the click on these elements does not affect any scope variable (let's say for example they just print out a console.log).
What I want to do is to react conditionally to an ngClick, where say elements of a specific css class will not have the automatic $apply at the end.
Edit:
What I ended up doing was replace the ng-click, ng-mouseenter and ng-mouseleave with the corresponding javascript replacements.
I did this for two reasons:
1. I don't actually affect the scope variables on those clicks, so I don't need to run a digest after each (I have mouseenters, so you can imaging that generated a lot of digest cycles for no reason).
2. This is content that I load late in the page loading sequence from another source (ng-bind), so it has to be sanitized by angular and then compiled. This took a log of time (almost a second) because I have many such links, and that was holding back the display of the content.
While I highly recommend against this, as the $apply in your application shouldn't really be affecting anything (even performance). You'll have to create your own directive for this.
HTML
<div data-no-apply-click="myFunction()">
</div>
Javascript
.directive('noApplyClick', function ($parse) {
return {
compile : function ($element, attr) {
var fn = $parse(attr['noApplyClick']);
return function (scope, element, attr) {
element.on('click', function (event) {
fn(scope, {
$event : event,
$element : element
});
});
};
}
};
});
JsFiddle: http://jsfiddle.net/gj54bjsh/
Similar to this question, I want to set focus on the last <select> whenever it gets added. As there's a single method doing it, I need no directive and no watch and no events. My function
$scope.addNew = function() {
$scope.items.push({});
$timeout(function() {
$("select").focus();
});
};
works nicely, except when called directly from the controller function definition like
angular.module('myModule').controller('MyCtrl', function($scope, $timeout) {
$scope.items = {};
...
$scope.addNew();
}
It looks like the timeout happens before the DOM gets constructed and $("select") is empty. With a delay of some 100 ms it works again, but this is a bad hack.
Contrary to what's said in the answer to the linked question, timeout doesn't suffice.
So what's a reliable way to wait for angularjs being really done with the DOM and everything?
Update:
It probably doesn't work because of the select to be focused being embedded in directives (including ng-repeat and some own ones) That's why there initially was no DOM element to focus on.
According to the comments, I need a directive. What's unclear is how exactly to do it. I tried and failed and found out a simpler solution.
What I need
I wasn't very explicit with this, so let me clarify.
I'm working with a table where each row contains some editable fields.
In addNew, I want to set focus on the first editable field of the new row.
In my case this happens to be the very last select.
It worked except at the very beginning, when I was adding the very first row from the controller body.
Why I'm opposed to using a directive
To my limited understanding, it's completely backwards:
A directive modifies the look, behavior, or structure of a given element. But there's no element which should be modified. I tried to put a directive on everything from the select itself to the whole body.
It needs to watch something or listen to an event, but I only want to invoke a function manually.
It didn't work (for me and others as the comments to the linked question shows).
I am going to try and influence you to use a directive here, just to perform the behavior.
Here is a fiddle.
Basic premise is adding the behavioral directive to the element inside repeater:
<table>
<tr ng-repeat="item in items">
<td>{{item}}: <input type="text" auto-focus/></td>
</tr>
</table>
Then your directive would put focus on the last added element:
app.directive('autoFocus', function(){
return function link(scope, elem){
elem[0].focus();
}
});
No watchers or events needed unless I am missing something that you require.
Code that manipulates the DOM should go in a directive, but if you switch to a directive and still have reason to wait until Angular is finished updating the scope and the dom, use $scope.$evalAsync:
$scope.$evalAsync( function() {
// This will wait until Angular is done updating the scope
// Do some stuff here
//
});
The solution was very trivial: Instead of calling $scope.addNew(); directly, I put it in $scope.init invoked from <form ng-init="init()">.
According to the documentation
The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.
this seems to be wrong (or maybe not, as ngRepeat si involved). I'm only using it to postpone the call to $scope.addNew();, where neither timeout nor posting events worked.
What I want to do is push an object to an array which have angularjs binding to html, then do a click function on the new created element.
the code is like this
$scope.fruits = {"orange", "plum", "cherry"};
$scope.add = function() {
$scope.fruits.push("apple");
//this would add an element to the html, then I want to click on the element
$('#fruits').children.eq(-1).click();//click the last element (which has just been created)
}
the html is like:
<div id="fruits">
<div ng-repeat="fruit in fruits"></div>
</div>
The problem is, when an object is pushed to the model, angular would do something like $scope.$apply() to dynamically update the html.
If I click the last element with .children().eq(-1).click(), it won't wait after the new element been created, what it actually clicks is the "cherry" div.
I tried to call $scope.$apply() before click(), it works, but I got this error message:
$apply already in progress
which I believe is not a good practise, and $apply() should not be called in this way.
When you add to the $scope you are inside a digest loop and the DOM won't be compiled and refreshed until those loops complete - basically in your $scope push you are changing the array but at that moment the rendered DOM still has the previous items. Once it exits the loop, the watchers from the repeater will fire and render the new DOM element, but you've already left your controller.
The solution is really simple. You just need to queue your update for the moment the current digest loop ends. You can do that using setTimeout with no delay. this will put your function on the queue and call it when the current processing is done:
setTimeout(function() {
$('#fruits').children().eq(-1).click();
}, 0);
Here is a working fiddle: http://jsfiddle.net/jeremylikness/bWeQL/
(I strongly encourage you to consider writing a directive for this to avoid manipulating the DOM directly from within your controller).
I am having a sortable list that gets populated by data in my Angular Controller. This list also contains an extra element containing some controls that can also be dragged
What I want do do, is make sure that the extra element stays in place after the $digest cycle is run, and I made a directive just for that case.
App.directive('ngIgnore', ['$timeout',function($timeout){
return {
link: function(scope, element){
scope.$watch(function(){
// Keep track of the original position of the element...
var el = element[0];
var siblings = Array.prototype.slice.call(el.parentNode.children);
var parent = el.parentNode;
var index = siblings.indexOf(el);
$timeout(function(){
// After the digest is complete, place it to it's previous position if it exists
// Otherwise angular places it to it's original position
var item;
if(index in parent.children)
item = parent.children[index];
if(!!item){
parent.insertBefore(el, item);
}
});
});
}
}
}]);
It worked, but not as I wanted it to... As you can see in this example shuffling the list does not move the ignored element, but the problem is that the $watch function gets executed infinitely since the $timeout triggers another $digest cycle... I tried changing the $timeout with setTimeout but no luck...
Is there any Angulary way to run code right after $digest is executed? Or is my entire logic wrong here?
Another (highly recommended) solution is to stop thinking in DOM manipulations.
Common problem with developers who start writing AngularJS code is tend to do DOM manipulations as the result of some event. Its common in jQuery:
$('#someid').click(function() { this.toggleClass('clicked') })
In AngularJS you should design your View to visualize your Model state (that should be in $scope). So
<div ng-click="clicked = !clicked" ng-class="{clicked: clicked}">I was clicked</div>
Same logic should be applied when designing components. In a HTML code you should put all visual logic - hide some elements using ng-show/ng-if, ng-switch. Add/remove classes using ng-class etc. So you define all possible model states.
Then by just changing model state you will get your view automatically updated reflecting current model state.
Same goes for repeated elements: you can repeat on some collection and then, depending on what element is present, you define how it would look. Keep in mind, that in ng-repeat each element will have own child (so mostly independed) Scope, so you can define some per-element manipulations.
See also this great answer: "Thinking in AngularJS" if I have a jQuery background?
You can try to use Scope.evalAsync Scope.evalAsync:
it will execute after the function that scheduled the evaluation
(preferably before DOM rendering).
So I am trying to use an external library function to create some manipulation in the DOM in one of my controllers that my ng-repeat is connected to. The problem is the following:
I am calling that external function in my controller that adds elements to the ng-repeat array , which in term adds the new elements to the DOM. However, when I am inside the controller, the element does not exist yet, even though I have added it to the array. How can I bound an external function to be called once the element has actually been appended to the DOM, rather than when it was actually added to the array that controls the ng-repeat?
I want to fire the event once the element has actually been created. Any suggestions?
Let me know if you would like to see a fiddle of this idea.
As mentioned in the comments, a check-last directive that checks for $last can be used to determine when the last iteration of ng-repeat is executing.
.directive('checkLast', function() {
return function (scope, element, attrs) {
if (scope.$last === true) {
element.ready(function() { // or maybe $timeout
... do something ...
});
}
}
});
See also https://stackoverflow.com/a/14656888/215945