1

I am building an AngularJS directive. I want that directive to wrap other contents which have an ng-click inside of it. The resulting button does not do anything when clicked. Here is a simplified version of the code which I tried:

(HTML)

<div ng-app="someapp">
    <div ng-controller="Ctrl1">
        <h2>Example</h2>
        <my-dir data-x="1">
            <button ng-click="refresh()" id="refresh1">Refresh</button>
        </my-dir>
        <my-dir data-x="2">
            <button ng-click="refresh()" id="refresh2">Refresh</button>
        </my-dir>
    </div>
</div>

(JavaScript)

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

app.controller('Ctrl1', function($scope){ });

app.directive('myDir', function(){
    return {
        restrict: 'E',
        scope: {},
        template: '<div><p>Directive contents:</p><div ng-transclude></div></div>',
        transclude: true,
        link: function(scope, element, attrs){
            $scope.y = attrs.x+1;
            scope.refresh = function(){
                console.log("Refresh Called, y = ", $scope.y);
            }
        }
    };
});

How can I change it so that the button actually triggers the $scope.refresh() function?

Extra clarification:

I need local object information for the directive (there can be multiple of this directive in the single controller), so I create a new scope.

Alphadelta14
  • 2,854
  • 3
  • 16
  • 19

2 Answers2

3

As dcodesmith points out, the transcluded ng-click will be bound to the controller's scope rather than the directive's. Depending on what exactly you want to do, you may wish for this to be the behavior (since the transcluded content is not part of the directive, why should it call a method of the directive's scope?). Personally, I would declare the method on the controller's scope instead.

app.controller('Ctrl1', function($scope){ 
    $scope.refresh = function() {
        console.log("Refresh called.");
    };
});

In this case, your directive should declare the isolate scope, even if there is nothing in it.

Update: Based on your comment, why not just include the button in the directive template? In this case, it will already be associated with the correct scope.

If there are some situations that you will not need the refresh button, then expose that as an option through the directive as an attribute:

<my-dir show-button='true'></my-dir>

// directive scope
scope: {
    showButton: '='
} 

The biggest problem that I have with this approach is using the "two-way-binding" operator (=) to cause 'true' and 'false' to be treated as expressions. I just don't like the feel of that very much.

Anyway, hopefully this solves your problem... One more comment, and I'm saying this not even knowing if what you are implementing is actually a refresh button, but if it is, I would take a minute and consider whether you really need a "refresh" button. Angular excels at eliminating refresh button!

Update 2:

I've created a plunkr that shows how I think I would handle your situation, especially if any of the miscellaneous controls are reused:

http://plnkr.co/edit/FhrSwcrSZScvCfhtCSjn

In this example, the two button directives are effectively children of the "videoPlayer" directive. Their logic is contained within that directive, but they are instantiated separately, and not required for the parent directive to operate. The "parent directive" (videplayer) simply exposes the API for the "children" to use. Also not that the methods of the parent are methods of the constructor, not the scope. I think this is very strange, but it is taken exactly from the angular documentation:

http://docs.angularjs.org/guide/directive (last example on the page)

Note that each videoPlayer directive will still have it's own isolate scope.

trey-jones
  • 3,329
  • 1
  • 27
  • 35
  • I'm not locking the directive down to a single controller though; I'm drawing graphs which could be used anywhere in my application. – Alphadelta14 Jan 06 '14 at 00:40
  • I've updated my answer based on your comment. In your other comment, I'm not sure I entirely understand the question. Using the "isolate" scope, like you are, you can have as many instances of this directive as you want within the same controller - their scopes are isolated. Are you talking about sharing data between the directives? – trey-jones Jan 06 '14 at 01:35
  • I know I need to use the Isolate scope. My other comment was me confirming that I cannot just inherit scope. I have a lot of miscellaneous controls for each directive (Think buttons on an online video player, except with graph controls). And yes, I realize that Angular can handle refreshes well. There was just one case where I could not avoid it (Hence why I needed that particular button). I probably can get it working with just a bunch more attributes though. Thanks! – Alphadelta14 Jan 06 '14 at 01:42
  • I added another possible solution, and a plunkr. – trey-jones Jan 06 '14 at 02:30
  • Is it really the Angular way to dynamically bind onclick's to elements like that? – Alphadelta14 Jan 06 '14 at 06:13
  • In a directive, yes. Look at the source for another directive, ng-click! But you could accomplish the very same thing by using ng-click in the template for the directive. The difference is that this would give you more control over what type of element to use... Really the point of that example would be using directives that interact with each other through a controller. The implementation would still be very flexible. – trey-jones Jan 06 '14 at 14:17
  • 1
    Thank you for this, "transcluded content is not part of the directive, why should it call a method of the directive's scope?" – Dennis Aug 31 '14 at 19:04
  • 1
    Hello from 5 years later - just wanted to say thanks for this (for Update 2), it saved my sanity! – PoorbandTony Jul 31 '19 at 08:48
0

Remove the scope object. There seems to be a conflict going on there.

Excerpt from Creating a Directive that Wraps Other Elements

The transclude option changes the way scopes are nested. It makes it so that the contents of a transcluded directive have whatever scope is outside the directive, rather than whatever scope is on the inside. In doing so, it gives the contents access to the outside scope.

app.directive('myDir', function(){
    return {
        restrict: 'E',
        //scope: {}, remove this line.
        template: '<div><p>Directive contents:</p><div ng-transclude></div></div>',
        transclude: true,
        link: function(scope, element, attrs){
            console.log("X-attr", attrs.x);
            scope.refresh = function(){
                console.log("Refresh Called");
            }
        }
    };
});
dcodesmith
  • 9,590
  • 4
  • 36
  • 40
  • Added some additional information which leads to the question: What if there are multiple of the same directive in a controller that require a local scope? – Alphadelta14 Jan 06 '14 at 00:34
  • The `link` function on the directive provides a `transclude` parameter that can provide access to the transcluded HTML: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/ – Thomas Higginbotham Jul 28 '14 at 13:49