27

I am using AngularJS with the alias controllers pattern. I can't access (or I don't know how to) directive methods from a parent controller.

I have a function inside my controller that should call a directive method but this directive method is not available inside the this controller value.

This is what I have. What I am doing wrong?

JS

angular.module('myApp', []).

controller('MyCtrl', function(){
  this.text = 'Controller text';

  this.dirText = 'Directive text';

  this.click = function(){
    this.changeText();
  }
})

.directive('myDir', function(){
  return {
     restrict: 'E',
     scope: {
       text: '='
     },
     link: function(scope, element, attrs){
       scope.changeText = function(){
         scope.text = 'New directive text';
       };
     },
     template: '<h2>{{text}}</h2>'
  };
});

HTML

<div ng-app="myApp">
  <div ng-controller="MyCtrl as ctrl">
    <h1>{{ctrl.text}}</h1>
    <my-dir text="ctrl.dirText"></my-dir>
    <button ng-click="ctrl.click()">Change Directive Text</button>
  </div>
</div>

Here a codepen with the code.

isherwood
  • 58,414
  • 16
  • 114
  • 157
ianaya89
  • 4,153
  • 3
  • 26
  • 34

5 Answers5

28

If you strictly want to use isolated scope inside a directive then the directive method can be only called by using angular events such as $broadcast & $emit

In your case, you need to use $broadcast to send event to entire $rootScope

You Code will become like this.

Working Code Pen

HTML

<div ng-app="myApp">
  <div ng-controller="MyCtrl as ctrl">
    <h1>{{ctrl.text}}</h1>
    <my-dir text="ctrl.dirText"></my-dir>
    <button ng-click="ctrl.click()">Change Directive Text</button>
  </div>
</div>

CODE

angular.module('myApp', []).

controller('MyCtrl', function($rootScope){
  var that = this;

  this.text = 'Controller text';

  this.dirText = 'Directive text';

  this.click = function(){
      $rootScope.$broadcast('changeText',{});
  }
}).

directive('myDir', function(){
  return {
     restrict: 'E',
     scope: {
       text: '='
     },
     link: function(scope, element, attrs){
       scope.changeText = function(){
         scope.text = 'New directive text';
       };
         scope.$on('changeText',function(event, data){
             scope.changeText()
         });
     },
     template: '<h2>{{text}}</h2>'
  };
});

Instead of calling method of child scope, you need to broadcast an event and that will have to be listened by the directive scope & it will fire changeText method after listening to that event.

NOTE

Using service / factory would be better approach.

This would be hopefully help you. Thanks.

Tushar Shukla
  • 5,666
  • 2
  • 27
  • 41
Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • why not? I don't get how can achieve a similar behaviour using services. – ianaya89 Jan 23 '15 at 19:45
  • @ianaya89 do you want to call different method from different directive..for that you are created isolated scope.which is overhead..isolated scope to define different method that curious to me.. – Pankaj Parkar Jan 23 '15 at 19:50
  • i.e.: I want a html modal which has a functions to open and close it and I want to use this modal in different pages with different controllers avoiding repeat code writing in all those controllers the open and close methods. So I want those methods defined inside the directives and then just call the directive method inside the controller. Makes any sense? – ianaya89 Jan 23 '15 at 20:08
  • 6
    @PankajParkar if there is someone with username `downvoter` he probably got a notification :D – T J Nov 23 '15 at 09:33
  • Correct your type `fire` not `file` – NamPNQ Apr 28 '16 at 14:59
  • $broadcast gets called multiple times on click from a controller method. (ng-repeat) How do I avoid it? – Smitha Nov 09 '17 at 12:37
16

You can achieve calling directive methods without relying on $broadcast or removing scope isolation. Similar approaches that have been posted here so far will break if there are 2+ instances of the directive on a page (they'll all reflect the same changes).

This codepen demonstrates a more robust way to do it.

angular.module('myApp', [])
.controller('myChat', function($scope) {
    
    function room () {return { accessor:{} }; }
    $scope.rooms = { 'RoomA': new room, 'RoomB': new room, 'RoomC': new room };

    $scope.addMessageTo = function(roomId, msg) {
      if ($scope.rooms[roomId].accessor.setMessage)
        $scope.rooms[roomId].accessor.setMessage(msg);
    };

    $scope.addMessages = function () {
      $scope.addMessageTo("RoomA", "A message");
      $scope.addMessageTo("RoomB", "Two messages");
      $scope.addMessageTo("RoomC", "More messages");
    }
    
}).directive('myChatRoom', function() {

    return {
      template: '<div>{{room}} message = {{message}}<div />',
      scope: { accessor: "=", room: "@" },
      link: function (scope) {
        if (scope.accessor) {
          scope.accessor.setMessage = function(msg) {
            scope.message = msg;
          };
        }
      }
    };
  
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
  <div ng-controller="myChat">

    <div ng-repeat="(roomId, room) in rooms">
      <div my-chat-room room="{{roomId}}" accessor="room.accessor"></div>
    </div>

    <button ng-click="addMessages()">Add messages to rooms</button>

  </div>
</div>
BernardV
  • 1,700
  • 20
  • 15
  • I really didn't want to use the broadcast approach, you saved the day for me. Cheers – Abubakr Dar Mar 23 '17 at 13:21
  • @BernardV: Can you elaborate on how your solution works and what it actually does? – user1438038 Sep 19 '17 at 08:41
  • @user1438038: I'm sorry but I hardly see what else can be added here. The code entirely defines how it works. And to see what it does, simply click on "Run code snipped" – BernardV Sep 20 '17 at 20:53
9

I have another solution, that lets you use isolate scope and don't rely on broadcast. In javascript methods can be used like variables, co you can simply pass the method you want to the directive.

so in html:

<my-dir text="ctrl.dirText" change-text="ctrl.changeText"></my-dir>

and in directive

scope: {
   text: '=',
   changeText: '='
 }

Here is slightly modyfied codepen, where You can see what i have in mind.

  • 4
    you are passing **father's** method to directive, but the question was about the reverse direction – Nir O. May 16 '17 at 07:28
2

You are isolating the scope when you write:

 scope: {
       text: '='
     },

Here's a slightly modified version of your code, this time, lets you call directive method. Mostly I just got rid of 'scope' in directive, and changed it to using $scope in the controller, rather than this, and Alias pattern..

WARNING: This might not reflect the correct behavior, with regard's to which variables get changed, but answers your question by showing how you can access directive's method from controller. This is usually not a good design idea..

http://codepen.io/anon/pen/azwJBm

angular.module('myApp', []).

controller('MyCtrl', function($scope){
  var that = this;

  $scope.text = 'Controller text';

  $scope.dirText = 'Directive text';

  $scope.click = function(){
    $scope.changeText();
  }
}).

directive('myDir', function(){
  return {
     restrict: 'E',
    /* scope: {
       text: '='
     },*/
     link: function(scope, element, attrs){
       scope.changeText = function(){
         scope.text = 'New directive text';
       };
     },
     template: '<h2>{{text}}</h2>'
  };
});


<div ng-app="myApp">
  <div ng-controller="MyCtrl">
    <h1>{{text}}</h1>
    <my-dir text="dirText"></my-dir>
    <button ng-click="click()">Change Directive Text</button>
  </div>
</div>
Shaunak
  • 17,377
  • 5
  • 53
  • 84
0

After trying both the $broadcastand the control object solutions, I would actually recommend trying to bind values or arrays. The control object is a simple way to achieve the desired result, but in my testing very undiscoverable and error-prone.

This Codepen builds BernardV's example, but uses an array of messages as a very visible control binding. If you want, you can easily $watch the messages array inside the directive as well. The core idea is to in the directive use:

scope: { messages: "=", room: "@" },

From a controller (having an array of "rooms") you would do this:

$scope.addMessages = function () {
  angular.forEach($scope.rooms, function(room, index) {
    room.messages.push("A new message! # " + (index+1);
  })
} 

Independent directives, independent messages and highly discoverable. You could of course in the directive only show the latest message, or simply bind a string instead of an array. This solution worked much better for us at least.

Community
  • 1
  • 1
Victor
  • 3,999
  • 3
  • 24
  • 27
  • I see no place in this code where the controller is calling a function that was declared inside of the directive. – CodeToad Apr 11 '18 at 23:37