Add ng-click dynamically in directive link function
I'm trying to create a directive that would allow an element to be defined as clickable or not, and would be defined like this:
<page is-clickable="true">
transcluded elements...
</page>
I want the resulting HTML to be:
<page is-clickable="true" ng-click="onHandleClick()">
transcluded elements...
</page>
My directive implementation looks like this:
app.directive('page', function() {
return {
restrict: 'E',
template: '<div ng-transclude></div>',
transclude: true,
link: function(scope, element, attrs) {
var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;
if (isClickable) {
attrs.$set('ngClick', 'onHandleClick()');
}
scope.onHandleClick = function() {
console.log('onHandleClick');
};
}
};
});
I can see that after adding the new attribute, Angular does not know about the ng-click
, so it is not firing. I tried adding a $compile
after the attribute is set, but it causes an infinite link/compile loop.
I know I can just check inside the onHandleClick()
function if the isClickable
value is true
, but I'm curious how one would go about doing this with dynamically adding an ng-click
event because I may need to do with this with multiple other ng-*
directives and I don't want to add unnecessary overhead. Any ideas?
Solution 1:
Better Solution (New):
After reading through the Angular docs I came across this:
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
So my new directive looks like this: (I believe this is the appropriate "Angular" way to go about this type of thing)
app.directive('page', function() {
return {
restrict: 'E',
replace: true,
template: function(tElement, tAttrs) {
var isClickable = angular.isDefined(tAttrs.isClickable) && eval(tAttrs.isClickable) === true ? true : false;
var clickAttr = isClickable ? 'ng-click="onHandleClick()"' : '';
return '<div ' + clickAttr + ' ng-transclude></div>';
},
transclude: true,
link: function(scope, element, attrs) {
scope.onHandleClick = function() {
console.log('onHandleClick');
};
}
};
});
Notice the new template function. Now I am manipulating the template inside that function before it is compiled.
Alternative solution (Old):
Added replace: true
to get rid of the infinite loop issue when recompiling the directive. And then in the link function I just recompile the element after adding the new attribute. One thing to note though, because I had an ng-transclude
directive on my element, I needed to remove that so it doesn't try to transclude anything on the second compile, because there is nothing to transclude.
This is what my directive looks like now:
app.directive('page', function() {
return {
restrict: 'E',
replace: true,
template: '<div ng-transclude></div>',
transclude: true,
link: function(scope, element, attrs) {
var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;
if (isClickable) {
attrs.$set('ngClick', 'onHandleClick()');
element.removeAttr('ng-transclude');
$compile(element)(scope);
}
scope.onHandleClick = function() {
console.log('onHandleClick');
};
}
};
});
I don't think that recompiling the template a second time is ideal though, so I feel that there is still a way to do this before the template is compiled the first time.
Solution 2:
You could always just modify your ng-click to look like this:
ng-click="isClickable && someFunction()"
No custom directive required :)
Here is a JSFiddle demoing it: http://jsfiddle.net/robianmcd/5D4VR/
Solution 3:
Updated answer
"The Angular Way" would be no manual DOM manipulation at all. So, we need to get rid off adding and removing attributes.
DEMO
Change the template to:
template: '<div ng-click="onHandleClick()" ng-transclude></div>'
And in the directive check for the isClickable
attribute to decide what to do when clicked:
link: function(scope, element, attrs) {
var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;
scope.onHandleClick = function() {
if (!isClickable) return;
console.log('onHandleClick');
};
}
You could also put the isClickable attribute in the directive scope so that it can dynamically change its behavior.
Old answer (wrong)
link
is run after the template is compiled. Use controller
for alterations on the template before compiling:
app.directive('page', function() {
return {
restrict: 'E',
template: '<div ng-transclude></div>',
transclude: true,
controller: function(scope, element, attrs) {
// your code
}
};
});
Solution 4:
HTML
<div page is-clickable="true">hhhh</div>
JS
app.directive('page', function($compile) {
return {
priority:1001, // compiles first
terminal:true, // prevent lower priority directives to compile after it
template: '<div ng-transclude></div>',
transclude: true,
compile: function(el,attr,transclude) {
el.removeAttr('page'); // necessary to avoid infinite compile loop
var contents = el.contents().remove();
var compiledContents;
return function(scope){
var isClickable = angular.isDefined(attr.isClickable)?scope.$eval(attr.isClickable):false;
if(isClickable){
el.attr('ng-click','onHandleClick()');
var fn = $compile(el);
fn(scope);
scope.onHandleClick = function() {
console.log('onHandleClick');
};
}
if(!compiledContents) {
compiledContents = $compile(contents, transclude);
}
compiledContents(scope, function(clone, scope) {
el.append(clone);
});
};
},
link:function(scope){
}
};
});
credit to Erstad.Stephen and Ilan Frumer
BTW with restrict: 'E' the browser crashed :(