How to call a method defined in an AngularJS directive?
Solution 1
If you want to use isolated scopes you can pass a control object using bi-directional binding =
of a variable from the controller scope. You can also control also several instances of the same directive on a page with the same control object.
angular.module('directiveControlDemo', [])
.controller('MainCtrl', function($scope) {
$scope.focusinControl = {};
})
.directive('focusin', function factory() {
return {
restrict: 'E',
replace: true,
template: '<div>A:{{internalControl}}</div>',
scope: {
control: '='
},
link: function(scope, element, attrs) {
scope.internalControl = scope.control || {};
scope.internalControl.takenTablets = 0;
scope.internalControl.takeTablet = function() {
scope.internalControl.takenTablets += 1;
}
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="directiveControlDemo">
<div ng-controller="MainCtrl">
<button ng-click="focusinControl.takeTablet()">Call directive function</button>
<p>
<b>In controller scope:</b>
{{focusinControl}}
</p>
<p>
<b>In directive scope:</b>
<focusin control="focusinControl"></focusin>
</p>
<p>
<b>Without control object:</b>
<focusin></focusin>
</p>
</div>
</div>
Solution 2
Assuming that the action button uses the same controller $scope
as the directive, just define function updateMap
on $scope
inside the link function. Your controller can then call that function when the action button is clicked.
<div ng-controller="MyCtrl">
<map></map>
<button ng-click="updateMap()">call updateMap()</button>
</div>
app.directive('map', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function($scope, element, attrs) {
$scope.updateMap = function() {
alert('inside updateMap()');
}
}
}
});
As per @FlorianF's comment, if the directive uses an isolated scope, things are more complicated. Here's one way to make it work: add a set-fn
attribute to the map
directive which will register the directive function with the controller:
<map set-fn="setDirectiveFn(theDirFn)"></map>
<button ng-click="directiveFn()">call directive function</button>
scope: { setFn: '&' },
link: function(scope, element, attrs) {
scope.updateMap = function() {
alert('inside updateMap()');
}
scope.setFn({theDirFn: scope.updateMap});
}
function MyCtrl($scope) {
$scope.setDirectiveFn = function(directiveFn) {
$scope.directiveFn = directiveFn;
};
}
Solution 3
Although it might be tempting to expose an object on the isolated scope of a directive to facilitate communicating with it, doing can lead to confusing "spaghetti" code, especially if you need to chain this communication through a couple levels (controller, to directive, to nested directive, etc.)
We originally went down this path but after some more research found that it made more sense and resulted in both more maintainable and readable code to expose events and properties that a directive will use for communication via a service then using $watch on that service's properties in the directive or any other controls that would need to react to those changes for communication.
This abstraction works very nicely with AngularJS's dependency injection framework as you can inject the service into any items that need to react to those events. If you look at the Angular.js file, you'll see that the directives in there also use services and $watch in this manner, they don't expose events over the isolated scope.
Lastly, in the case that you need to communicate between directives that are dependent on one another, I would recommend sharing a controller between those directives as the means of communication.
AngularJS's Wiki for Best Practices also mentions this:
Only use .$broadcast(), .$emit() and .$on() for atomic events Events that are relevant globally across the entire app (such as a user authenticating or the app closing). If you want events specific to modules, services or widgets you should consider Services, Directive Controllers, or 3rd Party Libs
- $scope.$watch() should replace the need for events
- Injecting services and calling methods directly is also useful for direct communication
- Directives are able to directly communicate with each other through directive-controllers
Solution 4
Building on Oliver's answer - you might not always need to access a directive's inner methods, and in those cases you probably don't want to have to create a blank object and add a control
attr to the directive just to prevent it from throwing an error (cannot set property 'takeTablet' of undefined
).
You also might want to use the method in other places within the directive.
I would add a check to make sure scope.control
exists, and set methods to it in a similar fashion to the revealing module pattern
app.directive('focusin', function factory() {
return {
restrict: 'E',
replace: true,
template: '<div>A:{{control}}</div>',
scope: {
control: '='
},
link : function (scope, element, attrs) {
var takenTablets = 0;
var takeTablet = function() {
takenTablets += 1;
}
if (scope.control) {
scope.control = {
takeTablet: takeTablet
};
}
}
};
});
Solution 5
To be honest, I was not really convinced with any of the answers in this thread. So, here's are my solutions:
Directive Handler(Manager) Approach
This method is agnostic to whether the directive's $scope
is a shared one or isolated one
A factory
to register the directive instances
angular.module('myModule').factory('MyDirectiveHandler', function() {
var instance_map = {};
var service = {
registerDirective: registerDirective,
getDirective: getDirective,
deregisterDirective: deregisterDirective
};
return service;
function registerDirective(name, ctrl) {
instance_map[name] = ctrl;
}
function getDirective(name) {
return instance_map[name];
}
function deregisterDirective(name) {
instance_map[name] = null;
}
});
The directive code, I usually put all the logic that doesn't deal with DOM inside directive controller. And registering the controller instance inside our handler
angular.module('myModule').directive('myDirective', function(MyDirectiveHandler) {
var directive = {
link: link,
controller: controller
};
return directive;
function link() {
//link fn code
}
function controller($scope, $attrs) {
var name = $attrs.name;
this.updateMap = function() {
//some code
};
MyDirectiveHandler.registerDirective(name, this);
$scope.$on('destroy', function() {
MyDirectiveHandler.deregisterDirective(name);
});
}
})
template code
<div my-directive name="foo"></div>
Access the controller instance using the factory
& run the publicly exposed methods
angular.module('myModule').controller('MyController', function(MyDirectiveHandler, $scope) {
$scope.someFn = function() {
MyDirectiveHandler.get('foo').updateMap();
};
});
Angular's approach
Taking a leaf out of angular's book on how they deal with
<form name="my_form"></form>
using $parse and registering controller on $parent
scope. This technique doesn't work on isolated $scope
directives.
angular.module('myModule').directive('myDirective', function($parse) {
var directive = {
link: link,
controller: controller,
scope: true
};
return directive;
function link() {
//link fn code
}
function controller($scope, $attrs) {
$parse($attrs.name).assign($scope.$parent, this);
this.updateMap = function() {
//some code
};
}
})
Access it inside controller using $scope.foo
angular.module('myModule').controller('MyController', function($scope) {
$scope.someFn = function() {
$scope.foo.updateMap();
};
});
Related videos on Youtube
Comments
-
mcbjam about 4 years
I have a directive, here is the code :
.directive('map', function() { return { restrict: 'E', replace: true, template: '<div></div>', link: function($scope, element, attrs) { var center = new google.maps.LatLng(50.1, 14.4); $scope.map_options = { zoom: 14, center: center, mapTypeId: google.maps.MapTypeId.ROADMAP }; // create map var map = new google.maps.Map(document.getElementById(attrs.id), $scope.map_options); var dirService= new google.maps.DirectionsService(); var dirRenderer= new google.maps.DirectionsRenderer() var showDirections = function(dirResult, dirStatus) { if (dirStatus != google.maps.DirectionsStatus.OK) { alert('Directions failed: ' + dirStatus); return; } // Show directions dirRenderer.setMap(map); //$scope.dirRenderer.setPanel(Demo.dirContainer); dirRenderer.setDirections(dirResult); }; // Watch var updateMap = function(){ dirService.route($scope.dirRequest, showDirections); }; $scope.$watch('dirRequest.origin', updateMap); google.maps.event.addListener(map, 'zoom_changed', function() { $scope.map_options.zoom = map.getZoom(); }); dirService.route($scope.dirRequest, showDirections); } } })
I would like to call
updateMap()
on a user action. The action button is not on the directive.What is the best way to call
updateMap()
from a controller?-
Noam about 9 yearsSmall side note: the convention is not to use the dollar sign for 'scope' in a link function, as the scope is not injected but passed in as a regular argument.
-
-
Florian F about 11 yearsWhat if the directive has an isolated scope ?
-
Florian F about 11 yearsThanks! (Maybe it would be easier to call a function defined in the directive's controller but I'm not sure about that)
-
romiem almost 11 years+1 This is also how I create APIs for my reusable components in Angular.
-
Blake Miller over 10 yearsThis is cleaner than the accepted answer, and +1 for the simpsons reference, if I'm not mistaken
-
Dema over 10 yearsThat's exatcly how I solved the same problem. It works, but it looks like a hack... I wish angular had a better solution for this.
-
BLSully over 10 yearsI'm learning angular, so my opinion may not hold much weight, but I found this approach much more intuitive than the other answer and would have marked it the correct answer. I implemented this in my sandbox application with zero trouble.
-
CheapSteaks over 10 yearsYou should probably do a check to make sure
scope.control
exists, otherwise other places that use the directive but don't need to access the directive's methods and don't have acontrol
attr will start throwing errors about not being able to set attributes onundefined
-
Oliver Wienand over 10 yearsUpdated the example to also work if no external control object is given. Thanks to CheapSteaks.
-
Cabes234 almost 10 yearsI don't understand why this works.. is it because the clear attribute is in scope some how?
-
Trevor almost 10 yearsIt becomes a part of the directive's scope as soon as you declare it (e.g.
scope: { clearFn: '=clearfn' }
). -
VitalyB almost 10 yearsI like this solution a lot. My only gripe with it is the need to create an empty object before calling a directive:
$scope.focusinControl = { };
that just looks wrong. -
Egel about 9 yearsThis hack works well only for calling a function without any parameters. See an example with passing a single parameter - example plunk.
-
Oliver Wienand about 9 yearsThe hack also works with parameters. You have to put them on the $scope in order to be accessible, e.g.
$scope.num = 4
instead ofnum = 4
. -
JSancho about 9 yearsspot on, using a revealing pattern inside the directive makes the intentions far clearer. nice one!
-
GentryRiggen about 9 yearsIs anyone finding that the method you place on the control is not defined right away? If you wrap the external call to the internal method with a $timeout of only 100ms it works, but I don't like that solution... Anyone else?
-
Oliver Wienand about 9 years@GentryRiggen: I guess you want to call the directive method during construction of the scope? In this case I would suggest to change the directive that it takes whatever you want to communicate to the directive with the method as an separate argument. (Haven't deeply read it, but maybe also check groups.google.com/forum/#!topic/angular/4rbZi97QCGc)
-
GentryRiggen about 9 years@OliverWienand I found out that I was getting undefined methods when doing an ng-if in the HTML around the directive. As soon as I removed it, everything was good to. Thanks for the reply!
-
Mikhail Batcer almost 9 yearsThe question was, "What is the best way to call updateMap() from a controller?". In this answer, I don't see any function calls in the controller. Could someone please tell me where it is?
-
stanleyxu2005 almost 9 yearsI reached to two solutions intuitively: (1) watch the change of a scope variable
=
, the variable contains method name and arguments. (2) expose a one-way-bind string@
as topic id and let callee send event on this topic. Now I saw the best practice wiki. I think there is reason not to do it in may way. But I'm still not very clear, how it works. In my case, I created a tabset directive, I want to expose aswitchTab(tabIndex)
method. Could you example more? -
Oliver Wienand almost 9 years@MikhailBatcer: The call to
focusinControl.takeTablet()
withinng-click
is more or less within the controller. You can also call$scope.focusinControl.takeTablet()
anywhere within the controller itself. I put it on ng-click just for brevity and to avoid unnecessary stubs. -
Mikhail Batcer almost 9 years@OliverWienand OK, I see now, thank you. But I'm not sure in one more thing: what is the magical way by which
$scope.focusinControl
references the same object, as doesscope.internalControl
inside the directive? Is it by attributecontrol="focusinControl"
in directive's<focusin>
tag? -
Oliver Wienand almost 9 years@MikhailBatcer yes. The directive gets a new reference to the object referenced by $scope.focusinControl and hence the method put on the object by the directive is also accessible from the original reference. The line
scope.internalControl = scope.control || {};
is where the magic happens ;) -
Jan Peša over 8 yearsThis proposal would need a bit more work in a real world when you have multiple directives of same type depending on your API service. You will sure get in a situation where you need to target and call functions from only one specific directive and not all of them. Would you like to enhance your answer with a solution to this?
-
Martin Frank about 8 yearsThis is much better way if you are not dealing with an isolated scope.
-
Daniel D almost 8 years"Angular's approach" looks great! There's a typo though:
$scope.foo
should be$scope.my_form
-
Mudassir Ali almost 8 yearsNah, it would be
$scope.foo
since our template is<div my-directive name="foo"></div>
andname
attribute's value is 'foo'.<form
is just an example of one of the angular's directive which employs this technique -
CodeGems almost 8 yearsthanks a lot. I was looking for this. I needed this as the last piece of a puzzle to start creating reusable angular components.
-
Suamere over 7 yearsYou wouldn't expose a
switchTab(tabIndex)
method, you would only bind to atabIndex
variable. Your page controller may have actions that change that variable. You bind/pass that variable into your tab Directive. Your tab Directive can then watch that variable for changes, and perform switchTab of its own accord. Because the directive decides when/how to control its tabs based on a variable. That isn't the job of an external source, otherwise the external sources requires knowledge of the inner workings of the directive, which is bad m'kay. -
Alburkerk about 7 yearsHow can you be sure the directive have already set the function attribute before calling the function from the controller ?
-
Oliver Wienand about 7 years@Alburkerk: Did you check my reply to @GentryRiggen?
-
Alburkerk about 7 years@OliverWienand Yes but I didn't want to use a controller especially for that directive. What I did was simply put the binding in the pre link function and it worked. Thank you for your reply !
-
Daniel G. almost 7 yearsThis answer does actually answers the OP question. It does also use isolated scope, in order to have an isolated scope you only need to add the
scope
property into the directive declaration. -
Alexander Mills over 6 yearsthe problem for me (I am on Angular 1.6) is that
scope.internalControl.takeTablet
is not being set before it gets called. -
Oliver Wienand over 6 years@AlexanderMills: I just tested it in this plunk plnkr.co/edit/L4YTTt?p=preview and it seems fine with 1.6.5.
-
Alexander Mills over 6 years@OliverWienand thanks yeah I am sure there is something different about the code I am using
-
Alexander Mills over 6 years@OliverWienand I figured it out - when I used restrict: "EA", link() does not get called. When I used restrict "E", then link() gets called. Unexpected.