Madhusudhan
Madhusudhan

Reputation: 8563

ng-click doesn't work within the template of a directive

Angular noob here. I am creating a directive to recursively display a tree of questions and sub questions. I am using a link in the template which calls a function within the scope. For some reason, it does't call the editQuestion() method.

Here's the code and the fiddle http://jsfiddle.net/madhums/n9KNv/

HTML:

<div ng-controller="FormCtrl">
  <questions value="survey.questions"></questions>
</div>

Javascript:

var app = angular.module('myApp', []);

function FormCtrl ($scope) {
  $scope.editQuestion = function (question) {
    alert('abc');
  };
  $scope.survey = {
    // ...
  }
}


app.directive('questions', function($compile) {
  var tpl = '<ol ui-sortable' +
    ' ng-model="value"' +
    ' class="list">' +
    '  <li ng-repeat="question in value | filter:search"' +
    '     <a href="" class="question">' +
    '       {{ question.name }}' +
    '     </a>' +
    '     <span class="muted">({{ question.type }})</span>' +
    '     <a href="" class="danger" ng-click="removeQuestion(question)">remove</a>' +
    '     <a href="" class="blue" ng-click="editQuestion(question)">edit</a>' +
    '     <choices value="question.choices"></choices>' +
    '  </li>' +
    '</ol>';

  return {
    restrict: 'E',
    terminal: true,
    scope: { value: '=' },
    template: tpl,
    link: function(scope, element, attrs) {
        $compile(element.contents())(scope.$new());
    }
  };
});

app.directive('choices', function($compile) {
  var tpl = '<ul class="abc" ng-repeat="choice in value">'+
    '  <li>' +
    '    {{ choice.name }}' +
    '    <span class="muted">' +
    '      ({{ choice.questions.length }} questions)' +
    '    </span>' +
    '' +
    '    <a href=""' +
    '      ng-click="addQuestions(choice.questions)"' +
    '      tooltip="add sub questions">' +
    '      +' +
    '    </a>' +
    '' +
    '    <questions value="choice.questions"></questions>'
    '  </li>' +
    '</ul>';

  return {
    restrict: 'E',
    terminal: true,
    scope: { value: '=' },
    template: tpl,
    link: function(scope, element, attrs) {
        $compile(element.contents())(scope.$new());
    }
  };
});

Any help in understanding this would be appreciated.

Upvotes: 39

Views: 40248

Answers (4)

Ryan Knell
Ryan Knell

Reputation: 6342

To anybody coming to this with and trying to do it with code that isn't running in your directive, check that you aren't using the option replace.

Eg:

angular.module('app').directive('myDirective', function () {
return {
    template: '<div ng-click="clicked()"></div>',
    scope: {
      options: "="
    },
    replace: true, //<---- Change this to false
    restrict: 'E',
    controller: function ($scope) {

      $scope.clicked = function(){
        console.log("Clicked");
      }
    }
  };
}

Upvotes: 1

user3777549
user3777549

Reputation: 419

Langdon's May10' 13 answer is correct. For demonstration purposes I streamlined Langdon's fiddle code and brought it down from 148 lines of angular to 23 lines of angular.
I also added functionality that enables passing a parameter value as a MouseEvent object through the function call method and retrieving said value in the function.

Here's the JSFIDDLE followed by the code and credits, it should be very easy to follow.

http://jsfiddle.net/BeyondLogical/evjzoo30/

--Html--

<div ng-controller="FormCtrl">
    <questions on-edit="editQuestion(ev,question)" ></questions>
</div>

--AngularJS--

var app = angular.module('myApp', []);
function FormCtrl ($scope) {
    $scope.editQuestion = function (ev,question) {
        //ev returns a MouseEvent object
        alert("ev: " + ev);
        //this is how you get the 'data' attribute set in the span tag below
        alert("ev-data: " + ev.target.attributes.data.value); 
    };
}
app.directive('questions', function($compile) {
    var tpl = 
    '<span ng-click="onEdit({ev: $event, myName: question})" data="This sentence would probably be replaced with a mustache brace parameter, example: {{someValue}}, returning a value from the scope." style="cursor:pointer;">Click Me</span>';
    return {
        restrict: 'E',
        terminal: true,
        scope: { onEdit: '&' },
        template: tpl,
        link: function(scope, element, attrs) {
            $compile(element.contents())(scope.$new());
        }
    };
});

Credits to,
Langdon - ng-click doesn't work within the template of a directive

Mark Rajcok - AngularJS getting $event from a directive (Langdon also gets an assist for asking the question Mark Answers)

PavanAsTechie - Access attribute value inside non-directive controller function and Pavan's JSFIDDLE - http://jsfiddle.net/brettdewoody/FAeJq/ (notably Pavan's following line of code):alert(obj.target.attributes.data.value);

Upvotes: 2

Langdon
Langdon

Reputation: 20073

You've got a scope issue. Since you used isolated scope in your directive with scope: { value: '=' }, it no longer has access to your controller's scope that has editQuestion.

You need to pass editQuestion along to your directive's scope so it knows how to call it. This is typically pretty easy, but because of your infinitely recursive directive structure where choices can include questions, it gets a bit trickier. Here's a working fiddle:

http://jsfiddle.net/n9KNv/14/

The HTML now includes a reference to editQuestion:

<div ng-controller="FormCtrl">
    <questions value="survey.questions" on-edit="editQuestion(question)"></questions>
</div>

And your questions directive now expects an onEdit attribute in its scope:

app.directive('questions', function($compile) {
  var tpl = '<ol ui-sortable' +
    ' ng-model="value"' +
    ' class="list">' +
    '  <li ng-repeat="question in value | filter:search"' +
    '     <a href="" class="question">' +
    '       {{ question.name }}' +
    '     </a>' +
    '     <span class="muted">({{ question.type }})</span>' +
      '     <a href="" class="blue" ng-click="onEdit({question: question})">edit</a>' +
      '     <choices value="question.choices" on-edit="onEdit({question: subQuestion})"></choices>' +
    '  </li>' +
    '</ol>';

  return {
    restrict: 'E',
    terminal: true,
      scope: { value: '=', onEdit: '&' },
    template: tpl,
    link: function(scope, element, attrs) {
        $compile(element.contents())(scope.$new());
    }
  };
});

app.directive('choices', function($compile) {
  var tpl = '<ul class="abc" ng-repeat="choice in value">'+
    '  <li>' +
    '    {{ choice.name }}' +
    '    <span class="muted">' +
    '      ({{ choice.questions.length }} questions)' +
    '    </span>' +
    '' +
      '    <questions value="choice.questions" on-edit="onEdit({subQuestion: question})"></questions>'
    '  </li>' +
    '</ul>';

  return {
    restrict: 'E',
    terminal: true,
      scope: { value: '=', onEdit: '&' },
    template: tpl,
    link: function(scope, element, attrs) {
        $compile(element.contents())(scope.$new());
    }
  };
});

Notice how we're targeting question in the ng-click. This is how you target arguments in callback functions. Also notice how in the on-edit we're passing to your choices directive, we're targeting subQuestion. This is because question is already reserved inside of the ngRepeat, so we need to differentiate between the two.

This was probably the hardest concept for me to learn in Angular so far. Once you understand how scope works between controllers, directives, and other directives, the world of Angular is yours. :)

Upvotes: 38

Jason
Jason

Reputation: 15931

This is a problem of scope. The directive's ng-click calling the current scope's editQuestion & removeQuestion methods, which don't exist in the directive's scope, as they are defined in module that is including the directive (i.e. the parent scope).

You want to establish a binding between the directive and the parent, so when, the directive calls the ngClick function, it fires on the module that is hosting the directive.

Either you can define the methods in the directive itself, or setup binding through the scope section of the directive definition object

Here's a plunker that illustrates firing ng-click events at different scopes (outputs to console)

http://plnkr.co/edit/9XfXCpU6lhUOqD6nbVuQ?p=preview

Upvotes: 7

Related Questions