We're using Angular and we're having trouble with resolving variables in directives.
This fiddle shows our issue:
Here's the full code: http://jsfiddle.net/VX5LE/65/
//data-translate should handle the translating of the useableButton text
app.directive('window', ['translateService', function (translateService) {
return {
restrict: 'E',
transclude: true,
scope: {
useableButtons: '='},
replace: true,
template:
'<div>' +
'<button data-ng-repeat="useableButton in useableButtons" data-translate>{{useableButton}}</button>' +
'</div>'
};
}]);
I have seen some answers that solve this by:
Using a filter to translate these. - That is actually our current solution but that hinders us with different functionality.
Attaching watches in the controller. - We actually want to avoid watches in our controllers as it makes the code quite dirty if you have a lot of them.
Preferably I would like to see a solution that resides inside of the translate directive without cluttering the controllers.
You can do this by interpolating the values manually, then parsing it with the $eval function of the desired scope.
Here is the fiddle: http://jsfiddle.net/VX5LE/66/
The code of the translate-directive:
app.directive('translate', ['translateService', '$interpolate', function (translateService, $interpolate) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var pHTML = element.html();
var parsed = $interpolate(pHTML);
var translated_result = translateService.translate(scope.$eval(parsed));
element.text(translated_result);
}
}
}]);
Related
I'm in need of building a transformation directive that transforms custom directives into html.
Input like: <link text="hello world"></link>
should output to: <a class="someclass" ng-click="linkClicked('hello world')"></a>
linkClicked should be called on the parent controller of the directive.
It would have been very easy if I was the one responsible for the html holding the 'link' directive (using isolated scope), but I'm not. It's an as-is input and I have to figure a way to still do it.
There are countless examples on how to do similar bindings using the default scope of the directive, but I'm writing my controllers using John Papa's recommendations with controllerAs, but don't want to create another instance on the controller in the directive.
This is what I have reached so far:
(function () {
'use strict';
angular
.module('app')
.directive('link', link);
link.$inject = ['$compile'];
function link($compile) {
return {
restrict: 'E',
replace: true,
template: '<a class="someclass"></a>',
terminal: true,
priority: 1000,
link: function (scope, element, attributes) {
element.removeAttr('link'); // Remove the attribute to avoid indefinite loop.
element.attr('ng-click', 'linkClicked(\'' + attributes.text + '\')');
$compile(element)(scope);
},
};
}
})();
$scope.linkClicked = function(text){...} in the parent controller works.
element.attr('ng-click', 'abc.linkClicked(..)') in the directive (where the parent's controllerAs is abc) - also works.
The problem is I don't know which controller will use my directive and can't hard-code the 'abc' name in it.
What do you suggest I should be doing?
It's difficult to understand from your question all the constraints that you are facing, but if the only HTML you get is:
<link text="some text">
and you need to generate a call to some function, then the function must either be:
assumed by the directive, or
conveyed to the directive
#1 is problematic because the user of the directive now needs to understand its internals. Still, it's possible if you assume that a function name is linkClicked (or whatever you want to call it), and the user of your directive would have to take special care to alias the function he really needs (could be done with "controllerAs" as well):
<div ng-controller="FooCtrl as foo" ng-init="linkClicked = foo.actualFunctionOfFoo">
...
<link text="some text">
...
</div>
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
link: function(scope, element, attrs, ctrl){
var template = '<a class="someclass" ng-click="linkClicked(\'' +
attrs.text +
'\')">link</a>';
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
Demo
#2 is typically achieved via attributes, which isn't possible in your case. But you could also create a sort of "proxy" directive - let's call it onLinkClick - that could execute whatever expression you need:
<div ng-controller="FooCtrl as foo"
on-link-click="foo.actualFunctionOfFoo($data)">
...
<link text="some text">
...
</div>
The link directive now needs to require: "onLinkClick":
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
scope: true,
require: "?^onLinkClick",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
var template = '<a class="someclass" ng-click="localClick()">link</a>';
scope.localClick = function(){
ctrl.externalFn(attrs.text);
};
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
app.directive("onLinkClick", function($parse){
return {
restrict: "A",
controller: function($scope, $attrs){
var ctrl = this;
var expr = $parse($attrs.onLinkClick);
ctrl.externalFn = function(data){
expr($scope, {$data: data});
};
},
};
});
Demo
Notice that having a link directive would also execute on <link> inside <head>. So, make attempts to detect it and skip everything. For the demo purposes, I used a directive called blink to avoid this issue.
I am trying to create a directive that adds some html code but also adds additional attributes/directives.
Using the code below, an ng-class attribute is indeed added, but it seems angular does not recognize it as a directive anymore. It is there, but it has no effect.
How can I get it to work? Thanks.
The Angular code:
angular.module('myModule', [])
.directive('menuItem', function () {
return {
restrict: 'A',
template: '<div ng-if="!menuItem.isSimple" some-other-stuff>{{menuItem.name}}</div>'
+'<a ng-if="menuItem.isSimple" ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.attr('ng-class', '{active : menuItem.isActivated()}');
}
}
});
And the html:
<li menu-item="menuItem" ng-repeat="menuItem in getMenuItems()" />
EDIT:
The solution by #Abdul23 solves the problem, but another problem arises: when the template contains other directives (like ng-if) these are not executed. It seems the problem just moved.
Is it possible to also make the directives inside the template work?
Or perhaps insert the html using the compile function instead of the template parameter. Since I want a simple distinction based on some value menuItem.isSimple (and this value will not change), perhaps I can insert only the html specific to that case without using ng-if, but how?
You need to use $compile service to achieve this. See this answer.
For your case it should go like this.
angular.module('myModule', [])
.directive('menuItem', function ($compile) {
return {
restrict: 'A',
template: '<a ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.removeAttr('menu-item');
element.attr('ng-class', '{active : menuItem.isActivated()}');
var fn = $compile(element);
return function(scope){
fn(scope);
};
}
}
});
The timing of (pre/post)link functions in AngularJS are well defined in the documentation
Pre-linking function
Executed before the child elements are linked. Not safe to do DOM
transformation since the compiler linking function will fail to locate
the correct elements for linking.
Post-linking function
Executed after the child elements are linked. It is safe to do DOM
transformation in the post-linking function.
and this blog post clearly illustrates this expected order.
But this order does not seem to apply when using ng-transclude and nested directives.
Here is an example for a dropright element (See the Plunkr)
<!-- index.html -->
<dropright>
<col1-item name="a">
<col2-item>1</col2-item>
<col2-item>2</col2-item>
</col1-item>
<col1-item name="b">
...
</col1-item>
</dropright>
// dropright-template.html
<div id="col1-el" ng-transclude></div>
<div id="col2-el">
<!-- Only angularJS will put elements in there -->
</div>
// col1-item-template.html
<p ng-transclude></p>
// col2-item-template.html
<div ng-transclude></div>
The dropright looks like
The directives write a log in the console when their link and controller functions are called.
It usually displays:
But sometimes (after few refreshes), the order is not as expected:
The dropright post-link function is executed before the post-link function of its children.
It may be because, in my particular case, I am calling the dropright controller in the children's directives (See the Plunkr)
angular.module('someApp', [])
.directive('dropright', function() {
return {
restrict: 'E',
transclude: 'true',
controller: function($scope, $element, $attrs) {
console.info('controller - dropright');
$scope.col1Tab = [];
$scope.col2Tab = [];
this.addCol1Item = function(el) {
console.log('(col1Tab pushed)');
$scope.col1Tab.push(el);
};
this.addCol2Item = function(el) {
console.log('(col2Tab pushed)');
$scope.col2Tab.push(el);
};
},
link: {
post: function(scope, element, attrs) {
console.info('post-link - dropright');
// Here, I want to move some of the elements of #col1-el
// into #col2-el
}
},
templateUrl: 'dropright-tpl.html'
};
})
.directive('col1Item', function($interpolate) {
return {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('-- controller - col1Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('-- post-link - col1Item');
droprightCtrl.addCol1Item(element.children()[0]);
}
},
templateUrl: 'col1-tpl.html'
};
})
.directive('col2Item', function() {
var directiveDefinitionObject = {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('---- controller - col2Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('---- post-link - col2Item');
droprightCtrl.addCol2Item(element.children()[0]);
}
},
templateUrl: 'col2-tpl.html'
};
return directiveDefinitionObject;
});
Is there any clean way to execute the link function of a directive after all the link functions of its children while using transclusion?
This is my theory - its not the transclude aspect that is causing the sequence issue but rather the template being a templateUrl. The template needs to be resolved before the post link function get to act on it - hence we say post link function is safe to do DOM manipulation. While we are getting 304s for all the 3 templates - we do have to read them and it ultimately resolves the template promise.
I created a plunker with template instead of templateUrl to prove the corollary. I have hot refresh/plunker Stop/Run many times but I always get link - dropright at the end.
Plunker with template instead of templateUrl
I don't pretend to understand the compile.js code fully. However it does appear that in
compileTemplateUrl function $http.success() resolves the template and then on success the applyDirectivesToNode function is called passing in postLinkFn.
https://github.com/angular/angular.js/blob/master/src/ng/compile.js
This may be just weirdness with Plunker. I tried copying the files to my local IIS, and was not able to replicate the issue.
In my understanding, $compile should be able to support nested directive compilation/link, but we came across an issue that the compilation/link is incomplete - only the outermost directive got rendered into DOM, and the issue only reproduced when both below conditions are true:
The inner directive template is loaded via templateUrl (e.g. async manner)
The compilation is triggered outside Angular context.
I wrote a jsfiddler to demo it, part code listed below, for complete case http://jsfiddle.net/pattern/7KjWP/
myApp.directive('plTest', function($compile){
return {
restrict :'A',
scope: {},
replace: true,
template: '<div>plTest rendered </div>',
link: function (scope, element){
$('#button1').on('click', function(){
var ele;
ele = $compile('<div pl-shared />')(scope);
console.log('plTest compile got : '+ ele[0].outerHTML);
// scope.$apply();
element.append(ele);
});
}
};
});
myApp.directive('plShared', function($compile, $timeout){
return {
restrict: 'A',
scope: {},
replace: true,
link: function (scope, element){
// comment out below line to make render success
//$timeout(function(){});
var el = $compile('<div pl-item></div>')(scope);
console.log('plShared compile got:' + el[0].outerHTML);
element.append(el);
}
};
});
myApp.directive('plItem', function($timeout){
return {
restrict: 'A',
scope:{},
template:'<div>plItem rendered <div pl-avatar/></div>',
link: function(scope){
}
};
});
myApp.directive('plAvatar', function(){
return {
restrict: 'A',
scope: {}
, templateUrl: 'avatar.html'
// ,template: 'content of avatar.html <div pl-image></div>'
};
});
Interestingly, i can workaround the issue by either calling scope.$apply() somewhere after compile() call (line 27)
or adding $timeout(function(){}) call into the link func of one of inner directive (line 41). Is this a defect or by design?
$(foo).on(bar, handler) is a jQuery event, which means AngularJS does not know the specifics of it, and will not (can not) run an apply-digest cycle after it to process all the bindings.
scope.$apply was made for this, and as you rightly say, fixes it. The rule of thumb is: if you implement UI functionality in an AngularJS application using other libraries (specifically: outside of apply-digest cycles), you must call scope.$apply yourself.
HTH!
After element.append(el), try to compile again as you have just modified the DOM.
You could try something such as $compile(element)(scope); or $compile(element.contents())(scope);.
As said before me, I would also change the event handler as follows :
$('#button1').on('click', function(){
scope.$apply( function(){
//blablalba
});
});
Also, justa piece of advice in case you would want to minify your code, I would declare the compile dependency using the following syntax :
.directive('directiveName',['$service1',''$service2,...,'$compile', function($service1, $service2,...,$compile){
//blablabla
}]}
The idea is to replace the directive element with the dynamic template which refers to interpolated strings.
If I use element.html() in my directive then the strings are interpolated fine but this leaves the original custom directive html element.
If I use element.replaceWith() then strings are not interpolated. I guess it has related to scope but can't figure out what's wrong.
Plunker: http://plnkr.co/edit/HyBP9d?p=preview
UPDATE
Found the solution. Using element.replaceWith($compile(html)(scope)); works.
Updated plunker: http://plnkr.co/edit/HyBP9d?p=preview
Not sure what objective is regarding replaceWIth. The problem is likely that when you replace an elememnt, you replace all events and data bound to it. This would include the angular scope for the element.
For demo provided could do it like this:
app.directive('status', function($compile) {
var linker = function(scope, element, attrs) {
element.contents().wrap('<h'+attrs.value+'>')
};
return {
restrict: 'E',
replace: true,
template:'<div>{{value}}</div>',
transclude: true,
link: linker,
scope: {
value: '='
}
};
});
DEMO:http://plnkr.co/edit/QDxIwE?p=preview