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?
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?
- 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 Commented Jan 16, 2016 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 Commented Jan 17, 2016 at 18:58
9 Answers
Reset to default 3 +50You 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){
......
}
}
}
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.
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() {
...
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.
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)
}
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
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.
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
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.