Como chamar um método definido em uma diretiva AngularJS?

297

Eu tenho uma diretiva, aqui está o 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);  
        }
    }
})

Eu gostaria de ligar updateMap() uma ação do usuário. O botão de ação não está na diretiva.

Qual é a melhor maneira de ligar updateMap()de um controlador?

mcbjam
fonte
11
Nota lateral pequena: a convenção não é usar o cifrão para 'escopo' em uma função de link, pois o escopo não é injetado, mas passado como argumento regular.
Noam

Respostas:

369

Se você deseja usar escopos isolados, pode passar um objeto de controle usando a ligação bidirecional =de uma variável do escopo do controlador. Você também pode controlar várias instâncias da mesma diretiva em uma página com o mesmo objeto de controle.

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
fonte
11
+1 Também é assim que eu crio APIs para meus componentes reutilizáveis ​​no Angular.
romiem 10/09/13
5
Esta é mais limpo do que a resposta aceita, e uma para a referência simpsons, se não me engano
Blake Miller
44
Foi exatamente assim que resolvi o mesmo problema. Funciona, mas parece um hack ... Eu gostaria que o angular tivesse uma solução melhor para isso.
Dema
1
Estou aprendendo angular, então minha opinião pode não ter muito peso, mas achei essa abordagem muito mais intuitiva que a outra resposta e a teria marcado como a resposta correta. Eu implementei isso no meu aplicativo sandbox sem nenhum problema.
precisa saber é o seguinte
4
Provavelmente, você deve fazer uma verificação para garantir que scope.controlexista, caso contrário, outros locais que usam a diretiva, mas não precisam acessar os métodos da diretiva e não possuem um controlatributo, começarão a gerar erros por não conseguir definir atributosundefined
CheapSteaks
73

Supondo-se que o botão de ação usa o mesmo controlador $scopecomo a directiva, apenas definir a função updateMapde $scopedentro da função de ligação. Seu controlador pode chamar essa função quando o botão de ação é clicado.

<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


Conforme o comentário do @ FlorianF, se a diretiva usa um escopo isolado, as coisas são mais complicadas. Aqui está uma maneira de fazê-lo funcionar: adicione um set-fnatributo à mapdiretiva que registrará a função de diretiva no 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
fonte
E se a diretiva tiver um escopo isolado?
Florian F
Obrigado! (Talvez seria mais fácil de chamar uma função definida na controladora da directiva mas não tenho certeza sobre isso)
Florian F
1
Essa é uma maneira muito melhor se você não estiver lidando com um escopo isolado.
Martin Frank
Essa resposta realmente responde à pergunta do OP. Ele também usa escopo isolado. Para ter um escopo isolado, você só precisa adicionar a scopepropriedade na declaração da diretiva.
Daniel G.
35

Embora possa ser tentador expor um objeto no escopo isolado de uma diretiva para facilitar a comunicação com ela, isso pode levar a códigos "spaghetti" confusos, especialmente se você precisar encadear essa comunicação por alguns níveis (controlador, diretiva, diretiva aninhada etc.)

Originalmente, seguimos esse caminho, mas depois de mais algumas pesquisas, descobrimos que fazia mais sentido e resultava em um código mais sustentável e legível para expor eventos e propriedades que uma diretiva usará para comunicação por meio de um serviço, em seguida, usando $ watch nas propriedades desse serviço em a diretiva ou quaisquer outros controles que precisem reagir a essas alterações na comunicação.

Essa abstração funciona muito bem com a estrutura de injeção de dependência do AngularJS, pois você pode injetar o serviço em qualquer item que precise reagir a esses eventos. Se você observar o arquivo Angular.js, verá que as diretivas também usam serviços e $ watch dessa maneira, mas não expõem eventos no escopo isolado.

Por fim, no caso de você precisar se comunicar entre diretivas que dependem uma da outra, eu recomendaria compartilhar um controlador entre essas diretivas como meio de comunicação.

O Wiki de Boas Práticas do AngularJS também menciona isso:

Use apenas. $ Broadcast (),. $ Emit () e. $ On () para eventos atômicos Eventos que são relevantes globalmente em todo o aplicativo (como autenticação do usuário ou fechamento do aplicativo). Se você deseja eventos específicos para módulos, serviços ou widgets, considere Serviços, Controladores de Diretiva ou Libs de Terceiros

  • $ scope. $ watch () deve substituir a necessidade de eventos
  • Injetar serviços e chamar métodos diretamente também é útil para comunicação direta
  • As diretivas podem se comunicar diretamente entre si por meio de controladores de diretiva
Sempre aprendendo
fonte
2
Cheguei a duas soluções intuitivamente: (1) observe a alteração de uma variável de escopo =, a variável contém o nome e os argumentos do método. (2) exponha uma string de ligação unidirecional @como identificação do tópico e permita que o receptor envie evento sobre esse tópico. Agora eu vi o wiki de melhores práticas. Eu acho que há razões para não fazê-lo de maneira diferente. Mas ainda não estou muito claro como isso funciona. No meu caso, eu criei uma diretiva tabset, quero expor um switchTab(tabIndex)método. Você poderia exemplificar mais?
stanleyxu2005
Você não expôs um switchTab(tabIndex)método, apenas vinculou a uma tabIndexvariável. Seu controlador de página pode ter ações que alteram essa variável. Você vincula / passa essa variável para a guia Diretiva. A guia Diretiva pode assistir a essa variável em busca de alterações e executar o switchTab por conta própria. Porque a diretiva decide quando / como controlar suas guias com base em uma variável. Esse não é o trabalho de uma fonte externa; caso contrário, as fontes externas exigem conhecimento do funcionamento interno da diretiva, o que é ruim.
Suamere
15

Com base na resposta de Oliver - nem sempre é necessário acessar os métodos internos de uma diretiva e, nesses casos, você provavelmente não deseja criar um objeto em branco e adicionar um controlattr à diretiva apenas para impedir que ela gere um erro (cannot set property 'takeTablet' of undefined )

Você também pode querer usar o método em outros locais da diretiva.

Eu adicionaria uma verificação para garantir que scope.controlexista e definiria métodos de maneira semelhante ao padrão do 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
fonte
No entanto, o uso de um padrão revelador dentro da diretiva torna as intenções muito mais claras. Agradável!
JSancho
12

Para ser sincero, não estava realmente convencido de nenhuma das respostas neste tópico. Então, aqui estão minhas soluções:

Abordagem do manipulador de diretivas (gerente)

Este método é independente do fato de a diretiva $scope ser compartilhada ou isolada

A factorypara registrar as instâncias da diretiva

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

O código de diretiva, eu costumo colocar toda a lógica que não lida com o DOM dentro do controlador de diretiva. E registrando a instância do controlador dentro de nosso manipulador

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

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

Acesse a instância do controlador usando os factorymétodos & execute publicamente expostos

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

Abordagem da Angular

Tirando uma folha do livro da angular sobre como eles lidam com

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

usando $ parse e registrando o controlador no $parentescopo. Essa técnica não funciona em $scopediretivas isoladas .

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

Acesse-o dentro do controlador usando $scope.foo

angular.module('myModule').controller('MyController', function($scope) {
    $scope.someFn = function() {
        $scope.foo.updateMap();
    };
});
Mudassir Ali
fonte
"A abordagem da Angular" está ótima! Há um erro de digitação no entanto: $scope.foodeve ser$scope.my_form
Daniel D
Não, seria $scope.fooporque nosso modelo é <div my-directive name="foo"></div>e nameo valor do atributo é 'foo'. <formé apenas um exemplo de um de directiva da angular que emprega essa técnica
Mudassir Ali
10

Um pouco tarde, mas esta é uma solução com o escopo isolado e "eventos" para chamar uma função na diretiva. Esta solução é inspirada nesta postagem de SO da satchmorun e adiciona um módulo e uma API.

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

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

Crie uma API para se comunicar com a diretiva. O addUpdateEvent adiciona um evento à matriz de eventos e o updateMap chama todas as funções de eventos.

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

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

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

(Talvez você precise adicionar funcionalidade para remover o evento.)

Na diretiva, defina uma referência ao MapAPI e inclua $ scope.updateMap como um evento quando MapApi.updateMap for chamado.

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

        }
    }
});

No controlador "main", adicione uma referência ao MapApi e chame MapApi.updateMap () para atualizar o mapa.

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

    $scope.updateMapButtonClick = function() {
        MapApi.updateMap();    
    };
}
AxdorphCoder
fonte
2
Essa proposta precisaria de um pouco mais de trabalho no mundo real quando você tiver várias diretivas do mesmo tipo, dependendo do seu serviço de API. Você certamente entrará em uma situação em que precisará direcionar e chamar funções de apenas uma diretiva específica e não de todas elas. Deseja melhorar sua resposta com uma solução para isso?
Smajl
5

Você pode especificar um atributo DOM que pode ser usado para permitir que a diretiva defina uma função no escopo pai. O escopo pai pode chamar esse método como qualquer outro. Aqui está um afundador. E abaixo está o código relevante.

clearfn é um atributo no elemento de diretiva no qual o escopo pai pode transmitir uma propriedade de escopo que a diretiva pode definir para uma função que realiza o comportamento desejado.

<!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
fonte
Eu não entendo por que isso funciona .. é porque o atributo clear está no escopo de alguma forma?
Quinn Wilson
1
Torna-se parte do escopo da diretiva assim que você a declara (por exemplo scope: { clearFn: '=clearfn' }).
Trevor
2

Basta usar o scope. $ Parent para associar a função chamada à função de diretiva

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

em HTML

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

Você pode dizer ao nome do método a diretiva para definir qual você deseja chamar do controlador, mas sem isolar o escopo,

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

TESTADO Espero que isso ajude alguém.

Minha abordagem simples (pense em tags como seu 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
fonte
0

Talvez essa não seja a melhor opção, mas você pode fazer angular.element("#element").isolateScope()ou $("#element").isolateScope()acessar o escopo e / ou o controlador de sua diretiva.

Alex198710
fonte
0

Como obter o controlador de uma diretiva em um controlador de página:

  1. escreva uma diretiva personalizada para obter a referência ao controlador de diretiva a partir do 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. use-o no html do controlador de página:

    <my-directive controller="vm.myDirectiveController"></my-directive>
  3. Use o controlador de diretiva no controlador de página:

    vm.myDirectiveController.callSomeMethod();

Nota: a solução fornecida funciona apenas para controladores de diretivas de elemento (o nome da tag é usado para obter o nome da diretiva desejada).

Robert J
fonte
0

A solução abaixo será útil quando você tiver controladores (pai e diretiva (isolados)) no formato 'controller As'

alguém pode achar isso ú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;
    }
}

diretiva Controller:

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
fonte