I have several directives that need to call the same function after doing their thing. This function needs to access the main controller scope, but also modify the DOM. How and where should this function be declared?
You should use a service, services has access to $rootScope, although is it better to keep DOM modification at directive level, in certain cases you can go for it.
angular.module("myModule", [])
.factory("MyService", function ($rootScope) {
return {
myFunction: function () { // do stuff here }
}
})
.directive("MyDirective", function (MyService) {
return {
link: function (scope, iElement, iAttrs) {
// try do keep dom modification here, you have access to MyService,
// let it do the algorithm and try to keep dom modification here.
// PS: you can also inject $rootScope to directives.
};
};
});
If this function needs to access Controller's scope, I would use the scope which is accessible in the directive and pass it as param, like so:
var MyDirective = function() {
var linkFn = function(scope, element, attrs) {
callAwesomeFn(scope);
};
return {
link: linkFn
}
};
Now, where to put it?
if this is utility function, I would put in some utilities namespace, like MyFunctions.callAwesomeFn()
if this is a component which needs to interact with other objects managed by Angular - use Dependency Injection and inject it into directive
Related
I saw the following question (among other similar questions) and it solves the problem of trying to inject a factory into a directive's link function:
Injecting service to Directive
The solutions I've seen keep the link function within the scope of the directive:
angular.module('myapp')
.directive('myDir', function(myService){
return {
restrict: 'E',
scope: {
frame: '='
},
link: function postLinkFn(scope, elem, attr) {
myService.doSomething();
}
};
});
However, I want to be able to separate the postLinkFn outside of the .directive scope for organization, just like I can do with controllers.
Is it possible to separate this function while also injecting a service into it?
.directive('myDir', function(myService){
var deps = { myService: myService };
return {
...
// myService is available as this.myService inside postLinkFn
link: angular.bind(deps, postLinkFn)
};
});
link function doesn't make use of dependency injection and doesn't have lexical this, binding injected dependencies to this is a reasonable move.
Sure you can put your code in a factory and then refer it globally across the app.
angular.module('myapp').factory('myfactory', myService, function(){
return{
var myfac;
my fac = function (myService){
var myItem = myService.doSomething;
return myItem;
};
};
}).
.directive('myDir', function(myService){
return {
restrict: 'E',
scope: {
frame: '='
},
link: myfactory.myItem;
};
});'
Just a little care you need to take is binding your factory with an angular promise $q if your service deals with asynchronous calls.
I have several directives that use the same link function. (The link function adds some extra state and html depending on the use case.) So I declared this as follows:
function common_linkfunc(){...}
var Directive_1 = function($scope, etc) {
return {restrict:"E", ...
link: common_linkfunc,
controller: function($scope, etc) {...}
};
}
Directive_1.$injects = ["$scope", "etc"];
angular.module("module").directive("D1", Directive_1)...;
First change was when link function required $compile. Next I need to add $templateCache and my question is how can I do this systematically?
My first approach was to rewrite common_linkfunc as
function foo($compile, $templateCache) {
return common_linkfunc($compile, $templateCache) {...}
}
and then use this in every directive:
...
link: foo($compile, $templateCache),
...
But this is copy-and-paste! Is there an easier and less error prone way to do the same?
Regardless of the solution, you'll need to pass some argument to your common link function, because Angular won't inject anything into it for you. That being said, I can think of two different approaches:
1) Use arguments
app.directive('foo', function($http, $timeout) {
return {
restrict: 'E',
link: linkFn1.apply(null, arguments)
}
});
function linkFn1($http, $timeout) {
return function(scope, element, attrs) {
// ...
};
}
The downside here is that the order of the arguments in the directive function matters. If some other directive uses a different order, the code won't work properly.
2) Use $injector
app.directive('bar', function($injector) {
return {
restrict: 'E',
link: linkFn2($injector)
}
});
function linkFn2($injector) {
var $http = $injector.get('$http'),
$timeout = $injector.get('$timeout');
return function(scope, element, attrs) {
// ...
};
}
Working Plunker
i wrote a function called getDepth(object) that gives me the depth of an object and then returns a string, for example if the depth of the object was 3 it will return the string "sub-sub-sub", and in my directive template i want to call that function from ng-class such as
'<span ng-class="getDepth(item)">{{item.content}}</span>'
but i am not sure where to put that function in my directive, should it just be inside the link function?
This is typically the job for a controller. So you can create an anonymous controller in your directive and place it there, or in the scope of the parent controller looking after this section of code - that is assuming of course one exists.
Maybe though you would like to reuse this functionality later in the app, so I recommend placing it high on the controller tree to allow others to inherit its function.
The linker functions job is strictly DOM manipulation, and this is not DOM manipulation this is a function returning a string and the ng-class directive in turn does the DOM manipulation.
If you check the docs:
Directives that want to modify the DOM typically use the link option. link takes a function with the following signature, function link(scope, element, attrs) { ... } where:
.directive('myCurrentTime', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}...
As you can see the link function is changing the DOM.
So to answer the question, this is how the directive should be structured.
app.directive('myDirective', function() {
var controller = function($scope) {
$scope.getDepth = function (item) {
return "text-success";
};
}
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '='},
controller: controller // or this can be the name of an outside reference controller as well - which i prefer for unit testing and reusability purposes.
}
};
}
]);
if you want to let this control to your directive instead controller you can put it in the link function, an example directive should be like this...
app.directive('myDirective', ['$compile',
function($compile) {
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '=item'},
link: function(scope, element, attrs) {
scope.getDepth = function (item) {
return "text-success";
};
}
};
}
]);
here is PLUNKER
I'm gradually getting the hang of Angular directives and so far, have resorted to creating a service as an intermediary between controllers.
I was just wondering, in the context of directives (and linking functions) is it possible to give the controller access to variables from the linking function? (Without a service or global variables).
module.exports = function() {
return {
restrict: 'A',
templateUrl: 'partials/collection',
link: function(scope, element, attrs) {
var name = attrs.collectionName;
// from here
},
controller: function($scope, socket) {
$scope.models = [];
// to here
socket.on('ready', function() {
socket.emit(name + '/get');
});
}
}
};
I want the collection-name attribute's value to be available within my controller, so that I can make appropriate socket calls. Any idea?
You can add a method on the controller and the call it from the link function.
controller: function($scope, socket) {
this.setSocket = function(name){
{...}
}
}
On link:
link: function(scope, element, attrs, controller){
var name = attrs.collectionName;
controller.setSocket(name);
}
They share the same scope, so this should work.
module.exports = function() {
return {
restrict: 'A',
templateUrl: 'partials/collection',
link: function(scope, element, attrs) {
scope.name = attrs.collectionName;
// from here
},
controller: function($scope, socket) {
$scope.models = [];
// to here
socket.on('ready', function() {
socket.emit($scope.name + '/get');
});
}
}
};
There are a couple of ways of doing what you want
Just put everything in the link function. You can set functions and variables on the scope just like you might put in a controller.
Just put everything in the controller, in terms of setting scope variables or functions. It is injected with $attrs, which contains the normalised attribute values, so you have access to the attributes if you need them.
As far as I know, in most cases it doesn't make a difference where you assign variables or functions on the scope. The main difference between the two is that if you want to give your directive a public API, so other directives can communicate to it via require, then you must use this.something in the controller.
There maybe a better way to do it, but I've managed to get around the problem by changing my controller functions dependencies to include an $element argument. Then just used jqLite to get the value of the attribute in question.
controller: function($scope, $element, socket) {
var name = $element.attr('collection-name');
}
It's not fantastically elegant, but it works.
I am trying to put some default values in my directive with Isolate scope. Basically, I need to do some DOM manipulations using the scope object when my directive is bound. Below is my code:
Controller:
angular.module('ctrl').controller('TempCtrl', function($scope, $location, $window, $timeout, RestService, CommonSerivce) {
$scope.showAppEditWindow = function() {
//Binding the directive isolate scope objects with parent scope objects
$scope.asAppObj = $scope.appObj;
$scope.asAppSubs = $scope.appSubscriptions;
//Making Initial Settings
CommonSerivce.broadcastFunction('doDirectiveBroadcast', "");
};
Service:
angular.module('Services').factory('CommonSerivce', function ($rootScope) {
return {
broadcastFunction: function(listener, args) {
$rootScope.$broadcast(listener, args);
}
};
Directive:
angular.module('directives').directive('tempDirective', function() {
return {
restrict : 'E',
scope:{
appObj:'=asAppObj',
appSubs: '=asAppSubs'
},
link : function(scope, element, attrs) {},
controller : function ($scope,Services,CommonSerivce) {
//Broadcast Listener
$scope.$on('doDirectiveBroadcast', function (event, args) {
$scope.setDefaults();
});
$scope.setDefaults = function() {
//Setting Default Value
alert(JSON.stringify($scope.appSubs)); //Coming as undefined
};
},
templateUrl:"../template.html"
};
});
Custom Directive element:
<temp-directive as-app-obj="asAppObj" as-app-subs="asAppSubs" />
Now, the issue is that while trying to access the isolate scope in the default method inside directive, I aam getting an undefined value whereas the data is coming and is getting bound to the DOM. How can I access the isolate scope in the broadcast listener and modify the directive template HTML? Is there another wasy for handling this?
The problem is: at that time angular does not update its bindings yet.
You should not access your variables like this, try to use angular js binding mechanism to bind it to view (by using $watch for example). Binding to parent scope variables means you're passive, just listen for changes and update other variables or your view. That's how we should work with angular.
If you still need to access it. You could try a workaround using $timeout
$scope.setDefaults = function() {
$timeout(function () {
alert(JSON.stringify($scope.appSubs)); //Coming as undefined
},0);
};
DEMO
It's better to use $watch
angular.module('ctrl', []).controller('TempCtrl', function ($scope, $location, $rootScope) {
$scope.appSubscriptions = "Subscriptions";
$scope.appObj = "Objs";
$scope.showAppEditWindow = function () {
//Binding the directive isolate scope objects with parent scope objects
$scope.asAppObj = $scope.appObj;
$scope.asAppSubs = $scope.appSubscriptions;
};
});
angular.module('ctrl').directive('tempDirective', function () {
return {
restrict: 'E',
replace: true,
scope: {
appObj: '=asAppObj',
appSubs: '=asAppSubs'
},
link: function (scope, element, attrs) {
},
controller: function ($scope, $timeout) {
$scope.$watch("appSubs",function(newValue,OldValue,scope){
if (newValue){
alert(JSON.stringify(newValue));
}
});
},
template: "<div>{{appSubs}}</div>"
};
});
DEMO
By using $watch, you don't need to broadcast your event in this case.
Most likely the isolated scope variable is not available when the directive's controller first instantiates but probably its available when you need it for a following event such as: within a function bound to an ng-click
its just a race condition and the object doesn't arrive exactly when directive's controller loads