Accessing attributes from an AngularJS directive - angularjs

My AngularJS template contains some custom HTML syntax like:
<su-label tooltip="{{field.su_documentation}}">{{field.su_name}}</su-label>
I created a directive to process it:
.directive('suLabel', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {
title: '#tooltip'
},
template: '<label><a href="#" rel="tooltip" title="{{title}}" data-placement="right" ng-transclude></a></label>',
link: function(scope, element, attrs) {
if (attrs.tooltip) {
element.addClass('tooltip-title');
}
},
}
})
Everything works fine, at the exception of the attrs.tooltip expression, which always returns undefined, even though the tooltip attribute is visible from Google Chrome's JavaScript console when doing a console.log(attrs).
Any suggestion?
UPDATE: A solution was offered by Artem. It consisted in doing this:
link: function(scope, element, attrs) {
attrs.$observe('tooltip', function(value) {
if (value) {
element.addClass('tooltip-title');
}
});
}
AngularJS + stackoverflow = bliss

See section Attributes from documentation on directives.
observing interpolated attributes: Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.

Although using '#' is more appropriate than using '=' for your particular scenario, sometimes I use '=' so that I don't have to remember to use attrs.$observe():
<su-label tooltip="field.su_documentation">{{field.su_name}}</su-label>
Directive:
myApp.directive('suLabel', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {
title: '=tooltip'
},
template: '<label><a href="#" rel="tooltip" title="{{title}}" data-placement="right" ng-transclude></a></label>',
link: function(scope, element, attrs) {
if (scope.title) {
element.addClass('tooltip-title');
}
},
}
});
Fiddle.
With '=' we get two-way databinding, so care must be taken to ensure scope.title is not accidentally modified in the directive. The advantage is that during the linking phase, the local scope property (scope.title) is defined.

Related

Directive to format date in angularjs?

I have a span in ng-repeat as follows:
span(humanize-date="{{file.date}}"
I'm to create a directive so that, directive changes the date format
directive('humanizeDate', [
function() {
return {
restrict: 'EA',
template: '<div value="{{formattedDate}}"/>',
replace: true,
scope: {
formattedDate: '=humanizeDate'
},
link: function(scope, elem, attrs) {
return scope.formatedDate = moment.duration(scope.humanizeDate).humanize();
}
};
}
]);
just use:
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>
here is more information about
https://docs.angularjs.org/api/ng/filter/date
hope to help
You should not use {{}} while you are using = inside your directive which indicated two way binding.
It should be direct variable reference rather than {{}}
span(humanize-date="file.date"
OR
Other way would be you could use # in your directive isolated scope as your are giving {{}} with expression to attribute which indicated one way binding.
scope: {
formattedDate: '#humanizeDate'
},

How do you pass in data to custom directives as a value of the attribute itself?

I have a directive defined as such:
angular.module("main.directives").directive("todo", function() {
return {
restrict: "A",
scope: {
todo: "=entity"
},
replace: false,
templateUrl: "todo.html",
link: function(scope, element, attributes) {
}
};
});
which I use like this from templates:
<div todo entity="todoData"></div>
todoData comes from a controller or some other the local scope. Anyway it all works like a charm, so that's cool!
My question is the following: How do I have to modify the directive definition so that it also works with a markup of this type:
<div todo="todoData"></div>
As you can see the data is now passed in as the value of the attribute marking the directive. Just like ng- directives do:
<p ng-repeat="bit in data"></p>
<p ng-click="whatever()"></p>
How can that be achieved?
Thanks
Replace
scope: {
todo: "=entity"
},
by
scope: {
todo: "=todo"
},
or simply
scope: {
todo: "="
},
When you write an attribute directive in angularjs you might want to have it fed by an attribute value.
For example, something like this:
<div my-attribute="somevalue"></div>
How then do you create a new scope that takes that in? It's not obvious. Any here's how you do it:
app.directive('myAttribute', function() {
return {
restrict: 'A',
scope: {
myAttribute: '='
},
template: '<div style="font-weight:bold">{{ myAttribute | number:2}}</div>'
};
});
The trick to notice is that the "self attribute" because of the name of the attribute in camel case.
Here is the Reference to This Answer!
you must eval the value of the attribute inself. The isolate scope is not one of my favorites kind of scopes for a directive. Instead you can use, scope = true, to inherit from the parent controller. This will allow you to use all the variable exposes on the parents scopes.
in your case.
angular.module("main.directives").directive("todo", function() {
return {
restrict: "A",
scope: true,
replace: false,
templateUrl: "todo.html",
link: function(scope, element, attributes) {
scope.todo = scope.$eval(attributes[todo]);
}
};
});
now your todo directive could be used. Like any other ng- directive.
example:
<div todo="getTodoList()"></div>
<div todo="[{description:'hahahha'}]"></div>

AngularJS add ng-model at compile time

I have previously created a directive called click-to-edit
you would use it like
<click-to-edit="someValue">
where $scope.someValue=7;
I have since realized that i need to use the ngModel controller, and it would be EASIER if i could recompile this directive and add ng-model="someValue" to the template. Im having trouble doing this as it is giving me the error
"Error: [$compile:ctreq] Controller 'ngModel', required by directive 'clickToEdit', can't be found!"
This is obviously because i have
require:'ngModel'
this is a snippet of the code so far
return {
restrict: 'A',
replace: true,
require: 'ngModel',
priority: 100,
compile: function(tElement, tAttrs, transclude) {
// Correct ngModel for isolate scope
tAttrs.$set('ngModel', tAttrs.clickToEdit, false);
}
return {
post: linkFn
};
},
scope: {
dp: '=?',
type: '#',
fn: '=?', //<--- formula
editFn:'&?' // if you want to execute a function on a valid save, add this
}
What i simply want to do, is take a directive that looks like
<click-to-edit="model"/>
and change it to
<click-to-edit="model" ng-model="model"/>
and then have it compile and work as expected.
Let me know if you need other code.
I'm not sure if you've considered this - but why not just do:
<click-to-edit ng-model="model"/>
To answer the original question, I suggest adding a body directive:
.directive('body', function() {
return {
restrict: 'E',
compile: function(element, attr) {
$('[click-to-edit]', element).attr('ng-model', 'model');
}
}
})
Plunker Here

Wrapping the angular-ui tabset directive and encountering the "Multiple directives asking for transclusion/ isolated scope" errors

I'm trying to extend the angular-ui tabset functionality and I'm running into issues with wrapping it.
This plunker is the tabset directive un-wrapped:
http://plnkr.co/edit/AhG3WVNxCal5fZOUbSu6?p=preview
This plunker contains my first attempt at wrapping the tabset directive:
http://plnkr.co/edit/naKXbeVOS8nizwDPUrkT?p=preview
The initial wrapping approach is straight-forward wrapping. But... I introduce extra divs in the replacement template to avoid the "Multiple directives asking for isolated scope" and "Multiple directives asking for transclusion" angular errors and to make sure transclusion happens.
Key code snippets:
.directive('urlTabset', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
tabManager: '='
},
controller: [ "$scope", function($scope) {
var tabManager = $scope.tabManager;
}],
template:
'<div>' +
'<tabset>' +
'<div ng-transclude>' +
'</div>' +
'</tabset>' +
'</div>'
};
})
.directive('urlTab', function() {
return {
require: '^urlTabset',
restrict: 'E',
transclude: true,
replace: true,
scope: { tabName: '#' },
link: function(scope, element, attrs, urlTabsetCtrl) {
},
template:
'<div>' +
'<tab>' +
'<div ng-transclude>' +
'</div>' +
'</tab>' +
'</div>'
};
});
However, I think the extra divs in the template are causing issues. Here is the unwrapped tabset with extra divs in the places my template would add them.
http://plnkr.co/edit/kjDs7xJcZqltCAqUSAmX?p=preview
So the logical thing is to eliminate the divs... but this is where I need the help. Does anyone know of a clean way to do this without hitting the "Multiple directives asking for isolated scope" and "Multiple directives asking for transclusion" angular errors. Here is one failed attempt.
http://plnkr.co/edit/0C6lFNhfdTVcF7ahuN3G?p=preview
Error: Multiple directives [urlTab, tab] asking for transclusion on: <tab class="ng-isolate-scope ng-scope">
BTW, in case you're wondering what I'm trying to do, my end goal is to use the tabManager attribute passed to urlTabset to auto-populate fields in the tab directive (wrapped by urlTab). To be more concrete this is what I'm aiming for:
.directive('urlTab', function() {
return {
require: '^urlTabset',
restrict: 'E',
transclude: true,
replace: true,
scope: { tabName: '#' },
link: function(scope, element, attrs, urlTabsetCtrl) {
scope.tabs = urlTabsetCtrl.tabs;
scope.tabSelected = urlTabsetCtrl.tabSelected;
},
template:
'<tab active="tabs[tabName].active" disabled="tabs[tabName].disabled" select="tabSelected(tabName)" ng-transclude>' +
'</tab>'
};
});
The template above obviously does not work, but it gives you the gist of what I'm trying to do.
And I'm okay with a solution that requires the wrapping directive not have an isolated scope. I can get around this by storing state in the controller context.
If you are trying to augment angular-ui's functionality, you may be better off doing it with attribute directives rather than brand new elements. I may be mistaken but it looks like you're not intending to alter the general stucture of the DOM other than to replace your directive with angular-ui's ones. For instance, using the HTML
<tabset url-tabset>
<tab url-tab>
<tab-heading>
<i class="icon-list"></i> Details
</tab-heading>
Details content.
</tab>
<tab url-tab>
<tab-heading>
<i class="icon-thumbs-up"></i> Impact
</tab-heading>
Impact tab content.
</tab>
</tabset>
would mean you no longer need to perform any transclusion or template replacement. This would avoid that problem all together.
This leaves the problem of isolated scope for attributes you want to use for the augmentation. Instead of using this, you can use scope: true to grab the same isolated scope as tab and tabset (though you cannot define bindings here) and you can get attributes as you would use normal bound values by using $parse and attrs.
Your directives (with the functionality from your second plunker) then end up looking something like this.
angular.module('plunker', ['ui.bootstrap'])
.directive('urlTabset', function() {
return {
restrict: 'A',
require: 'tabset', // Confirm the directive is only being used on tabsets
controller: [ "$scope", "$attrs", function($scope, $attrs) {
var tabManagerGetter = $parse($attrs.tabManager); // '='
this.getTabManager = function() {
return tabManagerGetter($scope);
};
// fun stuff here
}]
};
})
.directive('urlTab', function() {
return {
require: ['tab', '^urlTabset'],
restrict: 'A',
link: function(scope, element, attrs, ctrls) {
var urlTabsetCtrl = ctrls[1];
function getTabName() {
return attrs.tabName; // '#'
}
var tabManager = urlTabsetCtrl.getTabManager();
// fun stuff here
}
};
});

Is the compile method appropriate for altering the template dynamically in a directive?

I have a directive where I want to add a select and either a textarea or an input field depending on some input. For example
app.directive('monkey', function() {
return {
restrict: 'E',
replace: true,
template: '<div><select><option>1</option><textarea>{{text}}</textarea></div>'
scope: {
text: "="
},
link: function(element, attrs, scope) {
// Add listeners etc
}
}
});
so the html would look something like
<monkey text="model.name" type="input"></monkey>
I want to be able to look at the type attribute and change my template from
<div><select><option>1</option><textarea>{{text}}</textarea></div>
to
<div><select><option>1</option><input>{{text}}</input></div>
Is the compile function what I should be using?
Yes, you could use the compile method for this.
But, if you have a limited set, I would opt for ng-if first. Just because it keeps things slightly more simple.
So the template becomes something like:
<div><select><option>1</option><textarea ng-if="type == 'textarea'">{{text}}</textarea><input ng-if="type == 'input'">{{text}}</input></div>
So, you would need to take type into your directive as well. Here is the entire directive:
app.directive('monkey', function() {
return {
restrict: 'E',
replace: true,
template: '<div><select><option>1</option><textarea ng-if="type == 'textarea'">{{text}}</textarea><input ng-if="type == 'input'">{{text}}</input></div>'
scope: {
text: "=",
type: "#"
},
link: function(element, attrs, scope) {
// Add listeners etc
}
}
});

Resources