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/
Related
I have a video player directive that uses an ng-src in its template. How do I run directive code after the ng-src has been evaluated so the video is actually loaded?
Here is the directive code:
return {
restrict: 'A',
replace: false,
transclude: false,
scope: true,
templateUrl: "/modules/didyouknow/views/slideshow-frame.directive.client.view.html",
link: {
pre: function() {
console.log('a');
},
post: function(scope, element, attrs) {
/**
* scope.frame - frame information
*/
scope.frame = scope[attrs.slideshowFrame];
}
}
};
both link functions execute before {{expr}} has been evaluated in the template.
The whole point of post link is it that it's executed after child post-links, in reverse order as pre links. So why isn't it executing last? It executes immediately after the prelink function so why are they even separate functions?
You could have $observe inside your directive that will work same as that of the $watch but the difference is it evaluates {{}} interpolation expression.
Inside $observe you could see if the ng-src has value the only call the directive method. otherwise wait.
link: function(scope, element, attrs){
attrs.$observe('ngSrc', function(newVal, oldVal){
if(newValue){
//call desired code when `ng-src` have value
}
});
}
There is a couple of recipes to execute the code in link at the moment when directive DOM 'is there'. One is using zero-delayed timeout
$timeout(function () {
...
});
It is is generally preferable if you're after rendered DOM or interpolated values. It is not an option here, because templateUrl is used and directive template is loaded asynchronously, and the template is not available during the linking phase.
Another recipe is using scope watchers/attribute observers (one-time if you don't care about data bindings).
var unobserveNgSrc = attrs.$observe('ngSrc', function (ngSrc, oldNgSrc) {
if (!ngSrc) return;
unobserveNgSrc();
...
})
It executes immediately after the prelink function so why are they even separate functions?
This behaviour suggests what it can be used for. In parent preLink some things can be done that must precede child preLinks or the latter could benefit from, which you will rarely find useful for simple directives. And parent postLink executes last, and that's a good moment for 'ok, all of my children are already compiled and linked, let's do something at last'.
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/
I have this code:
<body ng-controller="testController">
<div test-directive transform="transform()">
</div>
<script type="text/ng-template" id="testDirective.html">
<div>
<p>
{{transform()}}
</p>
</div>
</script>
<script>
angular.module("Test", [])
.directive("testDirective", function() {
return {
templateUrl: "testDirective.html",
scope: {
transform: "&"
},
link: function(scope) {
}
};
})
.controller("testController", function($scope) {
$scope.transform = function() {
return "<a ng-click='somethingInController()'>Do Something</a>";
};
$scope.somethingInController = function() {
alert("Good!");
};
});
</script>
</body>
So basically what I want to accomplish is to create a directive with a method that will be called from the controller. And that method will do something with the values passed (in this example it does not receives nothing, but in the real code it does).
Up to that point is working. However, the next thing I want to do is create an element that will call a method in the controller. The directive does not knows what kind of element will be (can be anything) nor what method will be. Is there any way to do it?
Fiddle Example:
http://jsfiddle.net/abrahamsustaita/C57Ft/0/ - Version 0
http://jsfiddle.net/abrahamsustaita/C57Ft/1/ - Version 1
FIDDLE EXAMPLE WORKING
http://jsfiddle.net/abrahamsustaita/C57Ft/2/ - Version 2
The version 2 is now working (I'm not sure if this is the way to go, but it works...). However, I cannot execute the method in the parent controller.
Yes. However there is a few problems with your code. I will start by answering your question.
<test-directive transform='mycustommethod'></test-directive>
// transform in the directive scope will point to mycustommethod
angular.module('app').directive('testDirective', function() {
return {
restrict: 'E',
scope: {
transform: '&'
}
}
});
The problem is that printing the html will be escaped and you will get < instead of < (etc.). You can use ng-bind-html instead but the returned html will not be bound. You will need to inject the html manually (you can use jquery for this) in your link method and use var compiled = $compile(html)(scope) to bind the result. Then call ele.after(compiled) or ele.replace(compiled) to add it to your page.
I finally get to get it working.
The solution is combined. First of all, I needed to add another directive to parse the element I wanted:
.directive("tableAppendElement", function ($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs) {
var el = angular.element("<span />");
el.append(attrs.element);
$compile(el)(scope);
element.append(el);
}
}
})
This will receive the element/text that will be appended and then will registered it to the scope.
However, the problem still exists. How to access the scope of the controller? Since my directive will be used by a lot of controllers, and will depend on the model of the controller, then I just set scope: false. And with that, every method in the controller is now accessible from the directive :D
See the fiddle working here. This also helped me because now, there is no need to pass the transform method, so the controller can be the one handling that as well.
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.
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)
});
}
};
});