ChatGPT解决这个技术问题 Extra ChatGPT

How to call a method defined in an AngularJS directive?

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?

Small 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.

f
fracz

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: '

A:{{internalControl}}
', scope: { control: '=' }, link: function(scope, element, attrs) { scope.internalControl = scope.control || {}; scope.internalControl.takenTablets = 0; scope.internalControl.takeTablet = function() { scope.internalControl.takenTablets += 1; } } }; });

In controller scope: {{focusinControl}}

In directive scope:

Without control object:


+1 This is also how I create APIs for my reusable components in Angular.
This is cleaner than the accepted answer, and +1 for the simpsons reference, if I'm not mistaken
That'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.
I'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.
You 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 a control attr will start throwing errors about not being able to set attributes on undefined
M
Mark Rajcok

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()');
            }
        }
    }
});

fiddle

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;
    };
}

fiddle


What if the directive has an isolated scope ?
Thanks! (Maybe it would be easier to call a function defined in the directive's controller but I'm not sure about that)
This is much better way if you are not dealing with an isolated scope.
This 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.
A
Always Learning

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


I 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 a switchTab(tabIndex) method. Could you example more?
You wouldn't expose a switchTab(tabIndex) method, you would only bind to a tabIndex 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.
C
CheapSteaks

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
        };
      }
    }
  };
});

spot on, using a revealing pattern inside the directive makes the intentions far clearer. nice one!
M
Mudassir Ali

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();
    };
});

"Angular's approach" looks great! There's a typo though: $scope.foo should be $scope.my_form
Nah, it would be $scope.foo since our template is <div my-directive name="foo"></div> and name attribute's value is 'foo'. <form is just an example of one of the angular's directive which employs this technique
F
Farzad Karimi

A bit late, but this is a solution with the isolated scope and "events" to call a function in the directive. This solution is inspired by this SO post by satchmorun and adds a module and an API.

//Create module
var MapModule = angular.module('MapModule', []);

//Load dependency dynamically
angular.module('app').requires.push('MapModule');

Create an API to communicate with the directive. The addUpdateEvent adds an event to the event array and updateMap calls every event function.

MapModule.factory('MapApi', function () {
    return {
        events: [],

        addUpdateEvent: function (func) {
            this.events.push(func);
        },

        updateMap: function () {
            this.events.forEach(function (func) {
                func.call();
            });
        }
    }
});

(Maybe you have to add functionality to remove event.)

In the directive set a reference to the MapAPI and add $scope.updateMap as an event when MapApi.updateMap is called.

app.directive('map', function () {
    return {
        restrict: 'E', 
        scope: {}, 
        templateUrl: '....',
        controller: function ($scope, $http, $attrs, MapApi) {

            $scope.api = MapApi;

            $scope.updateMap = function () {
                //Update the map 
            };

            //Add event
            $scope.api.addUpdateEvent($scope.updateMap);

        }
    }
});

In the "main" controller add a reference to the MapApi and just call MapApi.updateMap() to update the map.

app.controller('mainController', function ($scope, MapApi) {

    $scope.updateMapButtonClick = function() {
        MapApi.updateMap();    
    };
}

This 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?
T
Trevor

You can specify a DOM attribute that can be used to allow the directive to define a function on the parent scope. The parent scope can then call this method like any other. Here's a plunker. And below is the relevant code.

clearfn is an attribute on the directive element into which the parent scope can pass a scope property which the directive can then set to a function that accomplish's the desired behavior.

<!DOCTYPE html>
<html ng-app="myapp">
  <head>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <style>
      my-box{
        display:block;
        border:solid 1px #aaa;
        min-width:50px;
        min-height:50px;
        padding:.5em;
        margin:1em;
        outline:0px;
        box-shadow:inset 0px 0px .4em #aaa;
      }
    </style>
  </head>
  <body ng-controller="mycontroller">
    <h1>Call method on directive</h1>
    <button ng-click="clear()">Clear</button>
    <my-box clearfn="clear" contentEditable=true></my-box>
    <script>
      var app = angular.module('myapp', []);
      app.controller('mycontroller', function($scope){
      });
      app.directive('myBox', function(){
        return {
          restrict: 'E',
          scope: {
            clearFn: '=clearfn'
          },
          template: '',
          link: function(scope, element, attrs){
            element.html('Hello World!');
            scope.clearFn = function(){
              element.html('');
            };
          }
        }
      });
    </script>
  </body>
</html>

I don't understand why this works.. is it because the clear attribute is in scope some how?
It becomes a part of the directive's scope as soon as you declare it (e.g. scope: { clearFn: '=clearfn' }).
r
ramon prata

Just use scope.$parent to associate function called to directive function

angular.module('myApp', [])
.controller('MyCtrl',['$scope',function($scope) {

}])
.directive('mydirective',function(){
 function link(scope, el, attr){
   //use scope.$parent to associate the function called to directive function
   scope.$parent.myfunction = function directivefunction(parameter){
     //do something
}
}
return {
        link: link,
        restrict: 'E'   
      };
});

in HTML

<div ng-controller="MyCtrl">
    <mydirective></mydirective>
    <button ng-click="myfunction(parameter)">call()</button>
</div>

N
Naveen raj

You can tell the method name to directive to define which you want to call from controller but without isolate scope,

angular.module("app", []) .directive("palyer", [ function() { return { restrict: "A", template:'

', link: function($scope, element, attr) { if (attr.toPlay) { $scope[attr.toPlay] = function(name) { $scope.text = name + " playing..."; } } } }; } ]) .controller("playerController", ["$scope", function($scope) { $scope.clickPlay = function() { $scope.play('AR Song'); }; } ]); .player{ border:1px solid; padding: 10px; }

Click play button to play


S
Santosh Kumar

TESTED Hope this helps someone.

My simple approach (Think tags as your original code)

<html>
<div ng-click="myfuncion"> 
<my-dir callfunction="myfunction">
</html>

<directive "my-dir">
callfunction:"=callfunction"
link : function(scope,element,attr) {
scope.callfunction = function() {
 /// your code
}
}
</directive>

A
António Ribeiro

Maybe this is not the best choice, but you can do angular.element("#element").isolateScope() or $("#element").isolateScope() to access the scope and/or the controller of your directive.


R
Robert J

How to get a directive's controller in a page controller:

write a custom directive to get the reference to the directive controller from the DOM element: angular.module('myApp') .directive('controller', controller); controller.$inject = ['$parse']; function controller($parse) { var directive = { restrict: 'A', link: linkFunction }; return directive; function linkFunction(scope, el, attrs) { var directiveName = attrs.$normalize(el.prop("tagName").toLowerCase()); var directiveController = el.controller(directiveName); var model = $parse(attrs.controller); model.assign(scope, directiveController); } } use it in the page controller's html: Use the directive controller in the page controller: vm.myDirectiveController.callSomeMethod();

Note: the given solution works only for element directives' controllers (tag name is used to get the name of the wanted directive).


R
Raunak Mali

Below solution will be useful when, you are having controllers (both parent and directive (isolated)) in 'controller As' format

someone might find this useful,

directive :

var directive = {
        link: link,
        restrict: 'E',
        replace: true,
        scope: {
            clearFilters: '='
        },
        templateUrl: "/temp.html",
        bindToController: true, 
        controller: ProjectCustomAttributesController,
        controllerAs: 'vmd'
    };
    return directive;

    function link(scope, element, attrs) {
        scope.vmd.clearFilters = scope.vmd.SetFitlersToDefaultValue;
    }
}

directive Controller :

function DirectiveController($location, dbConnection, uiUtility) {
  vmd.SetFitlersToDefaultValue = SetFitlersToDefaultValue;

function SetFitlersToDefaultValue() {
           //your logic
        }
}

html code :

      <Test-directive clear-filters="vm.ClearFilters"></Test-directive>
    <a class="pull-right" style="cursor: pointer" ng-click="vm.ClearFilters()"><u>Clear</u></a> 
//this button is from parent controller which will call directive controller function