I am going through this doc, the confusion I have is what is 'this' in link.apply(this, attrs). Can someone help?
$provide.decorator('fooDirective', function($delegate) {
var directive = $delegate[0];
directive.scope.fn = "&";
var link = directive.link;
directive.compile = function() {
return function(scope, element, attrs) {
link.apply(this, arguments);
element.bind('click', function() {
scope.$apply(function() {
scope.fn();
});
});
};
};
return $delegate;
});
});
when I try to debug it using console debugger, 'this' is undefined while link function is running.
There's no special this context in Angular decorator, so it may be window in loose mode or undefined in strict mode.
In nested functions this may refer to non-lexical context, which may take place in Angular directives:
directive.compile = function() {
// `this` is directive DDO in compile function
return function(scope, element, attrs) {
// `this` is `undefined` in link function
...
};
};
In compile function this is directive DDO. In controller function this is controller instance. There's no lexical this in link function.
link.apply(this, arguments) is an attempt to play safe but here it is just misleading. It may be link.apply(null, arguments) instead.
You need to create a compile function which will return your new link function.
Inside there, you call apply (passing as the first parameter the function itself) in the old link function to get the old functionality.
With that set, you just need to add the extra behavior (in this case you bind the click event into the element which will call the new function upon click).
Related
To start off, I know there are loads of similar questions but none that I found which supports execution of arbitrary methods with event listeners.
I have this directive which executes functions passed when the window resizes.
app.directive('onResize', function() {
var directive = {
'link': function(scope, element, attrs) {
var onResizeHandler = scope.$eval(attrs.onResize);
angular.element(window).on('resize', onResizeHandler);
angular.element(window).on('$destory', function() {element.off();});
}
};
return directive;
});
I can trigger the above directive with
<div data-on-resize="stickyHeader">...</div>
Which runs my method, inside my controller.
app.controller('myController', [$scope, function($scope) {
$scope.stickyHeader = function() {
console.log('event triggered')
};
}]);
All the above code works fine, but I need to pass some arguments to stickyHeader as in data-on-resize="stickyHeader(arg1, arg2)" When I try that, I get Cannot read property 'call' of undefined at ng (angular.js:3795) in the console. Not sure what I can do to make my directive support arbitrary methods with arguments.
The directive needs to evaluate the AngularJS expression defined by the on-resize attribute every time the events occurs:
app.directive('onResize', function($window) {
var directive = {
link: function(scope, elem, attrs) {
angular.element($window).on('resize', onResizeHandler);
scope.$on('$destroy', function() {
angular.element($window).off('resize', onResizeHandler);
});
function onResizeHandler(event) {
scope.$eval(attrs.onResize, {$event: event});
scope.$apply();
}
}
};
return directive;
});
Also since the resize event comes from outside the AngularJS framework, the event needs to be brought into the AngularJS execution context with $apply().
Further, to avoid memory leaks, the event handler needs to be unbound when the scope is destroyed.
Usage:
<div data-on-resize="stickyHeader($event)">...</div>
For more information, see AngularJS Developer Guide - $event.
I have a need to create a composite directive that incorporates separate fully functional directives. One of my component directives adds an element to the dom and that element binds to a value in the component directive's controller. When the composite directive adds the component directive in the compile function, it seems to work but the piece that has the 2 way binding in the component directive does not appear to get compiled and just renders the {{ctrl.value}} string on the screen. I realize this is a bit convoluted so I have included a plunk to help clarify the issue.
app.directive('compositeDirective', function($compile){
return {
compile: compileFunction
}
function compileFunction(element, attrs){
attrs.$set("component-directive", "");
element.removeAttr("composite-directive");
element.after("<div>Component value when added in composite directive: {{compCtrl.myValue}}</div>");
return { post: function(scope, element){
$compile(element)(scope);
}};
}
});
app.directive('componentDirective', function(){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
element.after("<div>Component value: {{compCtrl.myValue}}</div>");
}
});
app.controller('componentController', function(){
var vm = this;
vm.myValue = "Hello";
});
http://plnkr.co/edit/alO83j9Efz62VTKDOVgc
I don't think any compilation will happen as a result of changes in the link function, unless you call $compile manually, i.e.,
app.directive('componentDirective', function($compile){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
var elm = $compile("<div>Component value: {{compCtrl.myValue}}</div>")(scope);
element.append(elm);
}
});
Updated plunk: http://plnkr.co/edit/pIixQujs1y6mPMKT4zxK
You can also use a compile function instead of link: http://plnkr.co/edit/fjZMd4FIQ97oHSvetOgU
Also, make sure to use .append() instead of .after().
I have a simple Angular directive that adds a CSS class to an element when being clicked:
.directive("addClass", function () {
return {
scope: {
name: "=addClass"
},
link: function (scope, element, attributes) {
element.on("click", function () {
element.addClass(scope.name);
console.log("Element activated");
});
element.on("mouseleave", function () {
element.removeClass(scope.name);
console.log("Element deactivated");
});
}
}
});
I'm using it on my links:
...
But when I click my link my event handlers execute, although scope.name is undefined. I could read attribute value using attributes, but that beats the purpose of assigning attribute values to scope properties as described in Angular Directives API.
What am I doing wrong?
Replace =addClass with #addClass, or, in case you don't need an isolate scope, just read the class name right out of the attribute object:
element.on("click", function() {
element.addClass(attributes.addClass);
...
});
The reason = doesn't work in your case is that it expects a property so a two-way binding can be established and you're providing a static string (I'm assuming you are since class-name isn't a valid property name).
try ..., note the single-quoted 'class-name'
Perhaps I have a fundamental misunderstanding of how directive controllers work, from what I understand they are used as a sort of API to be exposed to other directives & controllers. I am trying to get the controller and link function to communicate internally.
For example I would like to be able to set a variable via the controller function and then use it in the link function:
var app = angular.module('test-app', []);
app.directive('coolDirective', function () {
return {
controller: function () {
this.sayHi = function($scope, $element, $attrs) {
$scope.myVar = "yo"
}
},
link: function(scope, el, attrs) {
console.log(scope.myVar);
}
}
});
How can I access myVar or sayHi within the link function? Or have I just missed the point completely?
Both controller's $scope (defined in the controller, not in the sayHi function) and link scope are the same. Setting something in the controller will be usable from the link or viceversa.
The problem you have is that sayHi is a function that is never fired so myVar is never set.
Since sayHi is not in the scope, you need a reference to the controller and to do it, you can add a fourth parameter like this:
link: function(scope, element, attr, ctrl) {}
Then you could do a ctrl.sayHi() (But again, those params of sayHi belongs to the controller function.)
If you ever need to require another controller and still wanting to use its own directive, then you will need to require it too. So if this coolDirective needs to access to the controller of notCoolAtAll you could do:
require: ['coolDirective', 'notCoolAtAll']
That will do the trick. The link function will receive then an array of controllers as the fourth param and in this case the first element will be coolDirective ctrl and the second one the notCoolAtAll one.
Here is a little example: http://plnkr.co/edit/JXahWE43H3biouygmnOX?p=preview
Rewriting your code above, it would look something like this:
var app = angular.module('test-app', []);
app.directive('coolDirective', function() {
return {
controller: function($scope) {
// bind myVar property to scope
$scope.myVar = 'yo';
// bind sayHi method to scope
$scope.sayHi = sayHi;
// abstracting out the sayHi function
function sayHi() {
console.log($scope.myVar);
}
},
link: function(scope, el, attrs) {
// execute the sayHi function from link
scope.sayHi(); // "yo" in console
}
};
});
Good Luck.
Plunker Link
I have a element which I would like to bind html to it.
<div ng-bind-html="details" upper></div>
That works. Now, along with it I also have a directive which is bound to the bound html:
$scope.details = 'Success! <a href="#/details/12" upper>details</a>'
But the directive upper with the div and anchor do not evaluate. How do I make it work?
I was also facing this problem and after hours searching the internet I read #Chandermani's comment, which proved to be the solution.
You need to call a 'compile' directive with this pattern:
HTML:
<div compile="details"></div>
JS:
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}])
You can see a working fiddle of it here
Thanks for the great answer vkammerer. One optimization I would recommend is un-watching after the compilation runs once. The $eval within the watch expression could have performance implications.
angular.module('vkApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
}
);
};
}]);
Here's a forked and updated fiddle.
Add this directive angular-bind-html-compile
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
// Incase value is a TrustedValueHolderType, sometimes it
// needs to be explicitly called into a string in order to
// get the HTML string.
element.html(value && value.toString());
// If scope is provided use it, otherwise use parent scope
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
Use it like this :
<div bind-html-compile="data.content"></div>
Really easy :)
Unfortunately I don't have enough reputation to comment.
I could not get this to work for ages. I modified my ng-bind-html code to use this custom directive, but I failed to remove the $scope.html = $sce.trustAsHtml($scope.html) that was required for ng-bind-html to work. As soon as I removed this, the compile function started to work.
For anyone dealing with content that has already been run through $sce.trustAsHtml here is what I had to do differently
function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(function(scope) {
return $sce.parseAsHtml(attrs.compile)(scope);
},
function(value) {
// when the parsed expression changes assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current scope.
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
});
}
This is only the link portion of the directive as I'm using a different layout. You will need to inject the $sce service as well as $compile.
Best solution what I've found! I copied it and it work's exactly as I needed. Thanks, thanks, thanks ...
in directive link function I have
app.directive('element',function($compile){
.
.
var addXml = function(){
var el = $compile('<xml-definitions definitions="definitions" />')($scope);
$scope.renderingElement = el.html();
}
.
.
and in directive template:
<span compile="renderingElement"></span>