I'm trying to create a custom directive which needs to use a separate controller because it needs to have functions which can be called by child directives.
Here is my code so far:
angular.module('myDirectives').controller('SlideInMenuController', function ($scope, $element, $attrs) {
$scope.isOpen = false;
// Toggle Function
this.toggle = function(){
$scope.$apply(function(){
$scope.isOpen = !$scope.isOpen;
});
};
// Watch $scope.isOpen and open the menu
$scope.$watch('isOpen', function() {
if($scope.isOpen == true){
$element.attr('is-open', true);
}
else{
$element.attr('is-open', false);
}
return false;
});
}
angular.module('myDirectives').directive('slideInMenu', function ($swipe) {
return {
restrict: 'EA',
scope: {},
controller:'SlideInMenuController'
};
});
angular.module('myDirectives').directive('slideInMenuToggle', function ($swipe) {
return {
restrict: 'EA',
require: '^slideInMenu',
link: function ($scope, $element, $attrs, SlideInMenuCtrl) {
$element.bind('click', function(){
SlideInMenuCtrl.toggle();
});
}
};
});
(Note: I'm using ng-annotate so I don't have to write all my dependencies twice)
I need to inject the $swipe service into the directive controller but a normal controller would't have $scope, $element, $attrs as the first three parameters. This has made me wonder if I should be putting those into the link function instead and doing DOM stuff there, but if I do that what goes in the controller and what goes in to the link function.
I've read numerous blogs and SO answers that say what order compile/link/controller are run in but still can't find a clear answer as to whatin my above example should go where.
Any help would really be appreciated.
There are two kind of functions for AngularJS. Neither of which is intended to be called directly.
1) Injectables: functions that receive parameters, whose names must (with a few exceptions) be registered with dependency injection subsystem. It's the reason for ng-annotate to exist. You can also use array notation for these.
angular.module('stackOverflow').service('answer', ['myService', function(myService) {
...
}]);
Some examples are the ones you pass to angular.module() functions, like service(), factory(), directive(), controller().
2) Plain functions. These have no special handling, it's vanilla JavaScript. They are passed to link and compile slots in directive definition objects.
You can omit rightmost parameters if you have no use for them, but not others. As the order of parameters is fixed, you cannot reorder them. But you can call them whatever you want.
That's it about functions.
About conventions using $: beware! AngularJS builtin services are prefixed with $, so you should name parameters this way for injectable functions. For all other cases, don't prefix with $: your own functions and positional parameters like you see in link() and compile(). Prefix with $ in those functions is misleading and bad guidance.
To better distinguish parameters for compile() and link, you can prefix with t for template and i for instance. Nowadays I prefer to use those unprefixed. It's better for moving them around.
compile: function (tElement, tAttrs) {
return function link(scope, iElement, iAttrs, ctrls) {
};
}
Related
i am learning angularjs from Pro angularjs by adam freeman.
This is an example he defined an directive in angularjs
myApp.directive("highlight", function () {
return function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
element.css("color", "red");
}
};
});
the first function the writer calls is factory function and it returns a worker function. I' m not able to understand why he returns another function.
When i write the code below, the code returns an error.
I don't know what i'm doing wrong.
myApp.directive("highlight", function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
return element.css("color","red")
}
});
If you take a look at the Angular docs about directives, you'll see that there are two ways you can define a directive. One is to use the directive definition object and the other is to just return a postLink function.
The example in your book shows the latter form. The reason your function doesn't work is because Angular calls your function and expects it to return a directive definition object or a postLink function. Instead the function you defined returns nothing or the result of element.css("color", "red") depending on your if statement.
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'.
In my app i am trying to call $('#accountTable').dataTable(); this function in my controller. But I think it doesnt work like that in angular.js. Tried to call this function in my Directive but i did not work.
My Directive:
'use strict'
app.directive('dataTableDirective', function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
$('#accountTable').dataTable();
}
}
});
Angular uses JQuery under the hood if you have JQuery referenced. If you don't then it falls back on a slimmer version of JQuery called JQuery Lite. The elem argument to the link function is already a JQuery wrapped object representing the element your directive is attached to. Just call the plugin from there and it should work fine. It is best to avoid the classic JQuery selectors to navigate the DOM and instead lean on Angular to provide the elements you need.
Make sure you have JQuery referenced before Angular in your script references.
app.directive('dataTableDirective', function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
elem.dataTable();
}
};
});
Angular needs to know about changes when they happen. If you assign any events and need to update scope variables, you'll need to make sure that Angular knows about those changes by wrapping them in scope.$apply. For example:
app.directive('dataTableDirective', function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
elem.on('order.dt', function (e) {
scope.something = 'someValue';
}).dataTable();
}
};
});
The above code will set the something property on scope, but because the event was triggered outside of an Angular digest cycle, any UI bound to the something variable may not appear to update. Angular needs to be told of the change. You can make sure the change happens during a digest cycle like this:
app.directive('dataTableDirective', function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
elem.on('order.dt', function (e) {
scope.$apply(function () {
scope.something = 'someValue';
});
}).dataTable();
}
};
});
Then in your markup:
<table data-data-table-directive>
<!-- table contents -->
</table>
#supr pointed this out in the comments. Note that the attribute is data-data-table-directive not data-table-directive. There is an HTML convention that you can begin arbitrary attributes with data- and Angular respects that and omits it. For example, you can put ng-click on an element or you can put data-ng-click on an element and they would both work the same. It also supports x-ng-click as another convention.
This is super relevant to you because it just so happens that your directive name begins with the word "data", so you'll need to double up on the data- in the beginning. Hopefully that makes sense.
The following controller does not seem to use $element, $attrs, $transclude. The controller code below runs fine if these params are commented out.
myApp.directive("menu", function () {
return {
restrict: "E",
scope: {},
transclude: true,
replace: true,
template: "<div class='menu' data-ng-transclude></div>",
controller: function ($scope ,$element, $attrs, $transclude) {
$scope.submenus = [];
console.log('[$element]->', $element);
console.log('[$attrs]->', $attrs);
console.log('[$transclude]->', $transclude);
this.addSubmenu = function (submenu) {
console.log('[addToggleMenu]->');
$scope.submenus.push(submenu);
}
this.closeAllOtherPanes = function (displayedPane) {
angular.forEach($scope.submenus, function (submenu) {
if (submenu != displayedPane) {
console.log('[displayedPane]->', displayedPane);
submenu.removeDisplayClass();
}
})
}
}
}
});
Here is my working fiddle.
Since posting, I have learned that in JavaScript, a function can be called with any number of arguments no matter how many of them are listed. In some languages, a programmer may write two functions with same name but different parameter list, and the interpreter/compiler would choose the right one. That is called function polymorphism. Having used function polymorphism most of my career I expected to be told "Hey , you're not using this param". Also I did not understand that well that the controller parameters are dependencies while the link function parameters are order based. I still struggle in understanding whether $scope, $element or commonly used parameters in the directives internal controller are required and which are optional. Apparently the $ is only required in the controller and not in link because of the DI injection of angular services..whew lot to digest.
A special thanks to Esteban for explaining special pseudo-array inside each function
called arguments. This explains the link function which is half the equation. So I have rewritten the question in hopes that it may get answered. This excellent explanation,
straightened out most of my confusion.
Why are you wanting to remove the $attrs argument? The directive's arguments are in a specific order and removing any one of them will cause subsequent arguments to not be what you think they are.
Say you changed your link function to something like this:
function ($scope, $iElement, menuController) {
menuController.addSubmenu($scope); // this will throw an exception
// because menuController is actually
// $attrs
The reason for this is due to the fact that the function will be called with the same arguments passed in regardless of whether they were defined in your directive's link function.
function argTest(one, two){
console.log('argTest.arguments', arguments);
// even though I don't have three defined, it is still passed in:
console.log('three is passed in', arguments[2]);
}
argTest(1,2,3);
JSFiddle for above: http://jsfiddle.net/TwoToneBytes/NM8DK/
I'm facing a situation in Angular where I sense I'm doing something wrong but I can't find the Angular way to solve it.
I'm developing a mobile app. I would like to create a directive let's call it cc-tap-highlight that would be used in conjunction with ng-click. Meaning that I could use it like this:
<a ng-click="doSomething()" cc-tap-highlight>Click me</a>
What this would do is to add a class to the clicked element and remove it after some seconds.
Now one could say, let's just manually bind to the elements click event in the directive. That would work for a desktop app. However, on mobile devices Angular does a lot of magic for us to have fast tap/clicks:
https://github.com/angular/angular.js/blob/master/src/ngMobile/directive/ngClick.js
For sure, I don't want to reimplement all of it's magic!
So, currently, instead of having my cc-tap-highlight directive I use this rather hackish approach:
In the view
<a ng-click="doSomething($event)" cc-tap-highlight>Click me</a>
In the controller:
$scope.doSomething = function($event){
//do your things with $event.currentTarget
}
There are two major problems with this approach:
the controller should not manipulate the DOM
We need to repeat the patter over and over through our entire code base violating DRY
However, I can't for the life of me, figure out how to write a directive that hooks into the ng-click handler and does it's things.
You can try to make your directive generate a ng-click directive with wrapper function.
Here's a quick example. It's by far not thoroughly tested but I think the principle is sound. What you want is your custom code to run before/after the click event regardless of how that's triggered(tap,click, whatever).
This does have the drawback that it creates a new scope so interaction with other directives that may need isolate scope was not tested.
DIRECTIVE
app.directive('myClick', ['$parse','$compile', function($parse, $compile) {
return {
restrict: 'A',
compile : function(tElement, tAttrs, transclude) {
//you can call the wrapper function whatever you want.
//_myClick might be more appropriate to indicate it's not really public
tElement.attr('ng-click', 'myClick($event)');
tElement.removeAttr('my-click');
var fn = $parse(tAttrs['myClick']);
return {
pre : function(scope, iElement, iAttrs, controller) {
console.log(scope, controller);
scope.myClick = function(event) {
console.log('myClick.before');
fn(scope, {$event:event});
console.log('myClick.after');
};
$compile(iElement)(scope);
},
post : function postLink(scope, iElement, iAttrs, controller) {
}
};
},
scope : true
};
}]);
CONTROLLER
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.test = function($event) {
console.log('test', $event);
};
//this is to show that even if you have a function with the same name,
//the wrapper function is still the one bound thanks to the new scope
$scope.myClick = function() {
console.log('dummy my click');
};
});
HTML
<button ng-click="test($event)">NG-CLICK</button>
<button my-click="test($event)">MY-CLICK</button>
<button ng-click="myClick($event)">MY-CLICK-DUPLICATE-FN</button>