¿Cómo llamar a un método definido en una directiva AngularJS?

297

Tengo una directiva, aquí está el código:

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

Me gustaría llamar updateMap()a una acción del usuario. El botón de acción no está en la directiva.

¿Cuál es la mejor manera de llamar updateMap()desde un controlador?

mcbjam
fuente
11
Pequeña nota al margen: la convención es no usar el signo de dólar para 'alcance' en una función de enlace, ya que el alcance no se inyecta sino que se pasa como un argumento regular.
Noam

Respuestas:

369

Si desea utilizar ámbitos aislados, puede pasar un objeto de control utilizando el enlace bidireccional =de una variable desde el ámbito del controlador. También puede controlar varias instancias de la misma directiva en una página con el mismo objeto de control.

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>

Oliver Wienand
fuente
11
+1 También es así como creo API para mis componentes reutilizables en Angular.
romiem
55
Esto es más claro que la respuesta aceptada, y +1 para la referencia de los simpsons, si no me equivoco
Blake Miller
44
Así es como resolví el mismo problema. Funciona, pero parece un truco ... Ojalá angular tuviera una mejor solución para esto.
Dema
1
Estoy aprendiendo angular, por lo que mi opinión puede no tener mucho peso, pero este enfoque me pareció mucho más intuitivo que la otra respuesta y lo habría marcado como la respuesta correcta. Implementé esto en mi aplicación sandbox sin problemas.
BLSully
44
Probablemente debería hacer una verificación para asegurarse de que scope.controlexista, de lo contrario, otros lugares que usan la directiva pero no necesitan acceder a los métodos de la directiva y no tienen un controlatributo comenzarán a arrojar errores sobre no poder establecer atributosundefined
CheapSteaks
73

Suponiendo que el botón de acción utiliza el mismo controlador $scopecomo la Directiva, simplemente definir la función updateMapen $scopeel interior de la función de enlace. Su controlador puede llamar a esa función cuando se hace clic en el botón de acción.

<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


Según el comentario de @ FlorianF, si la directiva usa un alcance aislado, las cosas son más complicadas. Aquí hay una manera de hacerlo funcionar: agregue un set-fnatributo a la mapdirectiva que registrará la función de la directiva con el controlador:

<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

Mark Rajcok
fuente
¿Qué pasa si la directiva tiene un alcance aislado?
Florian F
¡Gracias! (Tal vez sería más fácil llamar a una función definida en el controlador de la directiva, pero no estoy seguro de eso)
Florian F
1
Esta es una manera mucho mejor si no se trata de un ámbito aislado.
Martin Frank
Esta respuesta en realidad responde la pregunta OP. También utiliza un alcance aislado, para tener un alcance aislado solo necesita agregar la scopepropiedad en la declaración de directiva.
Daniel G.
35

Aunque puede ser tentador exponer un objeto en el ámbito aislado de una directiva para facilitar la comunicación con él, hacerlo puede llevar a un código confuso de "espagueti", especialmente si necesita encadenar esta comunicación a través de un par de niveles (controlador, directiva, a la directiva anidada, etc.)

Originalmente seguimos este camino, pero después de más investigaciones descubrimos que tenía más sentido y dio como resultado un código más fácil de mantener y legible para exponer eventos y propiedades que una directiva usará para la comunicación a través de un servicio y luego usar $ watch en las propiedades de ese servicio en la directiva o cualquier otro control que necesite reaccionar a esos cambios para la comunicación.

Esta abstracción funciona muy bien con el marco de inyección de dependencia de AngularJS, ya que puede inyectar el servicio en cualquier elemento que necesite reaccionar ante esos eventos. Si observa el archivo Angular.js, verá que las directivas allí también usan servicios y $ watch de esta manera, no exponen eventos en un ámbito aislado.

Por último, en el caso de que necesite comunicarse entre directivas que dependen unas de otras, recomendaría compartir un controlador entre esas directivas como medio de comunicación.

El Wiki de AngularJS para Mejores Prácticas también menciona esto:

Solo use. $ Broadcast (),. $ Emit () y. $ On () para eventos atómicos Eventos que son relevantes globalmente en toda la aplicación (como la autenticación de un usuario o el cierre de la aplicación). Si desea eventos específicos de módulos, servicios o widgets, debe considerar Servicios, Controladores Directivos o Libs de terceros

  • $ scope. $ watch () debería reemplazar la necesidad de eventos
  • La inyección directa de servicios y métodos de llamada también es útil para la comunicación directa.
  • Las directivas pueden comunicarse directamente entre sí a través de controladores de directivas
Siempre aprendiendo
fuente
2
Llegué a dos soluciones intuitivamente: (1) mire el cambio de una variable de alcance =, la variable contiene el nombre del método y los argumentos. (2) exponga una cadena de enlace unidireccional @como identificación del tema y deje que el destinatario envíe un evento sobre este tema. Ahora vi la wiki de mejores prácticas. Creo que hay razones para no hacerlo de una manera u otra. Pero todavía no tengo muy claro cómo funciona. En mi caso, creé una directiva de tabset, quiero exponer un switchTab(tabIndex)método. ¿Podría dar un ejemplo más?
stanleyxu2005
No expondrías un switchTab(tabIndex)método, solo te unirías a una tabIndexvariable. Su controlador de página puede tener acciones que cambien esa variable. Vincula / pasa esa variable a la pestaña Directiva. Su directiva de pestañas puede ver esa variable en busca de cambios y realizar switchTab por sí mismo. Porque la directiva decide cuándo / cómo controlar sus pestañas en función de una variable. Ese no es el trabajo de una fuente externa, de lo contrario, las fuentes externas requieren conocimiento del funcionamiento interno de la directiva, lo cual es malo.
Suamere
15

Sobre la base de la respuesta de Oliver: es posible que no siempre necesite acceder a los métodos internos de una directiva, y en esos casos probablemente no desee tener que crear un objeto en blanco y agregar un controlatributo a la directiva solo para evitar que arroje un error (cannot set property 'takeTablet' of undefined )

También es posible que desee utilizar el método en otros lugares dentro de la directiva.

Agregaría un cheque para asegurarme de que scope.controlexiste, y establecería métodos de manera similar al patrón del módulo revelador

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
        };
      }
    }
  };
});
CheapSteaks
fuente
En el clavo, el uso de un patrón revelador dentro de la directiva aclara las intenciones. ¡Buena esa!
JSancho
12

Para ser honesto, no estaba realmente convencido con ninguna de las respuestas en este hilo. Entonces, aquí están mis soluciones:

Enfoque de manejador de directivas (gerente)

Este método es independiente de si las directivas $scopeson compartidas o aisladas.

A factorypara registrar las instancias de la directiva

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

El código de la directiva, generalmente pongo toda la lógica que no trata con DOM dentro del controlador de la directiva. Y registrando la instancia del controlador dentro de nuestro controlador

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

código de plantilla

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

Acceda a la instancia del controlador utilizando factoryy ejecute los métodos expuestos públicamente

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

Enfoque angular

Sacando una hoja del libro de angular sobre cómo tratan

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

usando $ parse y registrando el controlador en el $parentalcance. Esta técnica no funciona en $scopedirectivas aisladas .

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

Acceda dentro del controlador usando $scope.foo

angular.module('myModule').controller('MyController', function($scope) {
    $scope.someFn = function() {
        $scope.foo.updateMap();
    };
});
Mudassir Ali
fuente
¡El "enfoque angular" se ve genial! Sin embargo, hay un error tipográfico: $scope.foodebería ser$scope.my_form
Daniel D
No, sería $scope.fooporque nuestra plantilla es <div my-directive name="foo"></div>y nameel valor del atributo es 'foo'. <formes solo un ejemplo de una de las directivas angulares que emplea esta técnica
Mudassir Ali el
10

Un poco tarde, pero esta es una solución con el alcance aislado y los "eventos" para llamar a una función en la directiva. Esta solución está inspirada en esta publicación SO de satchmorun y agrega un módulo y una API.

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

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

Cree una API para comunicarse con la directiva. AddUpdateEvent agrega un evento a la matriz de eventos y updateMap llama a cada función de evento.

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

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

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

(Quizás tenga que agregar funcionalidad para eliminar el evento).

En la directiva, establezca una referencia a MapAPI y agregue $ scope.updateMap como evento cuando se llame a 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);

        }
    }
});

En el controlador "principal", agregue una referencia al MapApi y simplemente llame a MapApi.updateMap () para actualizar el mapa.

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

    $scope.updateMapButtonClick = function() {
        MapApi.updateMap();    
    };
}
AxdorphCoder
fuente
2
Esta propuesta necesitaría un poco más de trabajo en un mundo real cuando tiene múltiples directivas del mismo tipo dependiendo de su servicio API. Seguramente se encontrará en una situación en la que necesita apuntar y llamar a funciones desde una sola directiva específica y no todas. ¿Le gustaría mejorar su respuesta con una solución para esto?
smajl
5

Puede especificar un atributo DOM que se puede usar para permitir que la directiva defina una función en el ámbito primario. El ámbito principal puede llamar a este método como cualquier otro. Aquí hay un saqueador. Y a continuación se muestra el código relevante.

clearfn es un atributo en el elemento directivo en el que el ámbito primario puede pasar una propiedad de ámbito que la directiva puede establecer en una función que logre el comportamiento deseado.

<!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>
Trevor
fuente
No entiendo por qué esto funciona ... ¿es porque el atributo claro está en el alcance de alguna manera?
Quinn Wilson el
1
Se convierte en parte del alcance de la directiva tan pronto como la declare (por ejemplo scope: { clearFn: '=clearfn' }).
Trevor
2

Simplemente use el alcance. $ Parent para asociar la función llamada a la función directiva

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>
ramon prata
fuente
2

Puede indicar el nombre del método a la directiva para definir a qué desea llamar desde el controlador pero sin aislar el alcance,

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>

Naveen raj
fuente
1

PROBADO Espero que esto ayude a alguien.

Mi enfoque simple (Piensa en las etiquetas como tu código original)

<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>
Santosh Kumar
fuente
0

Quizás esta no sea la mejor opción, pero puede hacerlo angular.element("#element").isolateScope()o $("#element").isolateScope()acceder al alcance y / o al controlador de su directiva.

Alex198710
fuente
0

Cómo obtener el controlador de una directiva en un controlador de página:

  1. escriba una directiva personalizada para obtener la referencia al controlador de la directiva desde el 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. utilízalo en el controlador html de la página:

    <my-directive controller="vm.myDirectiveController"></my-directive>
  3. Use el controlador directivo en el controlador de página:

    vm.myDirectiveController.callSomeMethod();

Nota: la solución dada funciona solo para los controladores de las directivas de elementos (el nombre de la etiqueta se usa para obtener el nombre de la directiva deseada).

Robert J
fuente
0

La siguiente solución será útil cuando tenga controladores (padre y directiva (aislados)) en formato 'controlador como'

alguien puede encontrar esto útil,

directiva:

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

controlador directivo:

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

function SetFitlersToDefaultValue() {
           //your logic
        }
}

código 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
Raunak Mali
fuente