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.
Related
I have a directive that defines a function innerBarStyle() at the link stage and binds it to the scope:
restrict : 'EA',
scope: {
values: '='
},
link: function(scope, elements, attributes){
scope.innerBarStyle = function(value){
console.count("innerBarStyleCounter");
return {
width: 10px;
};
}),
templateUrl: 'template.html'
};
The function does nothing but counting the number of times it gets executed and returning an object.
Now, in a template directive's template I'm calling this function by means of an expression. Something like <div ... ng-style=innerBarStyle(someValueInCurrentScope)><div>
What I get in practice is an infinite loop that causes the aforementioned function to be called repeatedly.
After some research, I've found that this usually occurs when the called function implicitly or explicitly triggers the digest cycle (e.g. if it makes use of the $http service). But in this case the function is really doing nothing. Is it possible that the digest cycle is triggered somewhere else or am I missing something?
BTW, I know that there would be better ways to achieve the same result, I'm just curious about how things works here.
Without seeing the actual code (I understand you can't post the exact code since it's for your work) I can only guess. But I think what's happening is that you are adjusting the style of the element via the return of the $scope. innerBarStyle which triggers the ng-style directive which calls a digest cycle, which triggers the scope function again. Ergo the continuous execution of this logic.
In order to fix this you should probably use the angular.element APIs on the elem of the directive to adjust the CSS.
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/
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/
If you prefer to see the question in working code, start here: http://jsbin.com/ayigub/2/edit
Consider this almost equivalent ways to write a simple direcive:
app.directive("drinkShortcut", function() {
return {
scope: { flavor: '#'},
template: '<div>{{flavor}}</div>'
};
});
app.directive("drinkLonghand", function() {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
scope.flavor = attrs.flavor;
}
};
});
When used by themselves, the two directives work and behave identically:
<!-- This works -->
<div drink-shortcut flavor="blueberry"></div>
<hr/>
<!-- This works -->
<div drink-longhand flavor="strawberry"></div>
<hr/>
However, when used within an ng-repeat, only the shortcut version works:
<!-- Using the shortcut inside a repeat also works -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-shortcut flavor="{{flav}}"></div>
</div>
<hr/>
<!-- HOWEVER: using the longhand inside a repeat DOESN'T WORK -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-longhand flavor="{{flav}}"></div>
</div>
My questions are:
Why does the longhand version not work inside an ng-repeat?
How could you make the longhand version work inside an ng-repeat?
In drinkLonghand, you use the code
scope.flavor = attrs.flavor;
During the linking phase, interpolated attributes haven't yet been evaluated, so their values are undefined. (They work outside of the ng-repeat because in those instances you aren't using string interpolation; you're just passing in a regular ordinary string, e.g. "strawberry".) This is mentioned in the Directives developer guide, along with a method on Attributes that is not present in the API documentation called $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
So, to fix this problem, your drinkLonghand directive should look like this:
app.directive("drinkLonghand", function() {
return {
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
attrs.$observe('flavor', function(flavor) {
scope.flavor = flavor;
});
}
};
});
However, the problem with this is that it doesn't use an isolate scope; thus, the line
scope.flavor = flavor;
has the potential to overwrite a pre-existing variable on the scope named flavor. Adding a blank isolate scope also doesn't work; this is because Angular attempts to interpolate the string on based on the directive's scope, upon which there is no attribute called flav. (You can test this by adding scope.flav = 'test'; above the call to attrs.$observe.)
Of course, you could fix this with an isolate scope definition like
scope: { flav: '#flavor' }
or by creating a non-isolate child scope
scope: true
or by not relying on a template with {{flavor}} and instead do some direct DOM manipulation like
attrs.$observe('flavor', function(flavor) {
element.text(flavor);
});
but that defeats the purpose of the exercise (e.g. it'd be easier to just use the drinkShortcut method). So, to make this directive work, we'll break out the $interpolate service to do the interpolation ourself on the directive's $parent scope:
app.directive("drinkLonghand", function($interpolate) {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
// element.attr('flavor') == '{{flav}}'
// `flav` is defined on `scope.$parent` from the ng-repeat
var fn = $interpolate(element.attr('flavor'));
scope.flavor = fn(scope.$parent);
}
};
});
Of course, this only works for the initial value of scope.$parent.flav; if the value is able to change, you'd have to use $watch and reevaluate the result of the interpolate function fn (I'm not positive off the top of my head how you'd know what to $watch; you might just have to pass in a function). scope: { flavor: '#' } is a nice shortcut to avoid having to manage all this complexity.
[Update]
To answer the question from the comments:
How is the shortcut method solving this problem behind the scenes? Is it using the $interpolate service as you did, or is it doing something else?
I wasn't sure about this, so I looked in the source. I found the following in compile.js:
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
var match = definiton.match(LOCAL_REGEXP) || [],
attrName = match[2]|| scopeName,
mode = match[1], // #, =, or &
lastValue,
parentGet, parentSet;
switch (mode) {
case '#': {
attrs.$observe(attrName, function(value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = parentScope;
break;
}
So it seems that attrs.$observe can be told internally to use a different scope than the current one to base the attribute observation on (the next to last line, above the break). While it may be tempting to use this yourself, keep in mind that anything with the double-dollar $$ prefix should be considered private to Angular's private API, and is subject to change without warning (not to mention you get this for free anyway when using the # mode).
I'm trying to build a directive to output some HTML formatted code for a paging control (Twitter Bootstrap styled), this directive needs to take the current page and total pages from the scope of my controller and when a paging link is clicked trigger a function on my controller to change the page (builds a url and calls $location to change page).
I've watched many of the excellent YouTube angularjs videos (http://www.youtube.com/watch?v=nKJDHnXaKTY) but none seem to cover this particular complex scenario.
Any help would be great!
Here is jsfiddle that makes it clearer:
http://jsfiddle.net/philjones88/dVFDT/
What I can't get working is passing the parameter, I get:
changing page to: undefined
In your directive add the changePage call there (I know it's not where you want it). Have it call the parents scope changePage with the same parameter.
$scope.changePage = function(index){
console.log("running changePage");
$scope.$parent.changePage(index); //might want to check if the parent scope has this too
}
As another tip, in directives you shouldn't use the $ in front of the variables being sent in. In this case that would be $scope, $element, $attrs. The $ you see in front of scope in controllers (not linking functions) is there to let you know that it is being injected. It is not being injected in the linking controller. For instance, here:
app.directive("pager", function ($injector1, $injector2) {
This is where injected parameters would go, and you want to be able to distinguish the two of them. I realize this got a little off track and I hope the suggestion I have for the changePage is what you're looking for.
Edit: Updated fiddle http://jsfiddle.net/dVFDT/48/
Modified answer for future searchers: The function you were passing in via the click method like so:
..... click="changePage()".....
Needed to be changed to:
..... click="changePage".....
This means you're passing the function through and not the function call. This meant that in your directive when you wired up the changePage callback you were calling the function with the index like this:
changePage()(1)
and that's why you were getting undefined.
I dont understand completely, but at the end of your directive you want to execute a function of your controller?
Try:
<div class="pagination">
<pager current-page="currentPage" total-pages="totalPages" query="query" callback="changePage()"></pager>
</div>
I realize this question is a bit old, but there's actually another way to solve this that doesn't require recompiling or calling the parent scope. It does, however, require calling the method from within the directive in a slightly different way.
You can see the fiddle here: Fiddle
The line that's of most interest is in the template declaration. The call to onClick requires you pass it an object rather than just the value.
template:
"<div ng:repeat='i in [] | range:totalPages'> " +
"<a ng:click='onClick({page: i + 1})'>Page {{i + 1}}</a>" +
"</div>",
This also makes use of a filter from this answer from Gloopy in order to iterate n number of times in an ng:repeat. This allows the binding to all happen in the template.