Angular: How to encapsulate logic in a directive? - angularjs

I wonder how I can encapsulate functionality inside of an angular directive according to Rober C. Martin's "Clean Code" book. I want to omit comments and use functions with speaking names instead.
Imagine this code:
app.directive('myDirective' function() {
return {
link: function(scope) {
// initialize visual user state
scope.visualUserState = {
// some very detailed state initialization
}
}
})
To encapsulate the load functionality, I would like to replace this code like this:
app.directive('myDirective' function() {
return {
link: function(scope) {
scope.initializeVisualUserState = function() {
scope.visualUserState = {
// some very detailed state initialization
}
}
scope.initializeVisualUserState();
}
})
What I do not like on the second approach is that "loadDataFromServer" is some functionality that is only used by the link function and not by the view, so I violate the rule that scope should only hold data and functions that is used to interact with the view.
Also the code is not very readable I think.
As the functionality handles very private stuff of the directive, I think that using and injecting a service is not the right way to go.
What would be a better practice to encapsulate this functionality?

You should use a controller to add logic to your directive. In your controller you can inject Services. It's best to write a service for a single purpose, and simply let your controller call the services.
In fact you should only use the link function if you need to have your DOM-node, which is actually pretty close to never.
Read the styleguide by John Papa
angular.module('myModule', []);
// Controller
(function() {
angular
.controller('myModule')
.controller('MyController', ['$scope', 'DataService', function($scope, DataService) {
DataService
.retrieveData()
.then(function(data) {
$scope.visualUserState = data;
});
}]);
})();
// Directive
(function() {
angular
.module('myModule')
.directive('myDirective', function() {
return {
'restrict': 'E',
'scope': true,
'controller': 'MyController',
'controllerAs': '$ctrl'
};
});
})();

(function(module){
module.directive('myDirective', myDirective);
function myDirective(){
var directive = {
link : link,
scope : {state : '='},
restrict : 'EA',
template : //Some template which uses state
}
return directive;
function link(){
}
};
module.controller('myController', myController);
myController.$inject = ['$scope', 'OtherService'];
function myController(scope, service){
//service is used to pull the data and set to $scope.userState.
}
})(angular.module('myApp'))
And your directive will be :
<myDirective state="userState"></myDirective>
Let me know if this helps.

Related

Controller executes twice, what is the best solution?

I know where the problem is i just don't know how to go about fixing it. I have two directives that call the same controller and after research i found out its a bad thing and i should use a service or something.
Now i believe i have to communicate between both these controllers. Every time i do a console.log inside the controller it runs twice.
What should i do?
Directives
app.directive("sidemenu", function() {
return {
restrict: 'A',
templateUrl: 'partials/sidemenu.html',
scope: true,
transclude : false,
controller: 'taskbarController'
}
});
app.directive("taskbar", function() {
return {
restrict: 'A',
templateUrl: 'partials/taskbar.html',
scope: true,
transclude : false,
controller: 'taskbarController'
}
});
Controller:
app.controller("taskbarController", ['$scope', 'authData', '$location', 'projectsModal', 'sendMessageModal', 'Poller',
function ($scope, authData, $location, projectsModal, sendMessageModal, Poller) {
$scope.inbox = Poller.msgdata;
$scope.project = Poller.newdata;
$scope.projects = Poller.projects;
$scope.messages = Poller.messages;
console.log($scope.inbox);
$scope.sendMessage = sendMessageModal.activate;
$scope.showModal = function() {
projectsModal.deactivate();
projectsModal.activate();
};
$scope.logout = function () {
authData.get('logout').then(function (results) {
authData.toast(results);
$location.path('login');
});
}
authData.get('session');
$scope.toggle = function(){
$scope.checked = !$scope.checked
projectsModal.deactivate();
sendMessageModal.deactivate();
}
}]);
You could still use controller (rather then service) as long as you are using it to bind the view (for e.g.) If you want to make a webservice call (for e.g.) then I would use service.
Thing you need to think about is that do you need two directive to share same service or just scope? If they (directive) are functionally different then use separate service/contr (Single Responsibility Principal) and if they have some shared data/scope then think about how to cater for that.If you want to share scope between controllers then you can use service which gets injects into the controller.

Can I require a controller, in a directive, set with ng-controller?

I have (sort of) the following html:
<div ng-controller="MyController">
<my-sub-directive></my-sub-directive>
</div>
how the controller looks is not important:
app.controller("MyController", function($scope) {
$scope.foo = "bar";
})
and my directive looks like this:
function mySubDirective() {
return {
restrict: "E",
templateUrl:"aTemplate.html",
require: "^MyController",
link: function($scope, element) {
}
};
}
app.directive("mySubDirective", mySubDirective);
In the documentation they always specify another directive in the require-property, but it says that it means you require the controller. So I wanted to try this solution. However I get the error
"Controller 'MyController', required by directive 'mySubDirective', can't be found".
Is it not possible to require a controller from the directive if it is set by ng-controller?
You can only do:
require: "^ngController"
So, you can't be more specific than that, i.e. you can't ask for "MainCtrl" or "MyController" by name, but it will get you the controller instance:
.controller("SomeController", function(){
this.doSomething = function(){
//
};
})
.directive("foo", function(){
return {
require: "?^ngController",
link: function(scope, element, attrs, ctrl){
if (ctrl && ctrl.doSomething){
ctrl.doSomething();
}
}
}
});
<div ng-controller="SomeController">
<foo></foo>
</div>
I don't think, though, that this is a good approach, since it makes the directive very dependent on where it is used. You could follow\ the recommendation in the comments to pass the controller instance directly - it makes it somewhat more explicit:
<div ng-controller="SomeController as ctrl">
<foo ctrl="ctrl"></foo>
</div>
but it still is a too generic of an object and could easily be misused by users of your directive.
Instead, expose a well-defined API (via attributes) and pass references to functions/properties defined in the controller:
<div ng-controller="SomeController as ctrl">
<foo do="ctrl.doSomething()"></foo>
</div>
You can use element.controller() in the directive link function to test the closest controller specified by ngController. A limitation of this method is that it doesn't tell you which controller it is. There are probably several ways you can do it, but I'm opting to name the controller constructor, and expose it in the scope, so you can use instanceof
// Deliberately not adding to global scope
(function() {
var app = angular.module('my-app', []);
// Exposed in so can do "instanceof" in directive
function MyController($scope) {}
app.controller('MyController', MyController);
app.directive("foo", function(){
return {
link: function($scope, $element){
var controller = $element.controller();
// True or false depending on whether the closest
// ngController is a MyController
console.log(controller instanceof MyController);
}
};
})
})();
You can see this at http://plnkr.co/edit/AVmr7Eb7dQD70Mpmhpjm?p=preview
However, this won't work if you have nested ngControllers, and you want to test for one that isn't necessarily the closest. For that, you can defined a recursive function to walk up the DOM tree:
app.directive("foo", function(){
function getAncestorController(element, controllerConstructor) {
var controller = element.controller();
if (controller instanceof controllerConstructor) {
return controller;
} else if (element.parent().length) {
return getAncestorController(element.parent(), controllerConstructor);
} else {
return void(0); // undefined
}
}
return {
link: function(scope, element){
var controller = getAncestorController(element, MyController);
// The ancestor controller instance, or undefined
console.log(controller);
}
};
})
You can see this at http://plnkr.co/edit/xM5or4skle62Y9UPKfwG?p=preview
For reference the docs state that the controller function can be used to find controllers specified with ngController:
By default retrieves controller associated with the ngController directive

AngularJS - how to add javascript dependency in Directives?

I have created a directives and while I call that directives, I need to load my JS first. Is there anyway to inject Javascript directly into Directives of angularjs?
Here is the code.
var directive = module.exports = function($rootScope, $location, service, user) {
return {
restrict: 'E',
link: function () {
$rootScope.$on( '$routeChangeSuccess', function(event,current) {
var path = $location.path();
var pageName = current.$$route.title ? current.$$route.title : path;
if(!current.redirectTo) {
user.then(function(userInfo) {
service.method(pageName, {
marketName: userInfo.marketName,
accountName: userInfo.name,
clientServices: userInfo.isClientServices(),
userType: userInfo.type
});
});
}
});
}
};
};
directive.$inject = ['$rootScope', '$location', 'service', 'user'];
Please do needful.
I dont think you can bundle a directive with js code (like web components), the js has to be included outside of the directive. You can however use angularjs dependency injection to inject the included angular services to your directive:
app.factory('myDependency', function() {
var myDependencyInstance;
return myDependencyInstance;
});
app.directive('directive', ['myDependency', function(myDependency) {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
}])
But I guess its not what you want. You could bundle js into the directive's template, but then the js code would be duplicated every time you add the directive and this would propably only work with non angular js code.

AngularJS abstracting directives

How does one abstract a directive properly?
As a really basic example, let's say I have this:
http://plnkr.co/edit/h5HXEe?p=info
var app = angular.module('TestApp', []);
app.controller('testCtrl', function($scope) {
this.save = function() {
console.log("hi");
}
this.registerListeners = function() {
console.log('do stuff to register listeners');
}
this.otherFunctionsNotToBeChangedWithDifferentInstances() {
console.log('these should not change between different directives')
}
return $scope.testCtrl = this;
});
app.directive("tester", function() {
return {
restrict: 'A',
controller: 'testCtrl',
template: '<button ng-click="testCtrl.save()">save</button>'
};
});
The tester directive has some methods on it, but only two will be changed or used depending on where the directive is placed. I could pass in the function as a directive attribute, but I am wondering if there is a better way to do this. I have been looking at providers, but I am unsure how or if those would even fit into this.
Instead of letting your directive assume that testCtrl.save() exist on the scope, you would pass in that function as an attribute. Something like this: http://jsbin.com/jidizoxi/1/edit
Your directive binds the value of the my-on-click attribute as a callable function. Your template passes in the controllers ctrlOnClick() function, and when the buttons ng-click calls myOnClick() Angular will call ctrlOnClick() since they are bound to each other.
EDIT:
Another common approach is to pass in a config object to the directive. So your controller would look something like:
$scope.directiveConfig = {
method1: function() { ... },
method2: function() { ... },
method3: function() { ... },
...
}
And your template:
<my-directive config="directiveConfig"></my-directive>
The directive then gets a reference to that object by:
scope: {
config: '='
}
The directive can then call methods on the object like this: $scope.config.method1().

Passing a callback form directive to controller function in AngularJS

I have a directive and a controller. The directive defines a function in its isolate scope. It also references a function in the controller. That function takes a callback. However, when I call it from the directive and pass in a callback, the callback is passed through as undefined. The code below will make this more clear:
Directive
directive('unflagBtn', ["$window", "api",
function($window, api) {
return {
restrict: "E",
template: "<a ng-click='unflag(config.currentItemId)' class='btn btn-default'>Unflag</a>",
require: "^DataCtrl",
scope: {
config: "=",
next: "&"
},
controller: ["$scope",
function($scope) {
$scope.unflag = function(id) {
$scope.next(function() { //this callback does not get passed
api.unflag(id, function(result) {
//do something
return
});
});
};
}
]
};
}
]);
Controller
controller('DataCtrl', ['$rootScope', '$scope', 'api', 'dataManager', 'globals',
function($rootScope, $scope, api, dataManager, globals) {
...
$scope.next = function(cb) { //This function gets called, but the callback is undefined.
// do something here
return cb ? cb() : null;
};
}
]);
HTML
<unflag-btn config="config" next="next(cb)"></unflag-btn>
I've read here How to pass argument to method defined in controller but called from directive in Angularjs? that when passing parameters from directives to controller functions, the parameters need to be passed in as objects. So I tried something like this:
$scope.next({cb: function() { //this callback does not get passed still
api.unflag(id, function(result) {
//do something
return
});
}});
But that did not work. I am not sure if this matters, but I should note that the directive is placed inside a form, which in its place is inside a controller. Just to illustrate the structure:
<controller>
<form>
<directive>
<form>
<controller>
Hope this is clear and thanks in advance!
Try this
controller: ["$scope",
function($scope) {
$scope.unflag = function(id) {
$scope.next({
cb: function() { //this callback does not get passed
api.unflag(id, function(result) {
//do something
return;
});
}
});
};
}
]
So I unintentionally figured out whats wrong after not being able to pass an object back to the controller as well. What happened, (and what I probably should have mentioned in the question had I known that its relevant) is that the parent scope of this directive unflagbtn is actually the scope of another directive that I have, call it secondDirective. In its turn the secondDirective is getting its scope from "DataCtrl". Simplified code looks like this:
directive("secondDirective", [function(){
require: "^DataCtrl" // controller
scope: {
next: "&" // function that I was trying to call
}
...
// other code
...
}]);
directive("unflagbtn", [function(){
require: "^DataCtrl" // controller
scope: {
next: "&"
},
controller: ["$scope", function($scope){
$scope.unflag = function(){
$scope.next({cb: {cb: callBackFunctionIWantedToPass}); // this is what worked
}
}
}]);
So passing a callback in that manner solved my problem as it made its way back to the controller. This is ugly most likely due to my poor understanding of angular, so I apologize as this is most likely not the correct way to do this, but it solved my problem so I though I'd share.
Cheers,

Resources