AngularJS detect when $compile HTML string is done - angularjs

I'm using $compile to compile a directive on the fly. What I would like to know is if there is a way to detect when the directive is done compiling (promise?) so that I can append it to the DOM. I want to do something like this:
function newMessage() {
var directive = $compile("<div select-contacts message=\"newShare\"></div>");
// Compile directive
directive($scope).then(function(compiled) {
// After compiling, append it somewhere in the DOM
angular.element('#new_message').html(compiled);
});
}
I've searched the documentation of $compile and I'm still not clear on how to do something like this, or if it's even possible.
Update
Here is what I have currently. This works on localhost with the timeout set to 0, however on production it only works when I introduce a delay greater than 50ms. I have 250ms to be safe, but that seems arbitrary.
function newMessage() {
angular.element('#new_message_container').html($compile("<div select-contacts message=\"newShare\"></div>")($scope));
$timeout(function() {
angular.element('#new_message').html(compiled);
content: angular.element('#new_message'),
elem: angular.element('#new_message_container'),
width: '1024px',
height: '480px'
});
});
}
Basically I have a container element that is hidden. I compile the directive and place it inside the hidden container. Then within a timeout block, I open a modal, which moves the root directive element from the hidden container to the modal element. When I run this on localhost it works. The modal opens and the directive is already compiled. On production, the directive appears not to be finished compiling before the code in the timeout block is called. The effect being that the modal opens but is empty.

To wait for the compile to finish and to be applied to the dom, a $timeout at 0 should suffice for all your needs:
function newMessage() {
var directive = $compile("<div select-contacts message=\"newShare\"></div>");
// Compile directive
$timeout(function(){
angular.element('#new_message').html(compiled);
}, 0);
}
Timeout at 0 executes your inner function at the next angular digest cycle: in other words, immediately after bindings are executed (and hence all dom elements are compiled and injected in the dom).

Related

Analyze transcluded content

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.

How to override an ng-click directive to not automatically run $apply

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/

Testing ng-if using $compile

I'm attempting to test an ng-if in one of my templates by compiling the view against a pre-defined scope and running $scope.$digest.
I'm finding that the compiled template is coming out the same regardless of whether my condition is truthy or falsy. I would expect the compiled html remove the ng-if dom elements when falsy.
beforeEach(module('templates'));
beforeEach(inject(function($injector, $rootScope){
$compile = $injector.get('$compile');
$templateCache = $injector.get('$templateCache');
$scope = $rootScope.$new();
template = angular.element($templateCache.get('myTemplate.tpl.html'));
}));
afterEach(function(){
$templateCache.removeAll();
});
it ('my test', function(){
$scope.myCondition = true;
$compile(template)($scope);
$scope.$digest();
expect(template.text()).toContain("my dom text");
// true and false conditions both have the same effect
});
Here's a plunkr attempting to show what's happening (not sure how to test in plunkr, so I've done it in a controller) http://plnkr.co/edit/Kjg8ZRzKtIlhwDWgB01R?p=preview
One possible problem arises when the ngIf is placed on the root element of the template.
ngIf removes the node and places a comment in it's place. Then it watches over the expression and adds/removes the actual HTML element as necessary. The problem seems to be that if it is placed on the root element of the template, then a single comment is what is left from the whole template (even if only temporarily), which gets ignored (I am not sure if this is browser-specific behaviour), resulting in an empty template.
If that is indeed the case, you could wrap your ngIfed element in a <div>:
<div><h1 ng-if="test">Hello, world !</h1></div>
See, also, this short demo.
Another possible error is ending with a blank template, because it is not present in the $templateCache. I.e. if you don't put it into the $templateCache exlicitly, then the following code will return undefined (resulting into an empty element):
$templateCache.get('myTemplate.tpl.html')

how to use angular chrome debug tool to debug across iframe

I have an iframe nested in my app that has an angular project in it.
How can I get access to the scope inside that?
The magic is in <iframe>.contentWindow
When going from the parent to the child iframe (which is what you do when debugging), you'll want get the child window object, and use its definition of angular.
var w = $('iframe')[0].contentWindow;
w.angular.element(<selector>).scope();
Note that this does work with chrome's development shortcuts, so you can do
w.angular.element($0).scope()
to get the scope of the currently selected element.
EDIT:
You can also directly assign the parent window's angular object to the child's definition (though I wouldn't recommend it if the parent is using angular itself)
window.angular = w.angular
This makes the parent effectively transparent, so you can use angular as normal
angular.element($0).scope()
and it has the advantage of fixing angular console tools like Batarang
Note: the iframe must be from the same origin in order for this to work. For instance, trying this in plunker won't work as the iframe is generated dynamically, and has a source of "about:blank".
Select iframe from debug options and do something like below:
jQuery('#my_element_id').scope();
To do this with javascript, I have setup a plunker link: http://plnkr.co/edit/whVo7wanhmcUfC7Nem9J?p=preview
Below js method access the scope of a element and passes to the parent method:
var passScopeToParent = function() {
var scope = angular.element(jQuery("#myListItem")).scope();
parent.change(scope);
}
parent widnow method accesses scope and changes its expressions:
var change = function(scope) {
scope.$apply(function() {
scope.department = 'Superhero';
});
}
You can tweak this link to access the scope an element within iframe from parent window and print it in the console window.
Late to the party. What I do is put a button in the directive and attach it to a function that prints the scope.
<button ng-click="echoScope()">Echo Scope</button>
$scope.echoScope = function() { console.debug($scope); };

Assuring $digest Is called Following non Angular Code Execution

I have a UI component which has a $watch callback on its width (the reason is not relevant for this post).
The problem is that in some cases:
The width is changed from a non angular context ->
There is no $digest cycle ->
My $watch callback is not called.
Eventhough my application is a full angular application there are still cases in which code is executed in non angular context. For example:
JQuery calles window.setTimeout - so even if my code called JQuery from within angular context the timeout callback is called in non angular context and my $watch callback will not be executed afterwards.
By the way, even angular themselves call window.setTimeout in their AnimatorService...
So my question is:
How can I make sure a $digest cycle is always performed after any code is executed? (even when the code is a 3rd party code...)
I thought about overriding the original window.setTimeout method but:
It feels a bit ugly and dangerous.
I'm afraid it won't cover all use cases.
Adding a plunker.
The plunker sample contains:
An element which can be hidden using JQuery fadeOut method.
A button which executes the fadeOut call for hiding the element.
A text showing the element display status (Shown!!! or Hidden!!!). This text is updated by $watching on the element display property.
A button which does nothing but to initiate some angular code so that a $digest cycle is called.
Flow:
Click the Fade Out button -> the element will be hidden but the status text will remain Shown!!!.
You can wait forever now - or:
Click the Do Nothing button -> suddenly the text will change.
Why?
When clicking the Fade Out button JQuery.fadeOut calls the window.setTimeout method. After that my $watch callback is called but the element is still not hidden.
The element is only hidden after the timeout callback is called - but then there is no $digest cycle (and i have no way that i know of to trigger one).
Only on the next time an angular code will run my $watch function will be called again and the status will be updated.
AngularJS provides a special $apply method on the scope object to allow you to execute code from outside the AngularJS framework.
The $apply function will execute your code in the correct context and apply a $digest cycle afterwards so you don't have to deal with that yourself.
To implement it in your code, you can:
// Get element
var $element = $('#yourElement');
// Get scope
var scope = angular.element($element).scope();
// Execute your code
scope.$apply(function(scope){
// Your logic here
// All watchers in the scope will be triggered
});
(The scenario above can change depending on your actual application).
You can read more about the $apply method of the scope object right here: http://docs.angularjs.org/api/ng.$rootScope.Scope
Hope that helps!
Looking at your plunker, you could add a callback on the call to animate to manually trigger an update to the Angular scope once the animation is complete:
Before:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow');
};
var getDisplay = function() {
return animatedElement.css('display');
};
$scope.$watch(getDisplay, function(display) {
console.log('$watch called with display = `' + display + '`');
$scope.display = display === 'none' ? 'Hidden!!!' : 'Shown!!!';
});
After:
$scope.fadeOut = function() {
animatedElement.fadeOut('slow', function() { $scope.$digest(); });
};
This would cause your watch on getDisplay to be called when the animation is complete, by which time it will return the correct value.

Resources