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).
Related
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.
I'm trying to get some graphing working with ng and d3, just having issues with digest cycle warnings.
I have a directive that does the graphing for me, and I want to filter the data when my checkboxes (that are bound to the same data) are changed.
This is the filter that I'm using:
<div data-d3-line data-chart-data="vm.chartData | filter:{show:true}" data-data-updated="vm.dataUpdated"></div>
If the filter is removed, the binding happens, no errors. I'm sure this is something simple that I'm overlooking, but it's one of those pull your hair out moments.
I put together this plunker in hopes of getting a hand:
http://plnkr.co/edit/5QVOvNKw0AqEogihcdyO
P.S. I know that I'm using a poor man's eventing with that dataUpdated watch. I was originally watching the vm.chartData, and thought that caused the error.
The filter can't be used with a two-way binding = of an isolated scope.
This is because in the two-way binding, the expression will be watched for an identity change. But everytime the expression is evaluated, the filter will produce a different (in identity) array, thus a digest cycle will go into a loop.
To solve this problem, it depends on how you use the vm.chartData.
If the d3 directive don't need to update and sync the chartData back to parent. One solution is to not use the two-way binding and manually watch the expression instead. For example:
var directive = {
scope: {
data: '&chartData' // use & instead of = here
},
link: function link(scope, element, attrs) {
scope.$watch('data()', function (newData) {
Update(angular.copy(newData));
}, true); // watch for equality, not identity (deep watch)
}
};
Or if each item of chartData will not be changed, may be using $watchCollection is enough.
scope.$watchCollection('data()', function (newData) {
Update(angular.copy(newData));
});
Example Plunker: http://plnkr.co/edit/0xArS1VAbZCwOpo4VYrw?p=preview
Hope this helps.
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 would like to subscribe to an ngChange event, but from code rather than the markup. That is, given a $scope and an expression that is bindable via ngModel, I want to subscribe to any changes made to that expression by any ngModel directive that binds to that expression. Is this possible?
something like:
$scope.field = "hello";
$scope.onButtonClick = function() {
$scope.field = "button clicked!";
}
// this callback is only when the user types in an input bound to field
// not when they click the button with ng-click="onButtonClick()"
$scope.$watchNgChange("field", function() {
console.log("user caused field to change via a binding");
});
// this callback is called for both ngModel binding changes and onButtonClick.
$scope.$watch("field", function() {
console.log("field was changed");
});
I can't just use $watch, because that will capture all changes, including those from loading the data from the database, from ng-click callbacks, and changes initiated from $watch callbacks for other expressions (in this case, if there are any circular references, then it's too easy to have $watch callbacks to get into an infinite loop and error out after 10 digest cycles), and who knows what else.
First, anytime I have tried to do something like this is turned out to be a bad idea - I was just working around design problems in my code or logic problems in my business logic. In general, the code should not care HOW the data was changed, only that it HAS changed.
Second, $watch can give you both the old and new value - this has been enough for me. if the old value not equal to the new value, I want to update the related data model(s). If that old and new value are equal, I want to ignore the update.
Finally, You may consider using resolve with your routes eliminate "database loading" as the fully located data can be passed into your controller (assuming you return a promise).
.
Jeremy you just describe what in Angular is known as a Directive. It is best practice to always use directives each time you need to touch the DOM. This logic should never live in the controller or even the Service.
directives are a big tricky but there is tons of documentation for it.
Visit docs.angularjs.org/directives
Don't do it, it sounds like you are trying to introduce the DOM logic (e.g. if the user has interacted with a DOM element) into the controller.
If you read the source code of a ngChange directive, you will found it requires a ngModel which is used as the bridge between the view and the controller.
I recommend creating a copy of the model and used the copy for data binding using ngModel+ngChange in your view, and then you can $watch that copy and do whatever you want.
$scope.field = "hello"; //the field you care
$scope.fieldCopy = $scope.field; //use 'fieldCopy' for databinding
In the html code you can have multiple way of changing the model fieldCopy
<input ngModel="fieldCopy" name='foo' />
<input ngModel="fieldCopy" name='foo2' />
You then watch the fieldCopy for changes related to user interaction and copy the change to 'field':
$scope.$watch("fieldCopy", function() {
$scope.field = $scope.fieldCopy;
console.log("user caused field to change via a binding");
});
If you want to keep fieldCopy in sync with field, add another watch:
$scope.$watch("field", function() {
$scope.fieldCopy = $scope.field;
});