AngularJS abstracting directives - angularjs

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().

Related

Angular: How to encapsulate logic in a directive?

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.

Is it possible to pass in a custom controller into custom directive?

Is it possible to pass in a custom controller into custom directive to be able to use the custom directive on the page with different controllers?
I can't find a solution for that on docs.angularjs.org
[Edited]
Let's say we have the following directive's defenition:
angular.module('myApp', [])
.controller('myDirectiveController', function ($scope) {
$scope.name = 'there, dude';
})
.directive('myDirective', function () {
return {
restrict: 'E',
replace: true,
template: '<div>Hello {{name}}!</div>',
controller: 'myDirectiveController' // can i overwrite it outside this code?
};
});
Can I simply overwrite the directive's controller not touching the directive's source code itself?
Use custom controller with one HTML template
Then pass your data from that page to directive.
and use that data in HTML template which is assign in directive or also you can write controller in your directive.
.controller('myController', function () {
// write business logic here
// take some data which you want to use in directive
});
Then pass it through HTML to directive
Use that data in,
.directive('dir', function () {
return {
scope: {
// collect your data and use it in link
}
};
});
Perhaps instead of defining the controller in the directive like you have, you can put it in the html template like:
.directive('myDirective', function () {
return {
restrict: 'E',
replace: true,
scope: {
ctrl: '='
},
template: '<div ng-controller="{{ctrl}}">Hello {{name}}!</div>'
};
});
Then I think you will be able to use the directive like:
<my-directive ctrl="myDirectiveController"></my-directive>

Angular directive scope parameter function, pass a function to it

I have a directive defined that contains a function binding:
angular
.module('my.module')
.directive("myDirective", [
function () {
return {
controller: 'MyDirectiveController',
templateUrl: 'controls/myDirective/myDirective.html',
replace: true,
restrict: 'E',
scope: {
openFunction: '&'
}
};
}]);
in my source html, I'm defining the directive like so:
<my-directive
open-function="openDrawer(open)"
</my-directive>
Then, in my directive controller, I'm calling it like this:
$scope.openFunction({
open: function() {
doSomething()
.then(function () {...})
.finally(function () {...});
}
});
And here's the parent controller openDrawer function:
$scope.openDrawer = function (open) {
$scope.alerts = null;
$scope.showActions = false;
if (service.editing) {
service.closeAndSave();
openDrawerAfterDelay(open);
} else if (otherService.editing) {
otherService.commit();
openDrawerAfterDelay(open);
} else {
open();
}
};
The problem is, when my directive controller calls the $scope.openFunction() function, nothing happens. Am I able to pass a function, to the bound function like this?
This appears to be something unrelated. I've reproduced the code in plunker here: http://plnkr.co/edit/SuzCGw5WVRClEwEcA17l?p=preview
Possible leads that I can see without seeing the entire code are:
I'm mocking the service and otherService, so the if conditions are always false - the problem may lie in the calls within the if blocks.
I'm unsure of what the doSomething promise is doing, this may be a cause.

Angular - create multiple directives from same function

To create a directive/controller/factory whatever, you provide a function serving as the "injection point":
angular.module('myApp').directive('myDirective', ['dependencies', function injectionPoint(dependencies) {}]);
Can you provide that injectionPoint via another function registered with Angular? e.g. a factory? I should note that I've declared everything in separate files, and I'm trying to do it 'the angular way' and not create unnecessary globals.
Specifically, I've got two input directives that are basically the same, just with different templates and isolate scope declaration.
I thought to create a "directive factory" I could use, like:
function directiveFactory(scopeOverride, extraInit) {
return function directiveInjectionPoint(depedencies) {
return { restrict...,
template...,
scope: angular.extend({/*defaults*/}, scopeOverride),
link: function(...) {
// default stuff
if(angular.isDefined(extraInit)) extraInit($scope, el, attrs);
}
}
}
which I would then use like:
angular.module('myApp')
.directive('myDirective1', directiveFactory({/* new stuff */))
.directive('myDirective2', directiveFactory({/* other stuff */, fn2...));
But how do I register that function with Angular, and then use it in the directive declaration?
You can use a service and directives like this:
app.factory('MyService', function() {
var MyService = {};
MyService.createDirective = function(scopeOverride, extraInit) {
return {
restrict: 'E',
transclude: true,
scope: scopeOverride,
template: '<pre ng-transclude></pre>'
};
};
return MyService;
});
app.directive('drzausDirective1', function(MyService) {
return MyService.createDirective(true);
});
app.directive('drzausDirective2', function(MyService) {
return MyService.createDirective(false);
});
http://plnkr.co/edit/BuKMXxMQ4XVlsykfrDlk?p=preview

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