11

I need to create a directive that acts upon table cells where the table rows are rendered using ng-repeat -- to that end I have relied in part on this answer to a question entitled "Calling a function when ng-repeat has finished". Unlike that Q&A however, I need to pass in an argument to my directive, and for this I have relied in part on this answer (to a question entitled "Angularjs - Pass argument to directive").

So in my case I've added fixed-column-tooltip for my directive, and columnselector as its argument to the <tr> as follows:

<tr fixed-column-tooltip columnselector=".td-keyField" ng-repeat="trData in trDataWatch">

But when per the second answer, I added what I've learned is an "isolate scope" to my directive, I no longer had access to the original scope necessary as per the first answer:

'use strict';

angular.module('cmt.cases.directives')

.directive('fixedColumnTooltip', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
            columnselector: '@'
        },
        link: function (scope, element, attr) {
            if (scope.$last === true) { //undefined because not operating on original scope
        ...

Is there a way to maintain access to the original scope, but also have access to the columnselector argument?

Community
  • 1
  • 1
Dexygen
  • 12,287
  • 13
  • 80
  • 147
  • There seems to be a lot of misunderstanding and I'm probably not going to be able to award the bounty. My understanding is that when creating a directive on an ng-repeat, the parent scope to that directive's scope is the scope of the ng-repeat. Some answers are referencing a controller's scope, but I've never introduced a controller into my example whatsoever. – Dexygen Jan 16 '16 at 22:56
  • You are correct. You can access the parent scope on the scope object within your link function (see my answer below). – BMcV Jan 17 '16 at 18:58

9 Answers9

3

You could use,

'use strict';

angular.module('cmt.cases.directives')

.directive('fixedColumnTooltip', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
            columnselector: '@',
            $last: '=$last',
        },
        link: function (scope, element, attr) {
            if (scope.$last === true) {
            ....

the second parameter to scope will pass $last parameter by reference.

EDIT:

Since the $last is only available in the scope of repeat element, you could get it from the element scope, like this

'use strict';

angular.module('cmt.cases.directives')

.directive('fixedColumnTooltip', function ($timeout) {
return {
    srestrict: 'A',
    scope: {
        columnselector: '@',
    },
    link: function (scope, element, attrs) {
      var elemScope = element.scope();
      if (elemScope.$last){
             ......
      }          
    }
}
Low Flying Pelican
  • 5,974
  • 1
  • 32
  • 43
  • I tried this, copied/pasted it directly and got the following error (BTW we are on Angular 1.4.2 if that makes a difference: Error: [$compile:iscp] Invalid isolate scope definition for directive 'fixedColumnTooltip'. Definition: {... $last: '=$last' ...} – Dexygen Jan 11 '16 at 14:34
  • I also tried using the shorthand `$last: '='` as described in the docs at https://docs.angularjs.org/guide/directive#! (search page for 'shorthand') and this cleared the error, but scope.$last was never equal to true :( – Dexygen Jan 11 '16 at 16:16
  • Are you sure you have $last defined in your original scope? – Low Flying Pelican Jan 11 '16 at 22:18
  • https://docs.angularjs.org/api/ng/directive/ngRepeat says "Special properties are exposed on the local scope of each template instance, including ... " `$last` Maybe that's why it's inaccessible? – Dexygen Jan 12 '16 at 02:10
  • Yeah... the $last can be only accessible from the element scope, I have updated the answer. – Low Flying Pelican Jan 17 '16 at 01:22
  • I am away from work and am not going to be able to test this before the bounty runs out. – Dexygen Jan 17 '16 at 01:27
1

Despite being almost completely new to Angular, I am answering my own question but still want additional answers in case the way I solved my problem is not considered "idiomatic" Angular.

Specifically, instead of using an isolate scope, I leveraged the third attrs (attributes) link/function argument in my code below, to otherwise get the new columnselector attribute to the html along with my directive. Is this a generally accepted practice?

'use strict';

angular.module('cmt.cases.directives')

.directive('fixedColumnTooltip', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            if (scope.$last === true) {
                $timeout(function () {
                    element.parent().find(attrs.columnselector).each(function() {
                        var td = $(this);
                        var span = td.find('span');

                        if (span.width() > td.width()){
                            span.attr('data-toggle','tooltip');
                            span.attr('data-placement','right');
                            span.attr('title', span.html());
                        }
                    });
                });
            }
        }
    }
});

ADDENDUM: As you can see from comments I have not been able to get the code from this answer to work, despite trying it a couple of different ways. If I'm doing something wrong with regard to incorporating that answer please let me know.

In the meantime I have found another way to do it, but this is almost certainly more of a "code smell" than leveraging the attrs argument. Specifically I have discovered that I can use an isolate scope, and then access that scope's $parent scope attribute. Then I would begin my code as follows, but I am not advocating this, but rather am just noting it as it seems that TMTOWTDI (there's more than one way to do it) certainly applies to Angular:

'use strict';

angular.module('cmt.cases.directives')

.directive('fixedColumnTooltip', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
            columnselector: '@'
        },
        link: function (scope, element, attrs) {
            if (scope.$parent.$last === true) {
                $timeout(function () {
                    element.parent().find(scope.columnselector).each(function() {
                    ...
Community
  • 1
  • 1
Dexygen
  • 12,287
  • 13
  • 80
  • 147
  • Personally I think isolate scopes are overused. Using the third parameter of the link function or `$attrs` in the controller works for me. Why create a scope if you don't need it? – georgeawg Jan 08 '16 at 21:27
  • Yeah, I always use `attr` to grab from `...`, and then `attr.columnSelector`. Isolate scope not necessarily desirable, and you can't have two directives with isolate scope on a single element. – ngDeveloper Jan 11 '16 at 18:58
1

Okay so first of all just because you are using an isolate scope doesn't mean you can't access something in the parent scope. An isolate scope is designed to limit what you get by default but you can specify whatever you want from the parent scope. The correct way to do that would be to set up a two way binding in your directive using the "parentScopeVariable: '='". Forgive the horrible formatting I'm on mobile and I want to go to bed :-).

So yes, like you said you can use the "attrs" parameter too, sure. There are even tricky $eval ways of setting things on the parent scope that are only passed in as attrs. You can't have more than one directive with an isolate scope on a given element/component anyway, so you really do need to be careful about when you use isolate scope. It definitely lends itself to clean design though because you have to be deliberate about what you use in your directive. Point being, relying on attrs is fine and necessary sometimes, in my mind. Admittedly, it does feel a little hackish or whatever (thinking code smell), but I don't think there's a strong case for that.

Lastly I have spent a ton of time on the Angular API doc site and there's a ton of good stuff on there. There's a pretty good directive reference on the $compile service page. Again, mobile, sorry. If I was on a full computer I'd do nice code blocks and link the the directive ref, sorry :-). A quick google and you'll find it.

So you definitely can use an isolate scope and there are ways to pass function call backs to a directive, pass directive function references out of a directive, back to a controller, two way data-binding, etc. Isolate scope is great for all that but it doesn't sound like you'ee trying to do anything too complex.

mwallace
  • 101
  • 1
  • 3
  • This answer is helping me understand some nuances too: http://stackoverflow.com/a/14322751/34806 Don't know if you have time to look at the other answer I got, but I used the exact code and got an error :( – Dexygen Jan 11 '16 at 14:41
  • I also tried the shorthand `$last: '='` (which I learned from the docs at: https://docs.angularjs.org/guide/directive#!), and at least this did not throw an error, but also it never evaluated to true ;( – Dexygen Jan 11 '16 at 15:58
0

In the Angular framework in a HTML template you can access the parent scope..

For example:

<div ng-model="$parent.$parent.theModel"></div>

This works when you create new scopes within the template, like for example ng-repeat and the like. In theory you can use this to access the parent scope you wish to use.

Amonn
  • 84
  • 6
0

maybe a bit ugly, but working: get the DOM element of your current directive, traverse backwards to its parent, make it an angular-element, call the inbuilt scope() function on it, e.g.

link: function (scope, elem) {
  var parentScope = angular.element ($(elem).parent()).scope();
  console.log (parentScope)
}
Patrick Kelleter
  • 2,631
  • 1
  • 13
  • 19
0

Touching parent scope might not be the best idea (i mean it's not angular way to access the different layer), better having some extra scope.models. Anyway here's a simple working demo.

angular.module('app', [])
.controller('ctrl', function($scope){
  $scope.trDataWatch = ['item1', 'item2', 'item3'];
  $scope.state = 'unrendered';
  $scope.$on('ngRepeatFinished', function(){
     $scope.state = 'ngRepeatFinished';
  });
})
.directive('fixedColumnTooltip', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
          columnselector: '@',
          first: '=?',
          middle: '=?',
          last: '=?',
          index: '=?',
          odd: '=?',
          even: '=?',
          
        },
        link: function (scope, element, attr) {
          if(scope.last){
              scope.$emit('ngRepeatFinished');
          }
        }
    };
});
td {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
  <h4>{{state}}</h4>
  
<table>
  <tr fixed-column-tooltip columnselector=".td-keyField"
      ng-repeat="trData in trDataWatch"
      index="$index"
      odd="$odd"
      even="$even"
      first="$first"
      middle="$middle"
      last="$last">
    <td>{{trData}}</td>
  </tr>
</table>
</div>

But i would strongly recommend you to redesign logic

As i understand, you want to show span-tooltips only for td which a wider, you should definitely use another directive, and inside second, require first directive, so that you could use it's controller logic, or whatever. Anyway - better design would help you better, so you better think deeper

Medet Tleukabiluly
  • 11,662
  • 3
  • 34
  • 69
  • What you are suggesting sounds like overkill. Why use a nuclear bomb to kill a fly when a fly-swatter will work just fine? – Dexygen Jan 15 '16 at 20:26
0

If you want to use the controller scope with in directive you should do the following

app.directive('fixedColumnTooltip', function ($timeout) {
return {
    restrict: 'A',

    link: function (scope, element, attr) {
      var columnselector = attr.columnselector;
      console.log(scope[columnselector]);
    }
}});

This wont create any scope for the directive, and you can still access the value of columnselector. If you want to pass function in the columselector then you can do $parse(attr.columnselector).If its a value then $parse is not required.

Deepak Sharma
  • 1,873
  • 10
  • 23
0

When you define the scope in the directive you are creating an isolate scope. The easiest way to pass in the $last variable would be as another attribute:

<tr fixed-column-tooltip columnselector=".td-keyField" ng-repeat="trData in trDataWatch" last="$last">

Your directive scope would look like this:

scope: {
    columnselector: '@',
    $last: '=last'
}

OR you could simply access the parent scope within your link function:

link: function (scope, element, attr) {
    if (scope.$parent.$last === true) { // Will evaluate true one time
    }
}

In which case, you will not need another attribute nor will you need to define $last in your directive scope. JSFiddle

BMcV
  • 593
  • 4
  • 13
0

Simply. Use your attribute argument on your link function...

link: function(scope, element, attributes, ctrl) {
  var selector = attributes.columnselector;
}

I don't know why I read extensive answers, seriously guys.

Miguel Lattuada
  • 5,327
  • 4
  • 31
  • 44
  • This is what my code, wherein I answered my own question, does – Dexygen Jan 17 '16 at 23:50
  • I don't see the above code on your question. That is how you access attributes. Which is the best approach. (Unless you want two way data binding for `columnselector`) – Miguel Lattuada Jan 17 '16 at 23:54
  • It's right there in http://stackoverflow.com/a/34686077/34806 -- I use `attrs.columnselector` – Dexygen Jan 18 '16 at 13:06