1

How to expose directive methods without using $broadcast or '=' between modules?

Using $broadcast (events) if there are multiple directives all will be notified. It cannot return value too.

Exposing directive's function by html attribute I think it is not that best that Angular has to offer.

Angular Bootstrap UI do it using services (I guess): It have a service named "$uibModal". You can call a function "$uibModal.open()" of Modal Directive by injecting $uibModal service.

Is that the right way?

saulsluz
  • 94
  • 10
  • The release of AngularJS V1.7.1 introduces the new `ng-ref` directive. The `ng-ref` attribute tells AngularJS to publish the controller of a component on the current scope. This is useful for having a component such as an audio player expose its API to sibling components. Its play and stop controls can be easily accessed. For more information, see [How to expose behavior from a directive with isolated scope?](https://stackoverflow.com/a/53733161/5535245). – georgeawg Dec 11 '18 at 22:21

3 Answers3

2

An example of a directive that registers its API with a service:

app.service("apiService", function() {
    var apiHash = {};
    this.addApi = function (name,api) {
        apiHash[name] = api;
    };
    this.removeApi = function (name) {
        delete apiHash[name];
    };
    this.getApi = function (name) {
        return apiHash[name];
    };
});

app.directive("myDirective", function (apiService) {
    return {
        restrict: 'E',
        scope: {},
        template: `<h1>{{title}}</h1>`,
        link: postLink
    };
    function postLink(scope, elem, attrs)
        var name = attrs.name || 'myDirective';
        var api = {};
        api.setTitle = function(value) {
            scope.title = value;
        };
        apiService.addApi(name, api);
        scope.$on("$destroy", function() {
            apiService.removeApi(name);
        });
    }
});

Elsewhere in the app, the title of the directive can be set with:

apiService.getApi('myDirective').setTitle("New Title");

Notice that the directive registers the api with a name determined by the name attribute of the directive. To avoid memory leaks, it unregisters itself when the scope is destroyed.


Update

How could I use it from a controller?

  app.controller('home', function($scope,apiService) {
    $scope.title = "New Title";
    $scope.setTitle = function() {
      apiService.getApi('mainTitle').setTitle($scope.title);
    };
  })
  <body ng-controller="home">

    <my-directive name="mainTitle"></my-directive>
    <p>
      <input ng-model="title" />
      <button ng-click="setTitle()">Set Title
      </button>
    </p>
  </body>

The DEMO

angular.module('myApp', [])
  .service("apiService", function() {
    var apiHash = {};
    this.addApi = function(name, api) {
      apiHash[name] = api;
    };
    this.getApi = function(name) {
      return apiHash[name];
    };
  })

.directive("myDirective", function(apiService) {
    return {
      restrict: 'E',
      scope: {},
      template: `<h1>{{title}}</h1>`,
      link: postLink
    };

    function postLink(scope, elem, attrs) {
      var name = attrs.name || 'myDirective';
      var api = {};
      api.setTitle = function(value) {
        scope.title = value;
      };
      apiService.addApi(name, api);
      scope.$on("$destroy", function() {
        apiService.addApi(name, null);
      });
    }
  })
  
  .controller('home', function($scope,apiService) {
    $scope.title = "New Title";
    $scope.setTitle = function() {
      apiService.getApi('mainTitle').setTitle($scope.title);
    };
  })
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="myApp" ng-controller="home">
    
    <my-directive name="mainTitle"></my-directive>
    <p>
      <input ng-model="title" />
      <button ng-click="setTitle()">Set Title
      </button>
    </p>
  </body>
georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Thanks for a reply. How could I use it from a controller? I tried something in plunker [here](http://plnkr.co/edit/EHqfKbGE92zoVNYPylIv). But did not worked. What I need is expose directive's methods to be used by another components. Like we was building a package. – saulsluz Jul 18 '17 at 22:53
  • Thanks for the reply! One disadvantage that I see here is, like I said at my question, "if there are multiple directives all will be notified". The service "Service" return a singleton already initialized by angular bootstrap process. Thinking about it, sounds me that I need to return a factory to manipulate each directive ... – saulsluz Jul 19 '17 at 17:12
  • Use the `name` attribute on the directive to separate the APIs. In the example above, the service stores the api under the name `mainTitle`. One can use other names for other instances of the directive. – georgeawg Jul 19 '17 at 17:39
  • I know its not place to say thanks but thanks any way! – saulsluz Jul 19 '17 at 18:01
0
.factory('myService', [function() {
    return {
        charCount: function(inputString) {
            return inputString.length;
        }
    }
}])

this service exposes function charCount(); in your directive you have to inject it like this

.directive('testDirective', ['myService', function(myService) {
    return {
        restrict: 'A',
        replace: true,
        template: "<div>'{{myTestString}}' has length {{strLen}}</div>",
        link: function($scope, el, attrs) {
            $scope.myTestString = 'string of length 19';
            $scope.strLen       = myService.charCount( $scope.myTestString );
        }
    }
}])

and, of course call it

$scope.strLen       = myService.charCount( $scope.myTestString );
Jarek Kulikowski
  • 1,399
  • 8
  • 9
  • I published your sugestion on a plunker [here](http://plnkr.co/edit/FgVsVBo0K6gB5EHZP0jM). Does not worked for me. Maybe I has forgot something. – saulsluz Jul 18 '17 at 21:03
  • I've added a snippet (below) it works there, the plunker looks good actually, I don't know why it isn't working there, let me take another look – Jarek Kulikowski Jul 18 '17 at 21:37
0

<html>
    <style>
        #out {
            width:96%;
            height:25%;
            padding:10px;
            border:3px dashed blue;
            font-family: monospace;
            font-size: 15px;
        }
    </style>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>

    <script>
        var APP = angular.module('MYAPP', []);

        APP.controller('main', ['$scope', '$element', '$compile', 'myService', function($scope, $element, $compile, myService) {
            $scope.test          = 'my Test Controller';
            $scope.directiveTest = "directive test";
            var testSvc = myService.charCount($scope.test);
            $scope.showTestDir = true;
        }])
        .directive('testDirective', ['myService', function(myService) {
            return {
                restrict: 'A',
                replace: true,
                template: "<div>'{{myTestString}}' has length {{strLen}}</div>",
                link: function($scope, el, attrs) {
                    $scope.myTestString = 'string of length 19';
                    $scope.strLen       = myService.charCount( $scope.myTestString );
                }
            }
        }])
        .factory('myService', [function() {
            return {
                charCount: function(inputString) {
                    return inputString.length;
                }
            }
        }])
        .filter('toUpper', function() {
            return function(input) {
                return input.toUpperCase();
            }
        })
        .filter('toLower', function() {
            return function(input) {
                return input.toLowerCase();
            }
        })
        ;
    </script>
        <body ng-app="MYAPP">
        <div id="out" ng-controller="main">
            {{test}} - not filtered
            <br/>
            {{test|toUpper}} - filtered toUpper
            <br/>
            {{test|toLower}} - filtered toLower
            <br/>
            <br/>
            <div test-directive ng-if="showTestDir"></div>
        </div>
    </body>
</html>
Jarek Kulikowski
  • 1,399
  • 8
  • 9
  • Thanks for the reply. But, if a wanted to call charCount() from controller "main" ? – saulsluz Jul 18 '17 at 22:56
  • you can inject service in the controller, I've updated the snippet – Jarek Kulikowski Jul 18 '17 at 23:05
  • Thanks for reply. But notice that "myService.charCount($scope.test);" on controller has no effect. What is really setting value to "test" is the first line of controller "$scope.test". The scope binding is what really changing the value. See at plunker [here](http://plnkr.co/edit/uO3ucVtePONAMaSq6DvT) – saulsluz Jul 19 '17 at 17:04
  • Yes, you are right. $scope.test is not changed by line var testSvc = myService.charCount($scope.test); I put it in to demonstrate that mySerice can be called from the controller. if you do console.log( testSvc); you will see the number of chars of $scope.test, but only in the console log. – Jarek Kulikowski Jul 19 '17 at 17:14
  • No worries. The above example covers a lot of concepts. My advice is to take your time and get a feel for it. Afterwards you will find that you can do a lot with just a few rules. Just keep in mind that we all learn at a different rate and everybody has a different rhythm. So, take your time and have fun playing with it. – Jarek Kulikowski Jul 19 '17 at 17:46
  • I know its not place to say thanks but thanks any way! – saulsluz Jul 19 '17 at 18:02