I'm refactoring some of my Angular JS application, and I'm going to learn more about directives.
I've read many times that bind a controller to a directive is a good practice, if we want to share logic and get code clean.
Bind a controller to a directive to share common tasks between many directives is pretty simple and I understand the interest of this pattern. But my question is why do we need to use a controller ?
(Example code come from this site)
Pattern 1 : Use controller to share logic between directives
Bind a controller to directive :
app.directive("superhero", function () {
return {
restrict: "E",
controller: function ($scope) {
$scope.abilities = [];
// [...] additional methods
this.addFlight = function() {
$scope.abilities.push("flight");
};
},
link: function (scope, element) {
element.addClass("button");
element.bind("mouseenter", function () {
console.log(scope.abilities);
});
}
};
});
Share logic with another directives :
app.directive("flight", function() {
return {
require: "superhero",
link: function (scope, element, attrs, superheroCtrl) {
superheroCtrl.addFlight();
}
};
});
When I want to share logic between my controller I create a Factory that I inject into my controller. So why do not use the same pattern ?
Pattern 2 : Use factory to share logic between directives
Declare the new factory :
app.factory("myAwesomeFactory", function () {
return {
addFlight: function () { /* ... */ }
};
});
Use the factory into directive :
app.directive("flight", function(myAwesomeFactory) {
return {
require: "superhero",
link: function (scope, element, attrs) {
myAwesomeFactory.addFlight();
}
};
});
I can't understand why the first method is better than the second.
Bonus question : Why do we use this keyword in controllers which are binded to directives ?
Thanks a lot. I've found lots of tutorials about how to bind a controller to directive. But no one explains why we need to do this way.
The biggest reason I've run across is that, since services are singletons, you can run into serious problems by having multiple directives relying on logic from the same service. This is why anything that has to do with the view is done through the controller. While you can sometimes get away with using the service within the directive, it's better to avoid the practice altogether if possible.
Related
I am quite familiar with CanJS, and kind of like the idea that you can instantiate a custom web widget on an HTML element, and now that we have an object, we can send messages to it (invoke a method on it):
lightbox.popUp();
or
reviewStars.setStars(3.5);
How could that be done in AngularJS? After you make a directive and set it on an HTML element or use the directive as an HTML element, how do you do something like above, as in OOP, or how Smalltalk would do it -- sending messages to a particular object?
I could think of a way, such as:
<review-stars api="ctrl.reviewStarAPI"></review-stars>
and then for the reviewStar directive, do this:
scope: { api: "=" }
link: function(scope, elem, attrs) {
// first define some functions
scope.setStars = function(n) { ... };
// and then set it to the api object:
scope.api.setStars = scope.setStars();
}
and then in the controller, do
vm.reviewStarAPI.setStars(3.5);
but this is a bit messy, and somewhat ad hoc. And it always need to have a controller... although, I suppose we can use 1 controller and as the main program and instantiate a bunch of objects and then invoke methods on them this way.
What is/are ways to accomplish this besides the method above?
A modular approach to this would be to create a directive called reviewStars. The directive should have a parameter that indicates the star rating.
For example:
<review-stars rating="3.5">
You would create using something like the following:
angular.module('myAngularModule', [])
.directive('reviewStars', function() {
return {
restrict: 'E',
scope: {},
bindToController: {
rating: '#'
},
link: function(scope, elem, attrs) {
// instantiate the lightbox code here
},
controller: function () {
var vm = this;
// controller code goes here (e.g. the onClick event handler)
},
controllerAs: 'ctrl',
templateUrl: 'review-stars.html' // the HTML code goes in this file
};
});
Check out Rangle's ng-course (https://github.com/rangle/ngcourse) or the Angular docs (docs.angularjs.org) for more on directives.
I'm curious as to how I would go about unit testing what I think is an anonymous controller inside of a directive.
directive.js
app.directive('directive',
function() {
var controller = ['$scope', function($scope) {
$scope.add = function() { ... };
}];
return {
restrict: 'A',
scope: {
args: '='
},
templateUrl: '...',
controller: controller
};
}
};
Is a controller defined as such able to be unit tested? I have tried to initialize it several different ways. Currently I have it setup like this:
describe('The directive', function() {
var element,
scope,
controller;
var args = {
...
}
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(function() {
inject(function($compile, $rootScope, $controller) {
scope = $rootScope.$new();
scope.args = args;
element = angular.element('<div directive></div>');
template = $compile(element)(scope);
scope.$digest();
controller = element.$controller;
});
});
// assertions go here
});
I keep getting TypeError: 'undefined' is not an object (evaluating ...) errors, so I don't think I am initializing the controller correctly. I mainly want to know if something like this is unit testable without changing the directive's source code at all.
I'm not sure if what you are trying to do is possible. However, I do know that there is a much easier way and that is to make it a standard controller. (You seem to be aware of this already but it's worth pointing out.)
The logic in a controller really shouldn't be dependent on the directive anyway so by making a named controller you are separating concerns which is a good thing. You can even see this used in recommended style guides for AngularJS. Once you have the controller set up properly you shouldn't have any issues testing it. Splitting it out like that also helps in doing proper dependency injection making for simpler code and simpler tests.
http://plnkr.co/edit/C4mFd5MOLBD2wfm8bMhJ?p=preview
Let's take a simple example and say you want to display the value of a cookie regardless of what it is, but this could be a customer name or whatever you want. There seem to be so many options available: directives, services, directives with services, controllers - and no matter how many docs I review or blog posts I read, I still have some fundamental questions about the appropriate way to access data and then update the scope accordingly.
What's clouding my thought right now is the fact that there doesn't seem to be the equivalent of NgModelController for non ngModel capable DOM elements like span or div or anything besides user input. Basically, seeing how ngModelCtrl is utilized in the link function of a directive seems to make a lot of sense, it doesn't allow you to drown in scope soup and it nicely organizes your thoughts, but how do we achieve this decoupling with ngBind elements?
I think the answer is just 'use services', but perhaps maybe not in all cases is the thing that's gnawing at me. Suppose you want to display a very specific cookie (or a customer name) and you don't know where you want to display it, you could continually inject your custom CookieService where ever you go, but what about a specific directive that cleans things up: <specific-cookie></specific-cookie> Would we just inject our CookieService into that directive, or just access it via $cookies like we've done elsewhere.
Does the answer simply lie in whether or not you'll be accessing more than one cookie in the future? That is, if you only need one <specific-cookie></specific-cookie>, then just use $cookies in you're directive and move on with your life, or it is always appropriate to abstract away this type of call into a service, or am I just being super pedantic about understanding this.
Directive
angular-myapp.js
var app = angular.module('myApp', ['ngCookies']);
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: true,
controller: function($scope, $element, $attrs) {
$scope.cookie = $cookies[$attrs.cookie];
}
}
}]);
index.html
<div cookie="__utma">Cookie: {{cookie}}</div>
Controller
angular-myapp.js
app.controller('CookieCtrl', function($attrs, $cookies) {
this.value = $cookies[$attrs['getcookie']];
});
index.html
<a ng-controller="CookieCtrl as cookie" getCookie="__utma" href="/{{cookie.value}}">{{cookie.value}}</a>
Service
angular-myapp.js
app.controller('SomeCtrl', function($scope, CookieService) {
$scope.cookie = CookieService.getCookie('__utma');
});
app.service('CookieService', function($cookies) {
var getCookie = function(cookie) {
return $cookies[cookie];
};
return ({ getCookie: getCookie });
});
index.html
<div ng-controller="SomeCtrl">Cookie: {{cookie}}</div>
Directive with service
angular-myapp.js
app.directive('specificCookie', function(CookieService) {
return {
scope: true,
template: 'Cookie: <span ng-bind="cookie"></span>',
controller: function($scope, $element, $attrs) {
$scope.cookie = CookieService.getCookie('__utma');
}
}
});
index.html
<specific-cookie></specific-cookie>
Unless I'm misunderstanding some of your scenarios, the simplest (and proper) way to do this is to create a reusable directive that displays a cookie based on a name passed to it via its attribute.
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: {},
template: "<span>{{cookie}}</span>",
restrict: "E",
link: function(scope, element, attrs) {
attrs.$observe("name", function(newVal){
scope.cookie = $cookies[newVal];
});
}
};
}]);
The usage would be trivial (and page controller-independent):
<cookie name="__utma"></cookie>
<input ng-model="cookieName" type="text">
<cookie name="{{cookieName}}"></cookie>
the resulting DOM would be:
<span class="ng-binding">137862001.838693016.141754...</span>
<span class="ng-binding">GA1.2.838693016.1417544553</span>
What would be considered as best practice, attaching directive to element or binding event inside the controller?
Directive
<openread-more what-to-expand="teds-bets-readmore" />
myApp.directive('openreadMore', function () {
return {
restrict: 'AE',
replace: false,
template: '<a class="attach-event" what-to-expand="readmore1">Event</a></span>',
link: function (scope, elem, attrs) {
elem.on('click', function () {
// attached code on click
});
}
}
});
Just attaching it inside the controller
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
angular.element(document.querySelectorAll('.attach-event')).on('click', function () {
// attached code on click
});
});
The second option seems shorter and much cleaner, but i don't know if it's considered as best practice or not.
Just use the ng-click directive.
<openread-more what-to-expand="teds-bets-readmore" ng-click="doSomeAction()" />
And on the controller:
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
$scope.doSomeAction = function() {
// onClick logic here...
};
});
Edit
In case you are binding other kind of events, just make this question to yourself: "Will I have different behaviours for this event depending on the current view or application state?".
If the answer is yes then you should register the event handlers on the controllers. If the answer is no (which means you will have always the same behaviour) then register and handle the events on the directive.
Nevertheless, you should not access UI elements on the controllers (e.g. don't use selectors or anything similar). The controllers are supposed to be reusable, which means you should be able to use them on different UIs, with different UI elements. The best approach is to define a directive that allows you to bind specific events, like Angular UI Event Binder.
I have this two directives, one nested inside each other :
<envato class="container content-view-container" data-ng-cloak data-ng-hide="spinner">
<items data-ng-repeat="items in marketplaces"></items>
</envato>
And each of those two are defined as such :
Application.Envato.directive("envato", ["$timeout", function($timeout){
var object = {
restrict : "E",
controller : "EnvatoAPIController",
transclude : true,
replace : true,
templateUrl : "templates/envato-view.php",
link : function(scope, element, attrs, controller) {
console.log(scope);
return controller.getLatestItems().then(function(data) {
scope.marketplaces = angular.fromJson(data);
scope.count = scope.marketplaces.length;
var tst = angular.element(element).find(".thumbnails");
/* $timeout(function() { scope.swiper = new Swipe(document.getElementById('swiper-container')); }, 5000); */
scope.spinner = false;
});
}
};
return object;
}]);
Application.Envato.directive("items", function(){
var iterator = [],
object = {
require : "^envato",
restrict : "E",
transclude : false,
replace : true,
templateUrl : "templates/envato-items-view.php",
link : function(scope, element, attrs, controller) {
iterator.push(element);
if (iterator.length === scope.$parent.$parent.count) { console.log(iterator); };
}
};
return object;
});
A lot of the code above might not make a lot of sense because it's part of a bigger application, but I hope it does for my question. What I'm trying to do is to change a scope property of the directive envato from the directive items. Because I have a iteration and I want to know when it's done so I can do another operation on the appended DOM elements during that iteration.
For instance let's say I will have the scope.swipe defined inside the directive envato, and watch it for changes. In the directive items, I will watch when the ng-repeat is done and then change the above defined scope property scope.swipe. This will trigger the change inside the directive envato, and now I will know that I can do my operation.
I hope that I'm clear enough, if not I could try having more code or I'll try being more specific. How could I achieve what I just described above ?
EDIT : I do know that using : console.log(angular.element(element.parent()).scope()); inside the directive items will give me the scope of the envato directive, but I was wondering if there was a better way of doing it.
For this kind of inter-directive communication, I recommend defining an API/method on your envato directive that your items directive can call.
var EnvatoAPIController = function($scope) {
...
this.doSomething = function() { ... }
}
Your items directive already requires the envato directive, so in the link function of your items directive, just call the the API when appropriate:
require : "^envato",
link : function(scope, element, attrs, EnvatoCtrl) {
...
if(scope.$last) {
EnvatoCtrl.doSomething();
}
}
What is nice about this approach is that it will work even if you someday decide to use isolate scopes in your directives.
The tabs and pane directives on the AngularJS home page use this communication mechanism. See https://stackoverflow.com/a/14168699/215945 for more information. See also John's Directive to Directive Communication video.
Use scope.$eval('count') at item directive and let angular resolve for you.
I think you are looking for a callback that gets called when the ng-repeat completes. If that's what you want, i have created a fiddle. http://jsfiddle.net/wjFZR/.
There is no much of UI in the fiddle. Please open the firebug console, and run the fiddle again. You will see an log. That log is called at the end of an ng-repeat defined in the cell directive.
$scope.rowDone = function(){
console.log($scope)
} this is the callback function that is defined on the row directive that will get called when the ng-repeat of the cell directive is completed.
It is registered in this way.
<cell ng-repeat="data in rowData" repeat-done="rowDone()"></cell>
Disclaimer: I'm too a newbie in angularjs.
Hmmm it appears you are trying to make it difficult for yourself. In your directive you do not set a scope property:
var object = {
restrict : "E",
transclude : true,
replace : true,
scope: true,
...
Setting scope: {} will give your directive an fully isolated new scope.
BUT setting scope: true will give your directive a fully isolated new scope that inherits the parent.
I use this method to contain the model in the top level parent directive and allow it to filter down through all the child directives.
I love Mark's answer but I eventually created an attribute directive to save element directives' scopes to the rootScope like so:
myApp.directive('gScope', function(){
return {
restrict: 'A',
replace: false,
transclude: false,
controller: "DirectiveCntl",
link: function(scope, element, attrs, controller) {
controller.saveScope(attrs.gScope);
}
}
});
...
function DirectiveCntl($scope, $rootScope) {
this.saveScope = function(id) {
if($rootScope.directiveScope == undefined) {
$rootScope.directiveScope = [];
}
$rootScope.directiveScope[id] = $scope;
};
}
...
<span>Now I can access the message here: {{directiveScope['myScopeId'].message}}</span>
<other-directive>
<other-directive g-scope="myScopeId" ng-model="message"></other-directive>
</other-directive>
Note: While this makes it a snap to collect data from all your various directives it comes with my word of caution that now you have to ensure the potential pile of scopes are properly managed to avoid causing a memory leak on pages. Especially if you are using the ng-view to create a one page app.