Angular Directive Controller best practice - javascript

Which one is the best way to define directive controller. I referred some site they explained Option1.
Option 1:
angular.module('app')directive('AppHeader', AppHeader);
function AppHeader() {
var headerDirective = {
restrict: 'E',
templateUrl: 'header.html',
link: linkFunc,
controllerAs: 'vm',
controller: NavCtrl
};
return headerDirective;
function linkFunc(scope, element, attrs) {
/* */
}
}
NavCtrl.$inject = ['$scope', 'Service'];
function NavCtrl($scope, Service) {
var vm = this;
/* Controller actions */
}
In the above code Controller separated from the directive function block.
Option 2:
angular.module('app').directive('AppHeader', AppHeader);
function AppHeader()
{
var headerDirective = {
restrict: 'E',
templateUrl: 'header.html',
link: linkFunc,
controllerAs: 'vm',
controller: NavCtrl
};
return headerDirective;
function linkFunc(scope, element, attrs) {
/* */
}
NavCtrl.$inject = ['$scope', 'Service'];
function NavCtrl($scope, Service) {
var vm = this;
/* Controller actions */
}
}
In the above code Controller added in directive function block.
So which one is best practice to create and define the Directive controller and Why. Thanks in advance.

With regards to Option 2, you have scoped the function to level where you are actually using.
In Option 1, you have scoped the function a level higher, There is no need for it to be a level above as it will not be reused.
I feel Option 2 makes the function correctly scoped.
Further you might want to enclose entire Option 2 code in IIFE (immediately invoked function expression)
(function(){
//Your code here
})();

You should go with option 1 because.
The directive's controller should be outside the directive's closure. This way you eliminate possible issues where the injection gets created as unreachable code after a return.
You should read John Papa's style guide, he explains all kind of things about code styling in angular.

Related

Angular directive: whats the difference between scope in controller vs scope in link function?

Im learning about Angular directives and I can't wrap my head around the scope topic. Suppose I have this custom directive which is named parentDirective. It has a controller property and a link property, as follows:
angular.module("app").directive("parentDirective", function () {
return {
restrict: "E",
templateUrl: "dirs/parent.html",
scope:{
character: "="
},
controller: function ($scope) {
$scope.getData = function (data) {
console.log(data);
}
},
link: function (scope,elem, attrs) {
elem.bind("click", function (e) {
//get object here?
});
scope.getData = function (data) {
console.log(data);
}
}
}
});
Its template is defined as follows:
<p ng-click="getData(character)">
{{character.name}}
</p>
I can get the character object in the controller function through the $scope variable and I have access to the same data in the link function through scope. Whats the difference between the two methods in this regard? Second question, Is it possible to bind a click to the directive and get the object like this:
elem.bind("click", function (e) {
//get object here?
});
Scope is specific to current directive instance and is the same object in both functions.
For defining methods on the scope, there's no difference if they are defined in controller or link function, unless there is race condition that requires the method to be defined as early as possible. For this reason it makes sense to define scope methods in controller.
Event handler doesn't differ from any other function, it is
elem.on("click", function (e) {
scope.$apply(function () {
scope.character...
});
});
scope.$apply(...) wrapper doesn't hurt anyway, but the necessity of it depends on what happens with scope.character.
The directive can have only controller and no link. Current Angular versions (1.5+) suggest the style where bindToController + controllerAs are used instead of scope bindings as common ground for directives and components.
Then the directive may look like
restrict: "E",
template: '<p>{{$ctrl.character.name}}</p>',
controllerAs: '$ctrl',
bindToController: { character: "=" },
controller: function ($element, $scope) {
var self = this;
self.getData = function (data) { ... };
$element.on("click", function (e) {
scope.$apply(function () {
self.character...
});
});
}
link function may appear as $postLink controller hook, but here it is not needed.

How to setup $emit in directive in my case

I am trying to use $emit in my code for a directive.
I have
(function () {
angular
.module('myApp')
.directive('testDirective', testDirective);
function testDirective() {
var directive = {
link: link,
restrict: 'A',
controller: testCtrl,
controllerAs: 'vm'
};
return directive;
function link(scope, element, attrs) {
}
}
function testCtrl() {
var vm = this;
// do something
vm.$emit('someEvent', {'id': '123'})
}
})();
However, I am getting 'TypeError: vm.$emit is not a function'. I am not sure how to fix this. Can anyone help? Thanks a lot!
controllerAs just means that scoped variables are attached directly to the controller -- it doesn't mean that the controller is an instance of an angular scope itself. In this case, you'll need to inject the scope into the controller and then emit the event from on the scope:
function testCtrl($scope) {
// do something
$scope.$emit('someEvent', {'id': '123'})
}
Also beware, the normal injection rules apply -- If you're going to minify this, you'll probably need something like:
testCtrl['$inject'] = ['$scope'];

AngularJS: Accessing an element directive's attributes from within a controller

Problem: The attribute I pass to my directive's controller is not evaluated. For example, I get {{ attribute.value }} instead of 5.
Desired Outcome: My directive's controller has access to a primary key contained in an object from a parent controller. I need it to make API calls like MyResource.save({id: attribute});.
Code Snippets:
Calling directive from HTML
<div ng-controller="BoatDetailController as boatCtrl">
<div class="row">
<booking-widget boat-id="{{ boatCtrl.boat.id }}"></booking-widget>
</div>
Directive
(function () {
'use strict';
angular.
module('trips').
directive('bookingWidget', bookingWidget);
bookingWidget.$inject = [];
function bookingWidget() {
return {
restrict: 'E',
scope: {
boatId: '#'
},
templateUrl: "/static/app/trips/trips.bookingwidget.template.html",
controller: 'BookingWidgetController as bookingCtrl'
}
}
})();
Controller
(function () {
'use strict';
angular.
module('trips').
controller('BookingWidgetController', BookingWidgetController);
BookingWidgetController.$inject = ['Trip', 'Booking', 'Messages', '$scope', '$attrs'];
function BookingWidgetController(Trip, Booking, Messages, $scope, $attrs) {
var vm = this;
vm.boatId = $attrs.boatId;
...
activate();
//////////////////////////////
function activate() {
console.log(vm.boatId);
//
}
Console Results:
With $scope.boatId: (logs a blank line)
With $attrs.boatId: {{ boatCtrl.boat.id }} (a string)
Recap: The boat-id attribute of my directive is not resolving. Can you help me figure out how to fix it?
You can actually create a custom directive like this:
function bookingWidget() {
return {
restrict: 'E',
scope: {
boatId: '#'
},
templateUrl: "/static/app/trips/trips.bookingwidget.template.html",
controller: 'BookingWidgetController as bookingCtrl',
link : function(scope, element, attrs, controller){
console.log(attrs.boatId);
scope.boatId = attrs.boatId;
}
}
}
The link function actually allows you to have an access to the element, the scope of the directive, the attributes associated to the directive and the controller of the directive. The function is called after everything associated to the directive has been performed. In other words, this is the last stage.
The same scope would be shareable between the link function and controller.
Now, to make the API call, you may actually add a function in your controller that accepts the boatID, makes a call to the API and accepts the response onto the controller object. After that, add a watcher within the link function that watches over "vm.boatId", within which you may call that function which makes the API call. So, even if the controller has initialized before the link function, you would still be able to perform what you wish to. So, it would be a "link-based activation".
You may give this solution a try. Hope it helps.
You can pass a function and call it. Need to use & then.
https://thinkster.io/egghead/isolate-scope-am

How to pass the angular's directive's link and controller to it

Say that I have a straight forward directive:
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
function someEmbeddedFunction()
{
// More embedding
// This code is really getting nasty
}
}
}
}
]);
The code above has so much indentation and crowded that at least to me, it is very hard to read and unenjoyable to work with.
Instead, I want to move the link and someEmbeddedFunction out and just call them. So something along the lines of this:
function link(scope, element, attrs, MyService1, MyService2)
{
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
}
function someEmbeddedFunction()
{
// This is much nicer and less tabbing involved
}
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: link // This is line that I need to get it to work
}
]);
The problem is that MyService1 and MyService2 are not passed to the link function (i.e. if I only had a link function with scope, element, attrs then the code above would work just fine). How can I pass those variables as well?
I tried to call the function as link: link(scope, element, attrs, MyService1, MyService2) but then it says scope, element, attrs are undefined.
Note I realize that the someEmbeddedFunction can right now be moved out without a problem. This was just for demonstrating purposes.
Edit
The only way I can think to get this to work is call the link function from the directive this way:
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
As you observed, the only way to call your non-standard link function is to do so manually within a "standard" link function.
i.e.
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
This is because the link function doesn't get injected like other functions in Angular. Instead, it always gets the same series of arguments (regardless of what you call the function parameters):
The scope
The element (as an angular.element() instance)
The attrs object
An array or single controller instance that you required
A transclude function (if your directive uses transclusion)
Nothing else.
I use this scheme to keep it simple & readable:
var awesomeDir = function (MyService, MyAnotherService) {
var someEmbeddedFunction = function () {
MyService.doStuff();
};
var link = function ($scope, $elem, $attrs) {
someEmbeddedFunction();
};
return {
template: '<div>...</div>',
replace: true,
restrict: 'E',
link: link
};
};
awesomeDir.$inject = ['MyService', 'MyAnotherService'];
app.directive('awesomeDir', awesomeDir);

Modify template in directive (dynamically adding another directive)

Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
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.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>

Categories