'attrs' property in angular directive is not working properly - angularjs

I am trying to write a very simple directive which involves setting a property based off one of the attributes that it is provided. The issue I'm facing is that the value of the attrs object is not being consistently recognized within the link function.
Here is the grand total of what I'm currently trying to achieve :
angular.module('directives').directive('wikiNotes',function() {
return {
restrict: 'EA',
templateUrl: 'common/directives/wiki-notes.tpl.html',
link: function(scope, element, attrs) {
console.log(attrs.openEdit); //true
if(attrs.openEdit===true){
console.log('open edit'); //not called
}
}
};
});
The console.log(attrs.openEdit) is showing as true, but then the console.log in the if block is not getting called. Am I missing something very obvious or is this a quirk with angular directives?

Did you consider adding this attribute in your directive scope section?
I think it is more in the Angular philosophy, but this imply you create a new scope for your directive.
angular.module('directives')
.directive('wikiNotes',function() {
return {
restrict: 'EA',
scope: {
openEdit: '='
},
templateUrl: 'common/directives/wiki-notes.tpl.html',
link: function(scope) {
console.log(scope.openEdit); //true
if(scope.openEdit===true){
console.log('open edit'); //should be a boolean
}
}
};
});
Here is a JS fiddle that demonstrate it works.
https://jsfiddle.net/c8mn9wka/3/

Related

binding data in a directive returns inconsistent values

i have this wierd problem with binding data to a directive. this is how i declare my directive:
<my-directive data="myArray"></my-directive>
my directive code looks like:
angular.module('ngApp')
.directive('myDirective', function () {
return {
scope:{
data: '='
},
template: '<div steps="data.length"></div>',
restrict: 'E',
link: function postLink(scope, element, attrs) {
console.log(scope);
console.log(scope.data);
}
};
});
in the first log, the data property is correct:
screenshot of console.log output
but the second log is undefined.
any idea why?
here is your solution :
<my-directive data-example="myArray"></my-directive>
you should always name your variables data-<name>. This is far easier to maintain and this prevent errors with keywords like this one ;)
angular.module('ngApp')
.directive('myDirective', function () {
return {
scope:{
data: '=example'
},
template: '<div steps="data.length"></div>',
restrict: 'E',
link: function postLink(scope, element, attrs) {
console.log(scope);
console.log(scope.data);
}
};
});
i hope this help,
Live example : JsFiddle
thanks to #hadiJZ and #Unex i figured it out:
my directive was nested in another directive. but the parent directive used the link function for the logic, since on creation it wasn't having child directives.
so moving the logic out to a controller solved my problem.
i figured it out when i added a $timeout to the child directive, and the log (scope.data) was correct.

Binding global scope variable to directive local variable

I'm having trouble understanding scope linking between controllers and directives.
What I'm trying to do (which should help me learn a lot) is bind $scope.systems in my controller to data in my directive.
So I setup a simple directive call:
<combobox data="systems"></combobox>
I also tried binding the variable, but it didn't make sense to me.
<combobox data="{{systems}}"></combobox>
Then I created my driver as such
.directive('combobox', function ($timeout) {
return {
restrict: 'E',
replace: true,
templateUrl: '/angular/directives/combobox.php',
link: function (scope, element, attrs) {
console.log(attrs.data);
$timeout(function () {
console.log(scope.systems);
console.log($scope[attrs.data]);
}, 1000);
}
}
});
I considered adding a scope parameter to the directive return
scope: {
'systems': '='
}
Or
scope: {
'systems': '=data'
}
I've been able to setup simple directives where values are bound to the directive scope, and they've worked. Now I'm trying to create a reusable directive where I can tell it what data from the controller scope to use, and I'm just stuck.
This should work. Although I'm not sure why your template is a php file...
<combobox data="foo"></combobox>
<combobox data="bar"></combobox>
app.directive('combobox', function ($timeout) {
return {
restrict: 'E',
replace: true,
scope: {
//this will set $scope.systems
//with the value gotten from evaluating what is in
//the data attribute
'systems': '=data'
},
templateUrl: '/angular/directives/combobox.php',
link: function (scope, element, attrs) {
console.log(scope.systems);
}
}
});
BTW, don't use replace. The Angular team said it will probably disappear soon because it is causing too many issues and is not that necessary anyway.

$watch in a link function of my directive is not updated

I have this directive which tries to watch for changes in a json object on scope. The JSON object is retrieved using a restangular based service, but somehow the $watch seems to be executed only once, logging 'undefined'.
The directive is used in the index.html of the app, so I suspect this has to do with the controller only working for the specific view or form...is there a way to get the directive to see those changes?
update: figured I could just call the TextsService from the directive itself, seems like a good solution to the problem. If anyone has better suggestions I'd welcome them still though.
service:
angular.module('main').service('TextsService', function(Restangular) {
this.getTexts = function(jsonRequestBase, jsonRequest, callback) {
Restangular.one(jsonRequestBase, jsonRequest).get().then(
function(texts) {
callback(texts);
}
);
};
});
call in controller:
TexstService.getTexts("content", "file.json", function (texts) {
$scope.mytest = texts;
});
directive:
app.directive('myDirective',
function() {
return {
restrict: 'A',
templateUrl:'test.html',
transclude: true,
link: function(scope, elm, attrs) {
scope.$watch('mytest', function(){
console.log(scope.mytest);
}, true);
I figured I could just call the TextsService from the directive itself, seems like a good solution to the problem. If anyone has better suggestions I'd welcome them still though.
Create the variable with null before receving it. Maybe that works.
The problem is that your directive's scope does not know about the mytest variable. You need to define the binding when you define the directive:
app.directive('myDirective',
function() {
return {
scope: {
myDirective: '='
},
restrict: 'A',
templateUrl:'test.html',
transclude: true,
link: function(scope, elm, attrs) {
scope.$watch('myDirective', function(newVal){
console.log(newVal);
}, true);
};
}
);
This will get whatever variable is assigned to the directive attribute and watch its' value.
And your directive in the view should look like this to bind it to the 'mytest':
<div my-directive="mytest"></div>

Access Parent Scope in Transcluded Directive

I would like to access a parent directive's scope, but I can't seem to get the right combination of settings. Is this possible and is it the right approach?
I really want to avoid putting something like SOME_CONST (which would help me make DOM updates through control flow) in MyCtrl
<div ng-controller="MyCtrl">
<parent>
<child></child>
</parent>
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
link: function(scope, elem, attrs) {
scope.SOME_CONST = 'someConst';
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
Please see this fiddle
Thanks
With transclude: true and scope: true, the parent directive creates two new scopes:
Scope 004 is a result of scope: true, and scope 005 is a result of transclude: true. Since the child directive does not create a new scope, it uses transcluded scope 005. As you can see from the diagram there is no path from scope 005 to scope 004 (except via private property $$prevSibling, which goes in the opposite direction of $$nextSibling -- but don't use those.)
#joakimbl's solution is probably best here, although I think it is more common to define an API on the parent directive's controller, rather than defining properties on this:
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.getConst = function() {
return $scope.SOME_CONST;
}
}
Then in the child directive:
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.getConst();
},
This is how the tabs and pane directives work on Angular's home page ("Create Components" example).
Normally the way you access a parent scope variable in a directive is through bi-directional binding (scope:{model:'=model'} - see the angular guide on directives) in the directive configuration), but since you're using transclusion this is not so straight forward. If the child directive will always be a child of the parent directive you can however configure it to require the parent, and then get access to the parent controller in the child link function:
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
$scope.SOME_CONST = 'someConst';
this.SOME_CONST = $scope.SOME_CONST;
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
scope.SOME_CONST = parentCtrl.SOME_CONST;
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{SOME_CONST}}. I really don\'t want to put everything inside the MyCtrl',
}
});
See this update: http://jsfiddle.net/uN2uv/
I just had the same problem and finally solved it with the angular manual ;)
In short: you need to use a controller in your parent directive and require that controller in your child directive. This way you are able to get your parent properties.
See https://docs.angularjs.org/guide/directive
Chapter: Creating Directives that Communicate
I changed your fiddle to use a controller, now you can access your constant:
https://jsfiddle.net/bbrqdmt3/1/
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.obj = {prop:'foo'};
}
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div ng-transclude><h1>I\'m parent {{obj.prop}}<h1></div>',
controller: function($scope) {
this.getConst= function() {
return 'someConst';
}
},
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require : '^parent',
link: function(scope, element, attrs, ctrl) {
scope.value= ctrl.getConst();
},
template: '<h1>I\'m child.... I want to access my parent\'s stuff, but I can\'t. I can access MyCtrlScope though, see <b>{{obj.prop}}</b></h1> how can I access the <b>SOME_CONST</b> value in my parent\'s link function? is this even a good idea? {{value}}. I really don\'t want to put everything inside the MyCtrl',
}
});
There's a transclude fn in the arguments of the link fn after the controller.
myApp.directive('parent', function() {
return {
scope: true,
transclude: true,
restrict: 'EA',
template: '<div><h1>I'm a parent header.</h1></div>',
link: function (scope, el, attrs, ctrl, transclude) {
transclude(scope, function (clone, scope) {
element.append(clone); // <-- will transclude it's own scope
});
},
controller: function($scope) {
$scope.parent = {
binding: 'I\'m a parent binding'
};
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
require:'^parent',
scope:true,
link:function(scope,element,attrs,parentCtrl){
},
template: '<div>{{parent.binding}}</div>' // <-- has access to parent's scope
}
});

How to hide element if transcluded contents are empty?

I created a very simple directive which displays a key/value pair. I would like to be able to automatically hide the element if the transcluded content is empty (either zero length or just whitespace).
I cannot figure out how to access the content that gets transcluded from within a directive.
app.directive('pair', function($compile) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div><span>{{label}}</span><span ng-transclude></span></div>"
}
});
For example, I would like the following element to be displayed.
<pair label="My Label">Hi there</pair>
But the next two elements should be hidden because they don't contain any text content.
<pair label="My Label"></pair>
<pair label="My Label"><i></i></pair>
I am new to Angular so there may be a great way handle this sort of thing out of the box. Any help is appreciated.
Here's an approach using ng-show on the template and within compile transcludeFn checking if transcluded html has text length.
If no text length ng-show is set to hide
app.directive('pair', function($timeout) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div ng-show='1'><span>{{label}} </span><span ng-transclude></span></div>",
compile: function(elem, attrs, transcludeFn) {
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcludded*/
var show=clone.text().length?'1':'0'
attrs.ngShow=show;
});
}
}
});
Plunker demo
Maybe a bit late but you can also consider using the CSS Pseudo class :empty.
So, this will work (IE9+)
.trancluded-item:empty {
display: none;
}
The element will still be registered in the dom but will be empty and invisible.
The previously provided answers were helpful but didn't solve my situation perfectly, so I came up with a different solution by creating a separate directive.
Create an attribute-based directive (i.e. restrict: 'A') that simply checks to see if there is any text on all the element's child nodes.
function hideEmpty() {
return {
restrict: 'A',
link: function (scope, element, attr) {
let hasText = false;
// Only checks 1 level deep; can be optimized
element.children().forEach((child) => {
hasText = hasText || !!child.text().trim().length;
});
if (!hasText) {
element.attr('style', 'display: none;');
}
}
};
}
angular
.module('directives.hideEmpty', [])
.directive('hideEmpty', hideEmpty);
If you only want to check the main element:
link: function (scope, element, attr) {
if (!element.text().trim().length) {
element.attr('style', 'display: none;');
}
}
To solve my problem, all I needed was to check if there were any child nodes:
link: function (scope, element, attr) {
if (!element.children().length) {
element.attr('style', 'display: none;');
}
}
YMMV
If you don't want to use ng-show every time, you can create a directive to do it automatically:
.directive('hideEmpty', ['$timeout', function($timeout) {
return {
restrict: 'A',
link: {
post: function (scope, elem, attrs) {
$timeout(function() {
if (!elem.html().trim().length) {
elem.hide();
}
});
}
}
};
}]);
Then you can apply it on any element. In your case it would be:
<span hide-empty>{{label}}</span>
I am not terribly familiar with transclude so not sure if it helps or not.
but one way to check for empty contents inside the directive code is to use iElement.text() or iElement.context object and then hide it.
I did it like this, using controllerAs.
/* inside directive */
controllerAs: "my",
controller: function ($scope, $element, $attrs, $transclude) {
//whatever controller does
},
compile: function(elem, attrs, transcludeFn) {
var self = this;
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcluded*/
var showTransclude = clone.text().trim().length ? true : false;
/* I set a property on my controller's prototype indicating whether or not to show the div that is ng-transclude in my template */
self.controller.prototype.showTransclude = showTransclude;
});
}
/* inside template */
<div ng-if="my.showTransclude" ng-transclude class="tilegroup-header-trans"></div>

Resources