Passing parameters to scope function from directive inside ng-repeat

14,535

Solution 1

Certainly for something like this the compile function isn't needed. In fact, since 1.2 the compile function is almost never needed. The only directive I've seen that MIGHT have needed the compile function is the ng-repeat directive itself, and I'm not even convinced that's true.

One important thing to remember is that when passing in a function to a directive to get called, when that function is specified in the html, THAT's when the parameters are bound. So given the following controller (note that I renamed the function being called to be more explicit)

app.controller("AppCtrl", function($scope) {
  $scope.raiseNotification = function(user) {
    alert(user.name)
  }
....

and the following directive:

app.directive("myDir", function($compile) {
  return {
    restrict: 'E',
    scope: {
      user: '=',
      click: '&'
    },
    template: '<a ng-click="click()" ><br>{{user.name}}</a>'
  };
});

if you want to call this method then the following HTML is what you want

<div ng-repeat="user in users">
  <my-dir user="user" click="raiseNotification(user)" ></my-dir>
</div>

when click is called (note that the click() in the template of the directive doesn't have its own parameter. that's because click() in your template is essentially a wrapper for calling raiseNotification(user) i.e.

function click() {
  raiseNotification(user);
}

on the off chance that you want to call the click function from within your directive somewhere and override what user was bound to, then you can do that like this:

click({user:myOtherUser})

and that will override what the "user" parameter is bound to. This is a pretty edge case.

Here's an adjustment to pixelbit's code that includes a fix with the user parameter not specified in the template of the directive.

Also, just for fun, here's another version that more closely approximates what you're doing but using the link function. It's not as succinct or elegant as the previously given code, but gives a little more control using the link function in case you need to do something more specific with the events on the node.

Solution 2

The issue is likely caused when you try to re-compile/re-link a directive that has already been compiled or is in the process of compiling. When you try to re-compile / re-link a directive, this is a major red flag that you are doing something you shouldn't be.

Rather than fight the framework, and get it to do obscure things that it wasn't meant to do, try to work within the framework, and you'll find that accomplishing these tasks is a lot easier to do.

First, here is a working demo.

Tip 1: If you're recompiling a directive - don't. This has a lot of pitfalls, and is more trouble than its worth. Re-organize your HTML instead so you don't have to:

<div ng-controller="AppCtrl">
    <div ng-init="user = {name: 'Works', id: 0}">
      <my-dir user="user"  click="click(user)"></my-dir>
    </div>

    <div ng-repeat="user in users">
      <my-dir user="user" click="click(user)" ></my-dir>
    </div>
</div>

Tip 2: If you're using isolated scope, make use of templates within your directive. Templates can leverage the isolated scope, and can help to make your directive more modular and re-usable.

app.directive("myDir", function($compile) {
  return {
    restrict: 'E',
    scope: {
      user: '=',
      click: '&'
    },
    template: '<a ng-click="click(user)" ><br>{{user.name}}</a>'
  };
});

Tip 3: Don't use 'replace', because its deprecated. Instead, use an element directive if you have to.

Angular does impose some conventions on the HTML format that you should follow, and that may mean that your HTML has some extra tags that you don't want in your final HTML. But I've learned that it's a small price to pay given the productivity gains.

Share:
14,535
Jim Cooper
Author by

Jim Cooper

I work at Pluralsight.com and love it. There's something awesome about working as a developer for a company that provides online developer training videos. I have been working in the industry for about 19 years. I love TDD and learning how to improve the way I write code. @jimthecoop

Updated on June 07, 2022

Comments

  • Jim Cooper
    Jim Cooper almost 2 years

    I am trying to call a function from a directive and pass a parameter. The callback function is being passed in to the isolate scope. I have two problems. First, this doesn't work at all when nested inside an ng-repeat and second, even when not in ng-repeat it I don't know how to pass a parameter to the callback function. Here is a plunker showing the problem: http://plnkr.co/edit/3FN0o3UE99wsmUpxMe4x?p=preview

    Notice that when you click on "This works", it at least executes the function from the parent scope, but when clicking on the others, it does nothing (because they're inside an ng-repeat). That's the first problem.

    The second problem is that when you click on "This works", even though it successfully calls the function, I can't figure out how to pass along the user from the directive scope (notice that it alerts undefined).

    Here is a sample directive (much simplified from my real-world application):

    var app = angular.module('plunker', []);
    app.controller("AppCtrl", function($scope) {
      $scope.click = function(user) {
        alert(user)
      }
      $scope.users = [{
        name: 'John',
        id: 1
      }, {
        name: 'anonymous'
      }];
    });
    app.directive("myDir", function($compile) {
      return {
        scope: {
          user: '=',
          click: '&'
        },
        compile: function(el) {
          el.removeAttr('my-dir');
          el.attr('ng-click', 'click(user)')
          var fn = $compile(el);
          return function(scope, el){
            fn(scope);
          };
        }
      };
    });
    

    Here is the html:

    <!DOCTYPE html>
    <html ng-app="plunker">
    
      <head>
        <meta charset="utf-8" />
        <title>AngularJS Plunker</title>
        <script>document.write('<base href="' + document.location + '" />');</script>
        <link rel="stylesheet" href="style.css" />
        <script data-require="[email protected]" src="http://code.angularjs.org/1.2.12/angular.js" data-semver="1.2.12"></script>
        <script src="app.js"></script>
      </head>
    
      <body>
        <div ng-controller="AppCtrl">
            <a my-dir user="{name: 'Works', id: 0}" click="click()">This works</a>
            <a my-dir user="user" click="click()" ng-repeat="user in users"><br>{{user.name}}</a>
        </div>
      </body>
    
    </html>
    

    Thanks!

  • Joseph Eames
    Joseph Eames over 9 years
    you don't need the "user" parameter in your template. This works: template: '<a ng-click="click()" ><br>{{user.name}}</a>'
  • Joseph Eames
    Joseph Eames over 9 years
    including the user parameter in your template like that is actually a bit misleading. It does nothing but looks like it does.
  • Joseph Eames
    Joseph Eames over 9 years
    I can only assume that's a joke, as it truly does nothing. See the following code which works just perfectly plnkr.co/edit/sL01BCEtU5uaA5i8MnBU?p=preview using this for a template: template: '<a ng-click="click(completelyIrrelevantValue)" ><br>{{user.name}}</a>'
  • pixelbits
    pixelbits over 9 years
    Now that's silly. I'm going to raise that an issue for the Angular team to fix.
  • Salivan
    Salivan over 7 years
    I think this is the most accurate and comprehensive explanation of how to pass parameters to methods bound to directives in AngularJS I've seen on the internet. Thank you! :)