21

I've created a simple directive that displays sort column headers for a <table> I'm creating.

ngGrid.directive("sortColumn", function() {
    return {
        restrict: "E",
        replace: true,
        transclude: true,
        scope: {
            sortby: "@",
            onsort: "="
        },
        template: "<span><a href='#' ng-click='sort()' ng-transclude></a></span>",
        link: function(scope, element, attrs) {
            scope.sort = function () {

                // I want to call CONTROLLER.onSort here, but how do I access the controller scope?...
                scope.controllerOnSort(scope.sortby);
            };
        }
    };
});

Here's an example of some table headers being created:

<table id="mainGrid" ng-controller="GridCtrl>
<thead>
    <tr>
        <th><sort-column sortby="Name">Name</sort-column></th>
        <th><sort-column sortby="DateCreated">Date Created</sort-column></th>
        <th>Hi</th>
    </tr>
</thead>

So when the sort column is clicked I want to fire the onControllerSort function on my grid controller.. but I'm stuck! So far the only way I've been able to do this is for each <sort-column>, add attributes for the "onSort" and reference those in the directive:

<sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column>

But that's not very nice since I ALWAYS want to call controllerOnSort, so plumbing it in for every directive is a bit ugly. How can I do this within the directive without requiring unnecesary markup in my HTML? Both the directive and controller are defined within the same module if that helps.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Matt Roberts
  • 26,371
  • 31
  • 103
  • 180

5 Answers5

24

Create a second directive as a wrapper:

ngGrid.directive("columnwrapper", function() {
  return {
    restrict: "E",
    scope: {
      onsort: '='
    }
  };
});

Then you can just reference the function to call once in the outer directive:

<columnwrapper onsort="controllerOnSort">
  <sort-column sortby="Name">Name</sort-column>
  <sort-column sortby="DateCreated">Date Created</sort-column>
</columnwrapper>

In the "sortColumn" directive you can then call that referenced function by calling

scope.$parent.onsort();

See this fiddle for a working example: http://jsfiddle.net/wZrjQ/1/

Of course if you don't care about having hardcoded dependencies, you could also stay with one directive and just call the function on the parent scope (that would then be the controller in question) through

scope.$parent.controllerOnSort():

I have another fiddle showing this: http://jsfiddle.net/wZrjQ/2

This solution would have the same effect (with the same criticism in regard to hard-coupling) as the solution in the other answer (https://stackoverflow.com/a/19385937/2572897) but is at least somewhat easier than that solution. If you couple hard anyway, i don't think there is a point in referencing the controller as it would most likely be available at $scope.$parent all the time (but beware of other elements setting up a scope).

I would go for the first solution, though. It adds some little markup but solves the problem and maintains a clean separation. Also you could be sure that $scope.$parent matches the outer directive if you use the second directive as a direct wrapper.

Community
  • 1
  • 1
Juliane Holzt
  • 2,135
  • 15
  • 14
  • I prefer this because it's a simpler solution than the other one. I still don't think that tightly coupling matters for me though in this scenario, since I'm making a grid control and the directive is only for use with the grid control... – Matt Roberts Oct 16 '13 at 07:58
  • I tried to create a parent directive as per your suggestion 1, but I seem to be having issues with that too - I'm not sure how to access the "scope" of the parent directive.. I posted another question with a plunker here : http://stackoverflow.com/questions/19405176/table-headers-with-sort-indicators – Matt Roberts Oct 16 '13 at 15:54
  • 2
    Although it works here, using `$parent` is a bad pattern. It will only work one deeper level, so it is difficult to reuse. – Carlos Morales Sep 02 '15 at 08:19
  • The use of two-way binding with `'='` is dated and makes migrating to Angular 2+ more difficult. For more information, see [AngularJS Developer Guide - Component-based application architecture](https://docs.angularjs.org/guide/component#component-based-application-architecture). – georgeawg Jun 28 '18 at 13:25
20

The & local scope property allows the consumer of a directive to pass in a function that the directive can invoke.

Illustration of & scope property

See details here.

Here is a answer to a similar question, which shows how to pass argument in the callback function from the directive code.

Community
  • 1
  • 1
gm2008
  • 4,245
  • 1
  • 36
  • 38
8

In your directive require the ngController and modify the link function as:

ngGrid.directive("sortColumn", function() {
    return {
        ...
        require: "ngController",
        ...
        link: function(scope, element, attrs, ngCtrl) {
            ...
        }
    };
});

What you get as ngCtrl is your controller, GridCtrl. You dont get its scope though; you would have to do something in the lines of:

xxxx.controller("GridCtrl", function($scope, ...) {
    // add stuff to scope as usual
    $scope.xxxx = yyyy;

    // Define controller public API
    // NOTE: USING this NOT $scope
    this.controllerOnSort = function(...) { ... };
});

Call it from the link function simply as:

ngCtrl.controllerOnSort(...);

Do note that this require will get the first parent ngController. If there is another controller specified between GridCtrl and the directive, you will get that one.

A fiddle that demonstrates the principle (a directive accessing a parent ng-controller with methods): http://jsfiddle.net/NAfm5/1/


People fear that this solution may introduce unwanted tight coupling. If this is indeed a concern, it can be addressed as:

Create a directive that will be side-by-side with the controller, lets call it master:

<table id="mainGrid" ng-controller="GridCtrl" master="controllerOnSort()">

This directive references the desired method of the controller (thus: decoupling).

The child directive (sort-column in your case) requires the master directive:

require: "^master"

Using the $parse service the specified method can be called from a member method of the master controller. See updated fiddle implementing this principle: http://jsfiddle.net/NAfm5/3/

Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90
  • I'm not sure that tightly coupling your directive and controller in this way is the best idea. It would definitely work, but then anytime you use this directive you need to define a very specific method on your controller. The way he's doing it currently is arguably better practice. – Adam Oct 15 '13 at 16:07
  • As a matter of fact these components are already coupled, since the one requires the other for correct operation. The directive depends on the existence of a controller (any controller) that has a specific method, here `controllerOnSort()`. This is equivalent to having the controller implement an interface in strongly typed langs. The parent controller can be made optional with the `require: "?^ngController"` spec. – Nikos Paraskevopoulos Oct 15 '13 at 16:11
  • Not true, because if you look at his question, he is passing the callback in through an attribute. Name This decouples the directive from any controller or scope. The sort method can be named anything in his example, whereas yours requires strict naming. His example also allows passing a method in from any scope in the hierarchy, whereas yours looks for a direct parent. His complaint was simply that he would have to repeat the attribute throughout all of his columns. – Adam Oct 15 '13 at 16:13
  • Indeed and this is (one) way to do it with a tradeoff: you gain convenience, you lose coupling (in theory, because these things are already logically coupled) – Nikos Paraskevopoulos Oct 15 '13 at 16:16
  • Thanks for the thorough response. The "tight coupling" is a valid point, although for this use I don't mind, because i ONLY want the directive to be used with the grid control, so I don't see a big problem with it being tightly coupled in this case – Matt Roberts Oct 16 '13 at 07:57
2

There is another way to do this, although given my relative lack of experience I can't speak for the fitness of such a solution. I will pass it along anyhow just for informational purposes.

In your column, you create a scope variable attribute:

<sort-column data-sortby="sortby">Date Created</sort-column>

Then in your controller you define the scope variable:

$scope.sortby = 'DateCreated' // just a default sort here

Then add your sort function in controller:

$scope.onSort = function(val) {
    $scope.sortby = val;
}

Then in your markup wire up ng-click:

<sort-column data-sortby="sortby" ng-click="onSort('DateCreated')">Date Created</sort-column>

Then in your directive you add the sortby attribute to directive scope:

scope: {
    sortby: '=' // not sure if you need
}

And in your "link:" function add a $watch:

scope.$watch('sortby', function () {
    ... your sort logic here ...
}

The beauty of this approach IMO is that your directive is decoupled completely, you don't need to call back to onSort from the directive because you don't really leave onSort in the controller during that part of the execution path.

If you needed to tell your controller to wait for the sort to finish you could define an event in the controller:

$scope.$on("_sortFinished", function(event, message){
   ..do something...  
});

Then in your directive simply emit the event then the process is done:

$scope.$emit('_sortFinished');

There's other ways to do that, and this kind of adds some tight-ish coupling because your controller has to listen for. and your directive has to emit a specific even... but that may not be an issue for you since they are closely related anyhow.

Brandon
  • 830
  • 1
  • 15
  • 35
1

Call me crazy, but it seems easier to just get the controller from the element via the inbuilt method for that, rather than fiddling with require:

var mod = angular.module('something', []).directive('myDir', 
  function () {
    return {
      link: function (scope, element) {
        console.log(element.controller('myDir'));
      },
      controller: function () {
        this.works = function () {};
      },
      scope: {}
    }
  }
);

http://plnkr.co/edit/gY4rP0?p=preview

marksyzm
  • 5,281
  • 2
  • 29
  • 27