Is it possible to pass 'required' into an AngularJS directive? - javascript

I'm making a custom auto-complete directive that uses an <input> within itself, but I'm having a bit trouble figuring out how to pass the 'required' attribute down, other attributes that have values I can see but 'required' appears to be blank whether it is set or not. The first part of my return statement below:
return {
restrict: 'E',
template: tpl,
replace: true,
scope: {
suggestionsPath: '=autoComplete',
method: '#method',
term: '#term',
required: '#required',
ngModel: "="
}...
thanks!

I've built a few extensions to inputs and the best (arguably) only way to extend existing ngModel bindings is using the ngModelController in your directive. You can require another directive's controller by using the "require" property. The documentation for ngModelController is here
This will allow you to get/set the model values as well as extend or replace the validation behavior as needed. Because you are now probably extending in combination AngularJS input directives you will also probably want to look at the input directives inside AngularJS for examples of how this work. They also can work in tangent with the ngFormController as a parent for the whole form. This took me a while to grasp so be patient but it is by far the best way to do this.
I would avoid isolate scopes here, they can be tricky, don't always play well with other directives (so typically only use it on new elements or things where only one directive will exists on it's own). I would design something like this:
return {
restrict: 'E',
template: tpl,
replace: true,
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
// Use attrs to access values for attributes you have set on the lement
// Use ngModelController to access the model value and add validation, parsing and formatting
// If you have an attribute that takes an expression you can use the attrs value along with $scope.$watch to check for changes and evaluate it or the $parse service if you just want to evaluate it.
}
I recommend getting as familiar as you can with directive design as custom inputs can get pretty tricky depending on what they do (we have built custom inputs that add +/- buttons as well as ones that format numbers as percentages, currencies or just numbers with commas while you type into them). Aside from the ngModelController docs these are useful to review:
Egghead.io tutorials
The AngularJS code for input directives and forms

Required is a bit of a funny attribute by itself (see here Setting an attribute named "required" and any value, with JQuery, doesn't work). You will likely have a lot of trouble passing any sort of value through on this as its effect is determined by whether it is present, not by its value. Different browsers will treat it differently and may rewrite the value.
You will also have trouble because both required and ngModel are directives that will be compiled on your element if they are provided. required will talk to ngModel and you will need to implement ngModel properly if you want things to work.
A simpler option is to rename required and ngModel to other names. e.g. myRequired and myNgModel. You can then bind ng-model directly to scope.myNgModel and bind an ng-required to myRequired.

I know this is an old question, but for others who come looking (which is how I ended up here):
you can pass the required tag to the directive to be read as a boolean, and then use that value in ng-required.
return {
restrict: 'E',
template: tpl,
replace: true,
scope {
required:'#'
}
Then in your directive template you would use it as
<input type="text" ng-required="required" />
Either the attribute 'required' is found and is then set to true in the directive, or the attribute is not found which will be interpreted by ng-required as a false value

Related

Understanding this AngularJS directive?

I have a directive that is currently working to change a HTML class element after you've scrolled passed a part of the page. I've essentially just hacked the code together from what I found on the internet and I am having trouble understanding why or how it is working. I know if I can understand it better I can attempt to recreate it for more meaningful aspects of my project. I would appreciate any insight someone could give. Below is the Angular part:
myApp.directive('changeClassOnScroll', function ($window) {
return {
restrict: 'A', // What does this line do?
scope: {
offset: "#", // A bit confused here
scrollClass: "#" // a bit confused here
},
link: function(scope, element) {
angular.element($window).bind("scroll", function() { // understood
if (this.pageYOffset >= parseInt(scope.offset)) { // understood
element.removeClass(scope.scrollClass); // understood
console.log('Scrolled below header.');
} else {
element.addClass(scope.scrollClass); // understood
}
});
}
};
})
In the HTML;
<nav change-class-on-scroll offset="100" scroll-class="navbar-transparent" class="navbar">
<!-- Don't understand why this works at all since these two elements are
<!-- not the same names as the function above? How does the directive
<!-- know to look for 'scroll-class' even tho in the directive it is
<!-- 'scrollClass' ?
Any help would really be much appreciated as to what is going on with it.
From the documentation
At a high level, directives are markers on a DOM element (such as an
attribute, element name, comment or CSS class) that tell AngularJS's
HTML compiler ($compile) to attach a specified behavior to that DOM
element (e.g. via event listeners), or even to transform the DOM
element and its children.
What you wrote is a standard angularjs code to create a custom directive that adds some functionalities to your dom.
restrict: 'A', // What does this line do?
'A' stands for attribute. Which means you can use this as an attribute of an html element like you used for your nav. You can use any of the following restrictions in a directive.
A - Attribute => <div change-class-on-scroll></div>
C - Class => <div class="change-class-on-scroll"></div>
E - Element => <change-class-on-scroll data="book_data"></change-class-on-scroll>
M - Comment => <!--directive:change-class-on-scroll --><br/>
scope: {
offset: "#", // A bit confused here
scrollClass: "#" // a bit confused here
},
'#' is used here to bind the data from your html to directives scope. With offset="100", you are making the value 100 to be available in the directives scope, and then when you call scope.offset in your link function, you'll get the value. You can use '#', '=' or '&' to bind values to the directive based on whether it is a definite value, model data or a function.
why scroll-class when in directive it is scrollClass
It works because that's how it should be. By Angularjs naming convention, the directive name and the scope objects to bind should be in camel case in your js and should be written using dashes in your html.
restrict: 'A', // this will restrict directive only as attribute(you can only use it as attribute or it defines directive type)
scope: {
offset: "#", // # means that the changes from the controller scope will be reflected in the directive scope but if you modify the value in the directive scope, the controller scope variable will not get affected.
scrollClass: "#"
},
There are Four types of directive elements (E), attributes (A), class names (C), and comments (M).A directive can specify which of the 4 matching types it supports in the restrict property of the directive definition object.
'#' is used to pass simple values in a directive.
In Html we cannot use camel case. Therefor we use snake case instead of camel case. Hence scrollClass will be written as scroll-class.

AngularJS formatter in nested directive

I have a really weird Problem about formatters in nested directives.
I want a modelType directive for formatting.
If I don't check the "modelType", it works.
All View-Values are changed to "Formatted: ...".
But if i implement the if (attrs.modelType == "testType") { ... } it wont work, but i don't know why.
myApp.directive('modelType', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
if(attrs.modelType == "testType") {
ngModel.$formatters.push(function(value){
//formats the value for display when ng-model is changed
return 'Formatted: ' + value;
});
ngModel.$parsers.push(function(value){
//formats the value for ng-model when input value is changed
return value.slice(11);
});
}
}
};
Does anyone know this Problem?
http://jsfiddle.net/nkop2uq0/2/
First, using the attribute model-type both in the outer and inner directive led to the inner's link function getting executed twice (once for the controller-owned HTML, once for the template HTML), overwriting your formatting. So you need (or at least I think you should) disentagle the attribute for the two directives by using a different attribute for the inner directive:
template: '<div><input type="text" ng-model="ngModel" my-type="modelType" /></div>'
and consequently rename the inner directive to
myApp.directive('myType', function(){
Next, since the inner directive now does no longer get called by the compile step for the outer one, you need to compile the template for the inner directive in your post-link function
This can be done like this:
link: function (scope, element){
$compile(element.innerHtml)(scope)
}
This leads to attrs being always the same, so you can't use it to test for your testType:
$$element: U[1]
$$observers: Object
$attr: Object
myType: "modelType" <-- see here
ngModel: "ngModel"
type: "text"
Therefore you need to to look for the actual value inside scope:
if(scope.modelType == "testType") {
Lastly (and frankly I would be happy if someone could explain this to me, I don't understand it), I had to define the modelType scope property in the outer directive as one-way bound via
modelType: '#modelType'
I put this together in an updated fiddle here: http://jsfiddle.net/nkop2uq0/8/
NB: I am far from understanding the intricacies of Angular's directives. As much as I like the framework, the number of ways things can be achieved is mind-boggling. So you should try to improve my answer, it is most likely not best practice. And drop me a line if you find bad ideas in there...

How to get the original attribute name before it was normalized/denormalize an attribute name?

I want to create a directive that I can use on <select> elements, to tell it to populate the <select> with a globally known list that's dynamically updated and shared among all the components in the app.
I envision using it like so:
<select ng-model="listentry" select-the-list></select>
Here's how I'm going about it so far:
.directive('selectTheList', function ($compile, ListData) {
return {
restrict: 'A',
priority: 1000,
terminal: true,
link: function (scope, el, attributes) {
var child = scope.$new();
child.listData = ListData.theList;
el.attr('ng-options', 'listItem.name for listItem in listData track by listItem.id');
el.removeAttr('select-the-list'); /**** ATTENTION ****/
$compile(el)(child);
}
};
});
That is, I assign an ng-options attribute that does what I want, based on the scope that I set up for this purpose, and then $compile it.
This works great. But note the line I commented with ATTENTION: This assumes that the user used <select select-the-list>, which then got normalized to selectTheList and used this directive. However, according to the directive docs:
Angular normalizes an element's tag and attribute name to determine which elements match which directives. We typically refer to directives by their case-sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case-insensitive, we refer to directives in the DOM by lower-case forms, typically using dash-delimited attributes on DOM elements (e.g. ng-model).
The normalization process is as follows: [... snip ...]
For example, the following forms are all equivalent and match the ngBind directive:
<div ng-controller="Controller">
Hello <input ng-model='name'> <hr/>
<span ng-bind="name"></span> <br/>
<span ng:bind="name"></span> <br/>
<span ng_bind="name"></span> <br/>
<span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"></span> <br/>
</div>
That is, if a user does <select select:the:list>, then the directive will be applied, element.removeAttr('select-the-list') will not work, and I'll get an infinite loop.
This may be an XY problem, which is why I provided all this context. But if this is a good way to do it - what's the best way to find the actual attribute on the element that caused my directive to be called, so I can remove it before re-compiling it?
The creators of angular did indeed envision the need for this. The attributes passed into your link function is not just a map, but an instance of $compile.directive.Attributes. It contains an $attr property:
Properties
$attr
A map of DOM element attribute names to the normalized name. This is needed to do reverse lookup from normalized name back to actual name.
Thus, your ATTENTION line should be:
el.removeAttr(attributes.$attr['selectTheList']);
This is XY problem indeed, because the subject is avoiding recursive compilation.
There are some tricks to handle this.
One of them is to make use of the fact that the only place where DDO object is available as this is compile:
...
compile: function (element, attrs) {
attrs.$set(this.name, null);
// so the appearance of 'compile' won't require nesting link fn
return this.link;
},
link: ...
And changing directive's name or pasting its code won't cause inconsistencies.
Another one is more generic and is particularly useful if removing an attribute isn't applicable for some reason or the directive isn't an attribute:
link: function (scope, element) {
...
if (element.data('$recursion')) {
element.data('$recursion', false);
} else {
element.data('$recursion', true);
$compile(element)(scope);
}
}

Angular directive not executed on ionic modal [duplicate]

I'm trying to implement a d3 directive in Angular, and it's hard because visually nothing is happening, and no errors are being thrown on the console.
Here's my d3 directive:
myApp.directive('d3-bars', ['d3Service', function($window, d3Service) {
return {
restrict: 'EA',
scope: {},
link: function(scope, element, attrs) {
// More code below ....
Here is my HTML:
<d3-bars bar-height="20" bar-padding="5"></d3-bars>
At first I thought it wasn't appending an svg, because inspecting the element that's what it looks like, but now I don't think the directive is even running at all. I stuck a console.log inside of it at the very beginning and it didn't appear either. Am I missing something simple?
EDIT:
I tried changing the top line to
angular.module('myApp.directives', ['d3'])
.directive('d3-bars', ['d3Service', function($window, d3Service) {
But that didn't work either. I don't even know what's the difference between the two headers anyway...
Your directive name may be wrong. Angular directives are commonly camel-cased. And when in the HTML they are hypenated. so ngClass turns into ng-class in the HTML.
At least when I've tried to use - or other characters in my directives it hasn't worked.
Check out this Google Group post for some validity: using dash in directive
Also here are the docs: Directives - matching directives
You'll also want to make the change that was suggested in the comments by JoshSGman:
.directive('d3Bars',['$window', 'd3Service', function($window, d3Service) {
the naming of your directive is the problem. Angular normalizes the names of directives in the html before it matches them to the names in JavaScript. The normalization process works in two steps:
Strip x- and data- from the front of the element/attributes.
Convert the colon-, hyphen-, or underscore-delimited name to camelCase.
So, the correct name for your directive in JavaScript would be d3Bars. Change it to that and it should work.
See https://docs.angularjs.org/guide/directive#matching-directives for more information.
I've had similar behavior when I forgot to define the link property.
No errors in the console, nothin.
One more thing happened to me that made the directive not to work at all
when you have one module have a directive and you created a new module with a new directive but due to the copy-paste you forget change Module name, this way AngulerJs somehow will remove the first directive
so
make sure that every module have unique name

AngularJS directive not being called

I'm trying to implement a d3 directive in Angular, and it's hard because visually nothing is happening, and no errors are being thrown on the console.
Here's my d3 directive:
myApp.directive('d3-bars', ['d3Service', function($window, d3Service) {
return {
restrict: 'EA',
scope: {},
link: function(scope, element, attrs) {
// More code below ....
Here is my HTML:
<d3-bars bar-height="20" bar-padding="5"></d3-bars>
At first I thought it wasn't appending an svg, because inspecting the element that's what it looks like, but now I don't think the directive is even running at all. I stuck a console.log inside of it at the very beginning and it didn't appear either. Am I missing something simple?
EDIT:
I tried changing the top line to
angular.module('myApp.directives', ['d3'])
.directive('d3-bars', ['d3Service', function($window, d3Service) {
But that didn't work either. I don't even know what's the difference between the two headers anyway...
Your directive name may be wrong. Angular directives are commonly camel-cased. And when in the HTML they are hypenated. so ngClass turns into ng-class in the HTML.
At least when I've tried to use - or other characters in my directives it hasn't worked.
Check out this Google Group post for some validity: using dash in directive
Also here are the docs: Directives - matching directives
You'll also want to make the change that was suggested in the comments by JoshSGman:
.directive('d3Bars',['$window', 'd3Service', function($window, d3Service) {
the naming of your directive is the problem. Angular normalizes the names of directives in the html before it matches them to the names in JavaScript. The normalization process works in two steps:
Strip x- and data- from the front of the element/attributes.
Convert the colon-, hyphen-, or underscore-delimited name to camelCase.
So, the correct name for your directive in JavaScript would be d3Bars. Change it to that and it should work.
See https://docs.angularjs.org/guide/directive#matching-directives for more information.
I've had similar behavior when I forgot to define the link property.
No errors in the console, nothin.
One more thing happened to me that made the directive not to work at all
when you have one module have a directive and you created a new module with a new directive but due to the copy-paste you forget change Module name, this way AngulerJs somehow will remove the first directive
so
make sure that every module have unique name

Categories