Comment appeler une méthode définie dans une directive AngularJS?


297

J'ai une directive, voici le 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);  
        }
    }
})

Je voudrais appeler updateMap()une action utilisateur. Le bouton d'action n'est pas sur la directive.

Quelle est la meilleure façon d'appeler à updateMap()partir d'un contrôleur?


11
Petite remarque: la convention ne consiste pas à utiliser le signe dollar pour «portée» dans une fonction de lien, car la portée n'est pas injectée mais transmise comme argument régulier.
Noam

Réponses:


369

Si vous souhaitez utiliser des étendues isolées, vous pouvez passer un objet de contrôle à l'aide de la liaison bidirectionnelle =d'une variable de la portée du contrôleur. Vous pouvez également contrôler plusieurs instances de la même directive sur une page avec le même objet de contrôle.

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>


11
+1 C'est aussi ainsi que je crée des API pour mes composants réutilisables dans Angular.
romiem

5
C'est plus propre que la réponse acceptée, et +1 pour la référence aux simpsons, si je ne me trompe pas
Blake Miller

44
C'est exactement comme ça que j'ai résolu le même problème. Cela fonctionne, mais cela ressemble à un hack ... Je souhaite que angular ait une meilleure solution pour cela.
Dema

1
J'apprends angulairement, donc mon opinion n'a peut-être pas beaucoup de poids, mais j'ai trouvé cette approche beaucoup plus intuitive que l'autre réponse et l'aurais marquée comme la bonne réponse. J'ai implémenté cela dans mon application sandbox sans aucun problème.
BLSully

4
Vous devriez probablement faire une vérification pour vous assurer qu'il scope.controlexiste, sinon d'autres endroits qui utilisent la directive mais n'ont pas besoin d'accéder aux méthodes de la directive et n'ont pas d' controlattr commenceront à générer des erreurs de ne pas pouvoir définir d'attributs surundefined
CheapSteaks

73

En supposant que le bouton d'action utilise le même contrôleur $scopeque la directive, définissez simplement la fonction updateMapà l' $scopeintérieur de la fonction de liaison. Votre contrôleur peut alors appeler cette fonction lorsque vous cliquez sur le bouton d'action.

<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


Selon le commentaire de @ FlorianF, si la directive utilise une portée isolée, les choses sont plus compliquées. Voici une façon de le faire fonctionner: ajoutez un set-fnattribut à la mapdirective qui enregistrera la fonction directive auprès du contrôleur:

<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


Et si la directive a un champ d'application isolé?
Florian F

Merci! (Peut-être qu'il serait plus facile d'appeler une fonction définie dans le contrôleur de la directive mais je n'en suis pas sûr)
Florian F

1
C'est une bien meilleure façon si vous ne traitez pas avec une portée isolée.
Martin Frank

Cette réponse répond en fait à la question OP. Il utilise également une portée isolée, pour avoir une portée isolée, il vous suffit d'ajouter la scopepropriété dans la déclaration de directive.
Daniel G.20

35

Bien qu'il puisse être tentant d'exposer un objet sur la portée isolée d'une directive pour faciliter la communication avec lui, cela peut conduire à confondre le code "spaghetti", surtout si vous avez besoin de chaîner cette communication à travers plusieurs niveaux (contrôleur, à directive, à la directive imbriquée, etc.)

À l'origine, nous avons emprunté cette voie, mais après quelques recherches supplémentaires, cela a semblé plus logique et a abouti à un code à la fois plus maintenable et plus lisible pour exposer les événements et les propriétés qu'une directive utilisera pour la communication via un service, puis en utilisant $ watch sur les propriétés de ce service dans la directive ou tout autre contrôle qui devrait réagir à ces changements de communication.

Cette abstraction fonctionne très bien avec le cadre d'injection de dépendance d'AngularJS car vous pouvez injecter le service dans tous les éléments qui doivent réagir à ces événements. Si vous regardez le fichier Angular.js, vous verrez que les directives qui y figurent utilisent également les services et $ watch de cette manière, elles n'exposent pas les événements sur la portée isolée.

Enfin, dans le cas où vous devez communiquer entre des directives qui dépendent les unes des autres, je recommanderais de partager un contrôleur entre ces directives comme moyen de communication.

Le Wiki for Best Practices d'AngularJS mentionne également ceci:

Utilisez uniquement. $ Broadcast (),. $ Emit () et. $ On () pour les événements atomiques Les événements qui sont pertinents à l'échelle mondiale dans l'ensemble de l'application (comme l'authentification d'un utilisateur ou la fermeture de l'application). Si vous souhaitez des événements spécifiques aux modules, services ou widgets, vous devez envisager les services, les contrôleurs de directive ou les bibliothèques tierces

  • $ scope. $ watch () devrait remplacer le besoin d'événements
  • L'injection directe de services et de méthodes d'appel est également utile pour la communication directe
  • Les directives peuvent communiquer directement entre elles par le biais de contrôleurs de directives

2
J'ai atteint intuitivement deux solutions: (1) observer le changement d'une variable de portée =, la variable contient le nom de la méthode et les arguments. (2) exposer une chaîne de liaison unidirectionnelle en @tant qu'ID de sujet et laisser l'appelé envoyer un événement sur ce sujet. Maintenant, j'ai vu le wiki des meilleures pratiques. Je pense qu'il y a une raison de ne pas le faire de la même manière. Mais je ne sais toujours pas très bien comment cela fonctionne. Dans mon cas, j'ai créé une directive tabset, je veux exposer une switchTab(tabIndex)méthode. Pourriez-vous donner plus d'exemples?
stanleyxu2005

Vous n'exposeriez pas une switchTab(tabIndex)méthode, vous ne vous lieriez qu'à une tabIndexvariable. Votre contrôleur de page peut avoir des actions qui modifient cette variable. Vous liez / transmettez cette variable dans votre onglet Directive. Votre onglet Directive peut alors surveiller les modifications de cette variable et exécuter switchTab de son propre gré. Parce que la directive décide quand / comment contrôler ses onglets en fonction d'une variable. Ce n'est pas le travail d'une source externe, sinon les sources externes nécessitent une connaissance du fonctionnement interne de la directive, ce qui est mauvais m'kay.
Suamere

15

S'appuyant sur la réponse d'Oliver - vous n'aurez peut-être pas toujours besoin d'accéder aux méthodes internes d'une directive, et dans ces cas, vous ne voudrez probablement pas avoir à créer un objet vide et ajouter un controlattr à la directive juste pour l'empêcher de générer une erreur ( cannot set property 'takeTablet' of undefined).

Vous pouvez également souhaiter utiliser la méthode à d'autres endroits de la directive.

J'ajouterais une vérification pour m'assurer qu'il scope.controlexiste, et je définirais des méthodes similaires au modèle de module révélateur

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

sur place, l'utilisation d'un modèle révélateur à l'intérieur de la directive rend les intentions beaucoup plus claires. joli!
JSancho

12

Pour être honnête, je n'ai été vraiment convaincu par aucune des réponses de ce fil. Voici donc mes solutions:

Approche du gestionnaire de directives (gestionnaire)

Cette méthode est indépendante de si la directive $scopeest partagée ou isolée

A factorypour enregistrer les instances de directive

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

Le code de directive, je mets généralement toute la logique qui ne traite pas du DOM à l'intérieur du contrôleur de directive. Et l'enregistrement de l'instance de contrôleur dans notre gestionnaire

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

code modèle

<div my-directive name="foo"></div>

Accédez à l'instance de contrôleur à l'aide de factory& exécutez les méthodes exposées publiquement

angular.module('myModule').controller('MyController', function(MyDirectiveHandler, $scope) {
    $scope.someFn = function() {
        MyDirectiveHandler.get('foo').updateMap();
    };
});

L'approche d'Angular

Prendre une feuille du livre d'Angular sur la façon dont ils gèrent

<form name="my_form"></form>

en utilisant $ parse et en enregistrant le contrôleur sur la $parentportée. Cette technique ne fonctionne pas sur des $scopedirectives isolées .

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

Accédez-y à l'intérieur du contrôleur en utilisant $scope.foo

angular.module('myModule').controller('MyController', function($scope) {
    $scope.someFn = function() {
        $scope.foo.updateMap();
    };
});

"L'approche angulaire" a fière allure! Il y a cependant une faute de frappe: $scope.foodevrait être$scope.my_form
Daniel D

Non, ce serait $scope.foopuisque notre modèle est <div my-directive name="foo"></div>et namela valeur de l'attribut est 'foo'. <formest juste un exemple de l'une des directives angulaires qui utilise cette technique
Mudassir Ali

10

Un peu tard, mais c'est une solution avec la portée isolée et les "événements" pour appeler une fonction dans la directive. Cette solution est inspirée de ce post SO de satchmorun et ajoute un module et une API.

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

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

Créez une API pour communiquer avec la directive. AddUpdateEvent ajoute un événement au tableau d'événements et updateMap appelle chaque fonction d'événement.

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

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

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

(Vous devrez peut-être ajouter des fonctionnalités pour supprimer l'événement.)

Dans la directive, définissez une référence à MapAPI et ajoutez $ scope.updateMap en tant qu'événement lorsque MapApi.updateMap est appelé.

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

        }
    }
});

Dans le contrôleur "principal", ajoutez une référence à MapApi et appelez simplement MapApi.updateMap () pour mettre à jour la carte.

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

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

2
Cette proposition nécessiterait un peu plus de travail dans un monde réel lorsque vous avez plusieurs directives du même type en fonction de votre service API. Vous vous retrouverez certainement dans une situation où vous devez cibler et appeler des fonctions à partir d'une seule directive spécifique et non de toutes. Souhaitez-vous améliorer votre réponse avec une solution à cela?
smajl

5

Vous pouvez spécifier un attribut DOM qui peut être utilisé pour permettre à la directive de définir une fonction sur la portée parent. La portée parent peut alors appeler cette méthode comme n'importe quelle autre. Voici un plongeur. Et ci-dessous est le code correspondant.

clearfn est un attribut sur l'élément de directive dans lequel la portée parent peut passer une propriété de portée que la directive peut ensuite définir sur une fonction qui accomplit le comportement souhaité.

<!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>

Je ne comprends pas pourquoi cela fonctionne .. est-ce parce que l'attribut clear est dans la portée quelque part?
Quinn Wilson

1
Elle fait partie du champ d'application de la directive dès que vous la déclarez (par exemple scope: { clearFn: '=clearfn' }).
Trevor

2

Utilisez simplement scope. $ Parent pour associer la fonction appelée à la fonction directive

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

en HTML

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

2

Vous pouvez indiquer le nom de la méthode à la directive pour définir celui que vous souhaitez appeler à partir du contrôleur, mais sans isoler la portée,

angular.module("app", [])
  .directive("palyer", [
    function() {
      return {
        restrict: "A",
        template:'<div class="player"><span ng-bind="text"></span></div>',
        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;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
  <div ng-controller="playerController">
    <p>Click play button to play
      <p>
        <p palyer="" to-play="play"></p>
        <button ng-click="clickPlay()">Play</button>

  </div>
</div>


1

TESTÉ J'espère que cela aide quelqu'un.

Mon approche simple (pensez aux balises comme votre code d'origine)

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

0

Ce n'est peut-être pas le meilleur choix, mais vous pouvez le faire angular.element("#element").isolateScope()ou $("#element").isolateScope()accéder à la portée et / ou au contrôleur de votre directive.


0

Comment obtenir le contrôleur d'une directive dans un contrôleur de page:

  1. écrire une directive personnalisée pour obtenir la référence au contrôleur de directive à partir de l'élément DOM:

    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);
        }
    }
  2. utilisez-le dans le html du contrôleur de page:

    <my-directive controller="vm.myDirectiveController"></my-directive>
  3. Utilisez le contrôleur de directive dans le contrôleur de page:

    vm.myDirectiveController.callSomeMethod();

Remarque: la solution donnée ne fonctionne que pour les contrôleurs des directives d'élément (le nom de balise est utilisé pour obtenir le nom de la directive souhaitée).


0

La solution ci-dessous sera utile lorsque vous avez des contrôleurs (parent et directive (isolés)) au format 'contrôleur en tant que'

quelqu'un pourrait trouver cela utile,

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

Contrôleur directif:

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

function SetFitlersToDefaultValue() {
           //your logic
        }
}

Code HTML :

      <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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.