Come chiamare un metodo definito in una direttiva AngularJS?


297

Ho una direttiva, ecco il codice:

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

Vorrei chiedere updateMap()un'azione da parte dell'utente. Il pulsante di azione non si trova sulla direttiva.

Qual è il modo migliore per chiamare updateMap()da un controller?


11
Piccola nota a margine: la convenzione non prevede l'uso del simbolo del dollaro per "ambito" in una funzione di collegamento, poiché l'ambito non viene iniettato ma passato come argomento regolare.
Noam,

Risposte:


369

Se si desidera utilizzare ambiti isolati, è possibile passare un oggetto di controllo utilizzando l'associazione bidirezionale =di una variabile dall'ambito del controller. Puoi anche controllare diverse istanze della stessa direttiva in una pagina con lo stesso oggetto di controllo.

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 Questo è anche il modo in cui creo le API per i miei componenti riutilizzabili in Angular.
rom

5
Questo è più pulito della risposta accettata e +1 per il riferimento dei simpson, se non sbaglio
Blake Miller

44
È esattamente come ho risolto lo stesso problema. Funziona, ma sembra un trucco ... Vorrei che Angular avesse una soluzione migliore per questo.
Dema

1
Sto imparando l'angolazione, quindi la mia opinione potrebbe non avere molto peso, ma ho trovato questo approccio molto più intuitivo rispetto all'altra risposta e l'avrei contrassegnata come la risposta corretta. Ho implementato questo nella mia applicazione sandbox senza problemi.
BLSully,

4
Probabilmente dovresti fare un controllo per assicurarti che scope.controlesista, altrimenti altri luoghi che usano la direttiva ma non hanno bisogno di accedere ai metodi della direttiva e non hanno un controlattr inizieranno a lanciare errori per non essere in grado di impostare gli attributiundefined
CheapSteaks

73

Supponendo che il pulsante di azione utilizza lo stesso controller $scope, come la direttiva, basta definire la funzione updateMapsul $scopeall'interno della funzione di collegamento. Il controller può quindi chiamare quella funzione quando si fa clic sul pulsante di azione.

<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


Secondo il commento di @ FlorianF, se la direttiva utilizza un ambito isolato, le cose sono più complicate. Ecco un modo per farlo funzionare: aggiungi un set-fnattributo alla mapdirettiva che registrerà la funzione direttiva con il 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


Cosa succede se la direttiva ha un campo di applicazione isolato?
Florian F,

Grazie! (Forse sarebbe più facile chiamare una funzione definita nel controller della direttiva ma non ne sono sicuro)
Florian F

1
Questo è molto meglio se non hai a che fare con un ambito isolato.
Martin Frank,

Questa risposta risponde effettivamente alla domanda OP. Utilizza anche un ambito isolato, per avere un ambito isolato è sufficiente aggiungere la scopeproprietà nella dichiarazione della direttiva.
Daniel G.

35

Sebbene possa essere allettante esporre un oggetto nell'ambito isolato di una direttiva per facilitare la comunicazione con esso, fare ciò può portare a confondere il codice "spaghetti", soprattutto se è necessario concatenare questa comunicazione attraverso un paio di livelli (controller, direttiva, alla direttiva nidificata, ecc.)

Inizialmente abbiamo intrapreso questo percorso, ma dopo alcune ulteriori ricerche abbiamo scoperto che aveva più senso e ha prodotto codice più gestibile e leggibile per esporre eventi e proprietà che una direttiva utilizzerà per la comunicazione tramite un servizio, quindi utilizzando $ watch sulle proprietà di quel servizio in la direttiva o qualsiasi altro controllo che dovrebbe reagire a tali modifiche per la comunicazione.

Questa astrazione funziona molto bene con il framework di iniezione delle dipendenze di AngularJS in quanto è possibile iniettare il servizio in tutti gli elementi che devono reagire a quegli eventi. Se guardi il file Angular.js, vedrai che le direttive ivi contenute usano anche i servizi e $ watch in questo modo, non espongono eventi sopra l'ambito isolato.

Infine, nel caso in cui sia necessario comunicare tra direttive che dipendono l'una dall'altra, raccomanderei di condividere un controllore tra tali direttive come mezzo di comunicazione.

Il Wiki di Best Practices di AngularJS menziona anche questo:

Utilizzare solo. $ Broadcast (),. $ Emit () e. $ On () per eventi atomici Eventi rilevanti a livello globale nell'intera app (come un utente che esegue l'autenticazione o la chiusura dell'app). Se desideri eventi specifici per moduli, servizi o widget, dovresti prendere in considerazione Servizi, Regolatori di direttiva o Lib di terze parti

  • $ scope. $ watch () dovrebbe sostituire la necessità di eventi
  • L'iniezione diretta di servizi e metodi di chiamata è utile anche per la comunicazione diretta
  • Le direttive sono in grado di comunicare direttamente tra loro tramite i controllori delle direttive

2
Ho raggiunto intuitivamente due soluzioni: (1) guarda il cambiamento di una variabile ambito =, la variabile contiene il nome del metodo e gli argomenti. (2) esporre una stringa di @associazione unidirezionale come ID argomento e consentire a callee di inviare un evento su questo argomento. Ora ho visto il wiki delle migliori pratiche. Penso che ci sia motivo di non farlo in qualche modo. Ma non sono ancora molto chiaro su come funzioni. Nel mio caso, ho creato una direttiva tabset, voglio esporre un switchTab(tabIndex)metodo. Potresti fare un esempio in più?
Stanleyxu2005,

Non esporrai un switchTab(tabIndex)metodo, ti legheresti solo a una tabIndexvariabile. Il tuo controller di pagina potrebbe avere azioni che cambiano quella variabile. Leghi / passi quella variabile nella tua scheda tab. La tua scheda tab può quindi guardare quella variabile per le modifiche ed eseguire switchTab da solo. Perché la direttiva decide quando / come controllare le sue schede in base a una variabile. Questo non è il lavoro di una fonte esterna, altrimenti le fonti esterne richiedono la conoscenza del funzionamento interno della direttiva, il che è un peccato.
Suamere,

15

Basandosi sulla risposta di Oliver, potrebbe non essere sempre necessario accedere ai metodi interni di una direttiva e, in quei casi, probabilmente non è necessario creare un oggetto vuoto e aggiungere un controlattr alla direttiva solo per evitare che generi un errore ( cannot set property 'takeTablet' of undefined).

Potresti anche voler usare il metodo in altri punti della direttiva.

Aggiungerei un segno di spunta per accertarmi che scope.controlesista e imposti i metodi in modo simile al modello di modulo rivelatore

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

esatto, l'uso di un modello rivelatore all'interno della direttiva rende le intenzioni molto più chiare. Ben fatto!
JSancho,

12

Ad essere sincero, non ero davvero convinto con nessuna delle risposte in questo thread. Quindi, ecco le mie soluzioni:

Approccio gestore direttiva (Manager)

Questo metodo è indipendente dal fatto che la direttiva $scopesia condivisa o isolata

A factoryper registrare le istanze della direttiva

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

Il codice della direttiva, di solito metto tutta la logica che non si occupa del DOM all'interno del controller della direttiva. E registrando l'istanza del controller all'interno del nostro gestore

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

codice modello

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

Accedi all'istanza del controller usando factory& esegui i metodi esposti pubblicamente

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

L'approccio di Angular

Estrarre una foglia dal libro di Angular su come affrontano

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

utilizzando $ parse e registrando il controller $parentnell'ambito. Questa tecnica non funziona su $scopedirettive isolate .

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

Accedilo all'interno del controller usando $scope.foo

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

"L'approccio di Angular" sembra fantastico! C'è un errore di battitura però: $scope.foodovrebbe essere$scope.my_form
Daniel D

No, lo sarebbe $scope.foodal momento che il nostro modello è <div my-directive name="foo"></div>e nameil valore dell'attributo è "pippo". <formè solo un esempio di una delle direttive dell'angular che utilizza questa tecnica
Mudassir Ali,

10

Un po 'in ritardo, ma questa è una soluzione con ambito isolato ed "eventi" per chiamare una funzione nella direttiva. Questa soluzione è ispirata a questo post SO di satchmorun e aggiunge un modulo e un'API.

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

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

Crea un'API per comunicare con la direttiva. AddUpdateEvent aggiunge un evento all'array di eventi e updateMap chiama ogni funzione di evento.

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

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

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

(Forse devi aggiungere funzionalità per rimuovere l'evento.)

Nella direttiva impostare un riferimento a MapAPI e aggiungere $ scope.updateMap come evento quando viene chiamato MapApi.updateMap.

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

        }
    }
});

Nel controller "principale" aggiungi un riferimento a MapApi e chiama MapApi.updateMap () per aggiornare la mappa.

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

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

2
Questa proposta avrebbe bisogno di un po 'più di lavoro in un mondo reale quando hai più direttive dello stesso tipo a seconda del tuo servizio API. Ti troverai sicuramente in una situazione in cui devi indirizzare e chiamare funzioni da una sola direttiva specifica e non tutte. Vorresti migliorare la tua risposta con una soluzione a questo?
Smajl

5

È possibile specificare un attributo DOM che può essere utilizzato per consentire alla direttiva di definire una funzione nell'ambito genitore. L'ambito padre può quindi chiamare questo metodo come qualsiasi altro. Ecco un plunker. E di seguito è riportato il codice pertinente.

clearfn è un attributo sull'elemento direttiva in cui l'ambito padre può passare una proprietà ambito che la direttiva può quindi impostare su una funzione che compie il comportamento desiderato.

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

Non capisco perché questo funzioni ... è perché l'attributo clear rientra nell'ambito in qualche modo?
Quinn Wilson,

1
Diventa parte del campo di applicazione della direttiva non appena lo dichiari (ad es scope: { clearFn: '=clearfn' }.).
Trevor,

2

Usa solo scope. $ Parent per associare la funzione chiamata alla funzione direttiva

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>

2

È possibile indicare il nome del metodo alla direttiva per definire quale si desidera chiamare dal controller ma senza ambito di isolamento,

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

TESTATO Spero che questo aiuti qualcuno.

Il mio approccio semplice (pensa ai tag come codice originale)

<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

Forse questa non è la scelta migliore, ma puoi fare angular.element("#element").isolateScope()o $("#element").isolateScope()accedere all'ambito e / o al controller della tua direttiva.


0

Come ottenere il controller di una direttiva in un controller di pagina:

  1. scrivere una direttiva personalizzata per ottenere il riferimento al controller della direttiva dall'elemento 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. usalo nel html del controller di pagina:

    <my-directive controller="vm.myDirectiveController"></my-directive>
  3. Utilizzare il controller della direttiva nel controller della pagina:

    vm.myDirectiveController.callSomeMethod();

Nota: la soluzione fornita funziona solo per i controller delle direttive element (il nome tag viene utilizzato per ottenere il nome della direttiva desiderata).


0

La soluzione seguente sarà utile quando si hanno controller (sia padre che direttiva (isolati)) nel formato "controller As"

qualcuno potrebbe trovarlo utile,

direttiva:

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

controllore direttiva:

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

function SetFitlersToDefaultValue() {
           //your logic
        }
}

codice 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.