AngularJS: Using $compile on html that contains directives with templateurl - javascript

I have a legacy application that has some content inserted into the DOM via jQuery. I would like the legacy parts of the codebase to be responsible for compiling the html that it inserts into the DOM.
I can get it to compile the initial html using $compile, but any DOM elements added by a directive's template or templateUrl are not compiled, unless I call $scope.$apply() from within the directive itself.
What am I doing wrong here?
Link to fiddle: http://jsfiddle.net/f3dkp291/15/
index.html
<div ng-app="app">
<debug source='html'></debug>
<div id="target"></div>
</div>
application.js
angular.module('app', []).directive('debug', function() {
return {
restrict: 'E',
template: "scope {{$id}} loaded from {{source}}",
link: function($scope, el, attrs) {
$scope.source = attrs.source
if( attrs.autoApply ) {
// this works
$scope.$apply()
}
},
scope: true
}
})
// mimic an xhr request
setTimeout(function() {
var html = "<div><debug source='xhr (auto-applied)' auto-apply='1'></debug><br /><debug source='xhr'></debug></div>",
target = document.getElementById('target'),
$injector = angular.injector(['ng','app']),
$compile = $injector.get('$compile'),
$rootScope = $injector.get('$rootScope'),
$scope = angular.element(target).scope();
target.innerHTML = $compile(html)($scope)[0].outerHTML
// these do nothing, and I want to compile the directive's template from here.
$scope.$apply()
$scope.$root.$apply()
angular.injector(['ng','app']).get('$rootScope').$apply()
}, 0)
output
scope 003 loaded from html
scope 005 loaded from xhr (auto-applied)
scope {{$id}} loaded from {{source}}
Update: Solution works for directives with a template property, but not templateUrl
So, I should have been compiling dom nodes, not an HTML string. However, this updated fiddle shows the same failing behavior if the directive contains a templateUrl:
http://jsfiddle.net/trz80n9y/3/

As you probably realised, you need to call $scope.$apply() for it to update the {{bindings}} from the scope values.
But the reason you couldn't do it inside your async function was that you were compiling the HTML against the existing scope for #target, but then trying to append just the HTML. That won't work, because you need to have the compiled node in the DOM, either by appending the entire compiled node using jQuery's .append() or similar, or by setting the DOM innerHTML first, then compiling the node that is in the DOM. After that, you can call $apply on that scope and because the directive is compiled and in the DOM, it will be updated correctly.
In other words, change your async code as follows.
Instead of:
target.innerHTML = $compile(html)($scope)[0].outerHTML
$scope.$apply()
Change it to:
target.innerHTML = html;
$compile(target)($scope);
$scope.$digest();
Note that I did a $digest() instead of $apply(). This is because $apply() does a digest of every single scope, starting from the $rootScope. You only need to digest that one scope you linked against, so it is sufficient (and faster, for any reasonably sized app with lots of scopes) to just digest that one.
Forked fiddle
Update: Angular can compile strings and detached DOM nodes
I just checked, and the OP was actually correct in assuming that Angular can compile strings of HTML or detached DOM nodes just fine. But what you do need to do is make sure you actually append the compiled node to the DOM, not just the HTML. This is because Angular stores things like the scope and the binding information as jQuery/jQueryLite data on the DOM node*. Thus you need to append the whole node, with that extra information, so that the $digest() will work.
So an alternative way of having this work is to change the same portion of the OP's code as above to:
target.appendChild($compile(html)($scope)[0]);
$scope.$digest()
* Technically, it is stored in the internal jQuery data cache, with the cache key being stored on the DOM node itself.

Append the element to the target first, then compile it.
html = angular.element(html);
target = angular.element(target);
target.append(html);
html = $compile(html)($scope)
http://jsfiddle.net/f3dkp291/16/

Related

$compile return html in object

so im calling a html template and want to bind the data with angular, so i get the data to bind, i get the html, when i try to compile it will return all the html binded but in (i think) object, what can i do to make it html.
This is the code
$.get("file.html", function(partial){
var scope = $rootScope.$new();
scope.data = result;
var el = angular.element(partial);
var compiled = $compile(el)(scope);
var finalHtml = el[0];
$timeout(function(){
var calendar = window.open();
calendar.document.write(finalHtml);
calendar.focus();
calendar.print();
});
});
I already try .html .toString String() nothing worked
Thank you in Advance
Your compiled variable is an angular jQuery or jqlite element that can be inserted into your document. If you want to get the html for it, you can use use the outerHTML attribute on the underlying node (you get the underlying node by grabbing the first array element compiled[0]) - https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML
var compiled = $compile(el)(scope);
// scope.$digest() // only call if not within an angular $digest already
$timeout(function() {
var finalHtml = compiled[0].outerHTML;
...
}
According to the documentation "After linking the view is not updated until after a call to $digest which typically is done by Angular automatically." so you either have to manually call scope.$digest() or actually use one the angular API to do the request using either $http or preferably using $templateRequest like #ThinkingMedia suggested. After the angular $digest has run, then you can access the updated view.
I created a plunker here that shows how it all works properly using just the AngularJS api: http://plnkr.co/edit/rFcfgB3FWhsfyySfr0rU?p=preview
I also changed how the popup is opened a bit to deal with the security implication of doing popups.

Interact with compiled Angular directives HTML

I'm creating two Angular directives, fooContainer and foo, where the fooContainer will load one or more foo elements and render them inside its container.
Now I also want to attach some JavaScript events to the foo directives compiled HTML from the fooContainer directives link function since the container is supposed to be responsible for some things like dragging the compiled foo elements around.
Since $compile is asynchronous when compiling directives with templateUrl the HTML isn't available directly after calling $compile(html)(scope), however if I use a timeout for about a second the HTML is rendered in and I can interact with it.
This isn't optimal. Does $compile expose an event or promise I can use to be notified when all HTML is rendered?
Here is a Plunker that describes my problem, http://plnkr.co/edit/coYdRqCsysV4txSFZ6DI?p=preview
Approaches in order of preference:
1) Follow pattern of simmi simmi whenever you can and use angular (ng-...) approach. This is most reliable.
1.5) UPDATE: Liquinaut 'attribute directive' approach below seems valid - I've only used it in a quick demo POC and it worked fine. Assuming this survives more complex usage I would prefer over the watch/jquery option 2 below. Please note however that the $last is specifically for ng-repeat. If you are injecting a $compile chunk of non-repeating markup as per the OP then you need to drop the $last check. And to be clear this requires you to add the attribute post-render to the element you are waiting to render i.e. (per OP plunker)
var el = $compile('<foo><div class="hide" post-render>...
with directive:
.directive('postRender', function() {
return function(scope, element, attrs) {
//your code here
element.hide();
};
});
I've forked the original plunkr and generalized the approach to allow passing a generic callback: http://plnkr.co/edit/iUdbn6zAuiX7bPMwwB81?p=preview
NOTE: This approach only works for basic activities on the element compiled in. If your compile string has angular variable interpolation e.g. {{value}} and you rely on these being resolved in the callback it won't work. They are not resolved at this time. This is also true if the postRender directive is rewritten with an explicit link function. Option 2 below works fine for these cases as it pushes resolution to at least the next digest.
2) I've found watching the DOM very reliable even in very complex apps (although performance should as always be monitored). Replace your el.find('.hide').hide(); line with this block:
scope.$watch(
function() { return element.find('.hide').length > 0;},
function(newVal, oldVal) {
if (newVal) {
//DO STUFF
el.find('.hide').hide();
}
}
);
I wouldn't be comfortable using this in a tight loop but for one off usage on directive instantiation (assuming you aren't creating a 1000 of them!) it seems reasonable - I used this approach for ng/ui-slider integration etc
3) pixelbits approach also good architectural approach if you are building something more complex (and for reusable components) but beware the extra scope that gets created if you are using transclude (e.g. nested directives) it will be $$nextSibling that gets the 'emit'. See: here
BTW: if just want a quick way to do drag and drop see: http://codef0rmer.github.io/angular-dragdrop/#/
The directive fires a postRender event:
fooContainer.directive('postRender', function() {
return function(scope, element, attrs) {
if (scope.$last){
//your code here
}
};
});
Hope that helps!
http://plnkr.co/edit/f4924y6GW7rAMItqVR0L?p=preview
.directive('fooContainer', function($compile, $timeout) {
return {
link: function(scope, element, attributes) {
console.log('link called');
var el = $compile('<foo><div class="hide" >I should always be hidden.</div><div class="hideDelay" ng-show="visiblity">I should be hidden after 1 second.</div></foo>')(scope);
element.append(el);
scope.visiblity=false;
},
restrict: 'E',
template: '<div class="fooContainer"></div>'
}
});
why Dont you try using ng-show/ng-hide
You can safely attach events to the element in the directive's link function, but only for the directive's children. The directive's parent haven't been linked yet.
So within fooContainer's link function, you know that foo has already been compiled and linked, and it's safe to attach events.
If foo needs to be notified once fooContainer is linked, you can use some form of inter-directive communication. i.e. $scope.$emit. If you want to attach events to foo, you can also use a jquery selector inside fooContainer's link function.
According to Angular documentation:
templateUrl
Same as template but the template is loaded from the specified URL.
Because the template loading is asynchronous the compilation/linking
is suspended until the template is loaded
This means your template is already loaded by the time your link function executes. A promise object is not needed.

Compile transcluded content before directive's link function executes

I'm looking to compile the contents of transcluded stuff before the link function executes. Currently, if I transclude an ng-bind-safe, the contents will not be added until after my link function.
I can do a scope.$apply() in the link function (and it works) but I get console errors since the digest cycle is already in progress.
Thoughts? Thank you!
During the compilation and linking phases ($compile and $link functions), you only have access to your template in your $compile function, and access to your scope and template in the $link function. You do not have access to the rendered template yet because it hasn't happened yet. For that, you need to setup a watch expression, which you will provide call back functions for. Angular will let you know when the value that you're watching has changed, and within this call back you have access to the rendered template.
This watch expression can only be done within the $link function because that is only place in the directive that you can properly access the scope.
Here is an example:
app.directive('tmTime', function() {
return {
restrict: 'A',
template: '<div>{{time}}</div><div ng-transclude></div>',
transclude: true,
link: function (scope, element, attr) {
scope.time = 'Its hammer time!';
scope.$watch('time', function(newVal, oldVal) {
// this is your call back function
// within here, you have access to the rendered template
alert(element[0].outerHTML); // (it's hammer time! in first div, transcluded contents in second div)
});
}
};
});

Angular.js UNIT test directive

I'm trying to make a unit test for angular directive. So this is the code:
describe('Test', function () {
var element,
scope;
// load the service's module
beforeEach(module('app'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope.$new();
element = angular.element('<div id="wow"></div>');
$compile(element)(scope);
element.scope().$apply();
}));
it('should', function () {
console.log(document.getElementById('wow'));
});
});
I got null in the console.
Of course this is just a test case. In real code getElementById is used by plugin which is wrapped with my directive, which is I'm trying to test.
So my question is: why the DOM element which is just compiled not be found by getElementById. If this is correct behavior, how can I avoid this mistake.
Thanks
You need to append the element in the document somewhere. Here's working plnk.
Only thing thing that I added:
angular.element(document.body).append(element);
The DOM element isn't found because jasmine never actually attaches it to the DOM. If you want to get the element, just refer to it as element. For example:
it('should', function() {
expect(element.attr('id')).toBe('wow');
});
You can attach the element to the DOM manually, but remember to remove it when you're done with your test (I use the afterEach blocks for this). Jasmine uses the same DOM for your entire test suite, so if you forget to remove an element, you could get false positives or other unexpected results in your tests from elements being in the DOM that you didn't expect to be there. In general, you shouldn't have to attach your element to the DOM unless you're testing certain interactions (blur works without attaching to the DOM, but focus doesn't, for example).

Access the compiled template in a directive

If I do elm.html() in the link function then I get the uncompiled template with {{curly_brackets}}. How do I get access to the compiled html where the {{template_variables}} have been replaced by the correct scope variables.
If this is not possible from the directive then what is a good alternative solution? Ultimately, the purpose of the directive is to get the height of the compiled element.
Here is a example fiddle: http://jsfiddle.net/RfHx8/1/
You have to let the Angular get finished before you check the height. To do this you can use setTimeout (or preferably the $timeout service) with a zero delay:
link: function(scope, elm, attrs) {
$timeout(function(){
var height = elm.height();
},0,false);// No delay is necessary
}
See the update for an example: http://jsfiddle.net/RfHx8/2/

Categories

Resources