I'm attempting to use Angular directives to create custom input field types. For example, I have a type called "duration" which draws three separate hours, minutes, and seconds fields. I'm trying to use the directive as an attribute of an input element, and the directive replaces the input element.
For example, in the following code:
<input duration>
Would be rendered as a <div> with several inputs inside of it, and the original input would be out of the picture.
I'm running an ngRepeat loop through several form fields of different types, including duration. I'd like to find a way that only requires me to put one input in the HTML, with the duration attribute applied only if the field is supposed to be of duration type. I tried the following:
<div ng-repeat="field in fields">
<input type='{{field}}' ng-attr-duration="field==='duration'">
</div>
The problem with that code is that every element is rendered as duration because ng-attr-duration gets evaluated to duration='false' when the field is not duration, which triggers my directive.
Is there a way for me to apply the directive conditionally without having to define multiple <input>s to reduce redundancy in my code?
Take a look at what the directive docs say about ng-attr:
When using ngAttr, the allOrNothing flag of $interpolate is used, so if any expression in the interpolated string results in undefined, the attribute is removed and not added to the element.
Note that you need to be using curly braces so Angular will interpolate the expression. And, if your expression has an undefined term in it, the attribute won't be added.
How about something like this:
<div ng-repeat="field in fields">
<input type='{{field}}' ng-attr-duration="{{field==='duration' ? true : undefined}}">
</div>
The true in the ternary expression can be whatever you want, just not undefined.
What i can think of you can use condition if-else of ng-switch
<div ng-repeat="field in fields">
<div ng-switch on="field">
<input ng-switch-when="true" type='{{field}}' ng-attr-duration="field==='duration'">
<input ng-switch-default="true" type='{{field}}'>
</div>
</div>
It will also maintain readability of code.
Related
I'm trying to go with the best approach and avoid unnecessary rendering/processing time in my AngularJS app when choosing between 2 directives to be displayed in the page inside an ngRepeat loop, want to know which is the best way:
If by setting the ng-if directly in the directive html element, like:
<div ng-repeat="element in list">
<my-directive-a ng-if="someFunction(element)"></my-directive-a>
<my-directive-b ng-if="!someFunction(element)"></my-directive-b>
</div>
Or by moving out the first <div> from the directive's template and use it as a wrapper for each directive. For instance:
<div ng-repeat="element in list">
<div ng-if="someFunction(element)">
<my-directive-a></my-directive-a>
</div>
<div ng-if="!someFunction(element)">
<my-directive-b></my-directive-b>
</div>
</div>
NOTE: The starting <div> element on each directive could be modified behave the same so I will basically take that out of the directive's html and moving it outside the directive declaration in order to place the ng-if there
What would be the best approach for this case? Are there any performance implications from doing it one way or another? Or is it just the same thing? Consider that the number of elements in the list could get really big.
They are quite the same, but you can improve performance with one-time binding, but only when element does not change at runtime (for example, let's say that it has property name, and your someFunction is like return element.name === 'John'). Angular just stop observing this function when it returns value, and watches will be deleted. There are 2 prerequisites to use this solution:
Elements properties in list does not change (if you rely on them in someFunction), for example if you rely on name property name must not change, because watcher on someFunction is note available.
When list changes or its elements properties change, you reload all list (for example, you fetch it from server again if you know that change occurred)
What you get with this? There is no watches after my-directives are drawn on ng-ifs, and when something changes, new reference is bound to list (for example, it comes from server) and everything will be redrawn, ng-ifs will run again and when will become stable (function returns value) then will be unbound. How it looks like? Like this:
<div ng-repeat="element in list">
<div ng-if="::(someFunction(element))">
<my-directive-a></my-directive-a>
</div>
<div ng-if="::(!someFunction(element))">
<my-directive-b></my-directive-b>
</div>
</div>
Two colons before expression. But be aware, that with one-time binding it's easy to mess up - you need to be sure that you test your code enough to be sure it works.
Actually I'm confused between when to use {{ }} when using angular directives and when to not to use {{ }}
For example:
<div data-ng-init="isHidden=false">
<div data-ng-show="isHidden">
...
</div>
</div>
and
<div data-ng-init="isHidden=false">
<div data-ng-show="{{isHidden}}">
...
</div>
</div>
I'm confused between these syntax ? What are the differences between those? And when to use what? Thanks in advance :)
There is no difference except the "look" u need to use the {{value}} syntax in case you want to write data anywhere in your html body
<div>{{value}}</div>
It's all explained here: Difference between double and single curly brace in angular JS?
For quick answer:
{{}} are Angular expressions and come quite handy when you wish to
write stuff to HTML
Don't use these at a place that is already an expression!
For instance, the directive ngClick treats anything written in between
the quotes as an expression
<div data-ng-init="isHidden=false">
<div data-ng-show="isHidden">
...
</div>
</div>
In This Situation data-ng-show = false , Takes From data-ng-init As Statically,if You Have Given true Then It Returns True .
But Here
<div data-ng-init="isHidden=false">
<div data-ng-show="{{isHidden}}">
...
</div>
{{}} Called As Expressions In Angular One Of The Most Important Concept
It Directly Evaluate If isHidden = true Or False Based On Any Condition Written In Your App.js File .
Example:
<div data-ng-init="isHidden=YourVariable">
<div data-ng-show="{{isHidden}}">
...
</div>
if(YourVariable == true){
Do Somthing
}
else{
Do Something
}
If you are asking when to use {{}} while assigning value to a attribute and when not.
It depends on the binding types of directive. '#' or '='
So here, you have to use:
data-ng-show="{{isHidden}}" if the binding type of directive scope data-ng-show is '#', that mean the data-ng-show will be expecting a string value. So in this case if you keep data-ng-show="isHidden" it will take data-ng-show's value as 'isHidden', but data-ng-show="{{isHidden}}" will take the value of the $scope.isHidden and assign to data-ng-show.
Now if the binding type of directive scope data-ng-show is '=', that means the data-ng-show will be expecting a value from a scope. So data-ng-show="isHidden" itself will take the value of he $scope.isHidden and assign to data-ng-show.
Note: all the default HTML attributes expect a string so you have to use {{}} for default HTML attributes.
There is no as such major difference unless one uses them in the DOM for the value.
When one uses the following:
<div data-ng-show="isHidden">
then, expression is evaluated and on the basis of it respective value, the ng-show either hides or displays the div. But the value of the isHidden cannot be seen, when one inspects the HTML using the browser developer tool.
When one uses the following:
<div data-ng-show="{{isHidden}}">
In this case, the value of the isHidden can be seen from the developer tools, and the rest of the expression does evaluates the same as that of (1).
I am creating a directive in which template I need to use the a scope's variable value as the name of the directive (or alternatively controller) to load.
Say I have a directive widget that has a template called widget.html which looks like:
<div class="widget widget.type" {{widget.type}} ng-controller="widget.type">
<div class="navBar">
<div ng-include="widget.type + '-t.html'"></div>
<i class="fa fa-close"></i>
<hr>
</div>
<div ng-include="widget.type + '-f.html'"></div>
</div>
Now widget.type is not getting evaluated in the first line. It works fine for ng-include. Say widget.type's value is weather. The first line should then be interpolated first to look like (doesn't matter if class attribute, widget.type-attr or ng-controller is interpolated)
<div class="widget" weather>
and then compiled to include the weather directive.
How can I get widget.type interpolated in the template?
Not an option is to use ng-include to load the directive. I need to use one common template for the widget and want to add/override/extend the base directive with additonal functionality/Variables.
If this is not the way to achieve that, is there a way to extend a directive the OOP-way?
See the plunkr
You can only place interpolation expressions in text nodes and attribute values. AngularJS evaluates your template by first turning it into DOM and then invoking directive compilation, etc. If you try to place {{...}} instead of attribute name, you'll just end up with messed-up DOM.
If you really need to replace a whole directive based on $scope variable value, you'll need to create a directive for application of other directives and do some heavy lifting with $compile (you'll have to completely re-compile the template each time the value changes). I'd recommend trying to find other designs solving your situation before attempting this.
For adjusting your template based on element attributes, see this answer.
<div ng-if="question.type =='options'" ng-repeat="option in question.options" >
<ion-radio name="{{$parent.question.id}}"
ng-model="$parent.question.answer"
required ng-value="option.id"
ng-class="{'has-custom-error':submitted && questionForm.{{$parent.question.id}}.$invalid,
'has-custom-success':questionForm.{{$parent.question.id}}.$valid }"
>
{{option.text}}
</ion-radio>
</div>
I have to add has-custom-error on user submitting the form without selecting the option.That is form is not valid but the ng-classis not getting evaluated.
The demo code is over here:http://plnkr.co/edit/FwjoCTYONvi3BoFHNKFK?p=preview
I don't think it is possible to use {{ }} expressions on a ng-class expression. You should be able to use it like this questionForm[$parent.question.id]
Also I never got ng-class working with classes with - between the parts, only CamelCase like hasCustomError. So {hasCustomError: questionForm.$invalid} should work.
Have you considered using the styleClass ng-invalid-required that is already used by angular for your styling? It is usually a lot cleaner, since there is no need for all the ng-class expressions
There is at least one problem with your code. You must not use interpolation {{}} within your ng-class attribute. Here is the correct syntax:
ng-class="{'has-custom-error':submitted && questionForm[$parent.question.id].$invalid, 'has-custom-success':questionForm[$parent.question.id].$valid }"
When you are inside an Angular expression, you don't need the mustache braces. So your code should be:
<div ng-if="question.type =='options'" ng-repeat="option in question.options">
<ion-radio name="{{$parent.question.id}}"
ng-model="$parent.question.answer"
required ng-value="option.id"
ng-class="{'has-custom-error':submitted && questionForm[$parent.question.id].$invalid,
'has-custom-success':questionForm[$parent.question.id].$valid }">
{{option.text}}
</ion-radio>
</div>
The variable form a.b has been replaced by the indexer form a["b"] so that it is evaluated correctly.
You can't use double-mustached expressions ({{ ... }}) inside expressions. The ng-class value is already an expression of type object, so you're supposed to use JavaScript syntax inside this attribute:
ng-class="{'has-custom-error': submitted && questionForm[$parent.question.id].$invalid,
'has-custom-success': questionForm[$parent.question.id].$valid }"
The use of $parent is also unecessary. BTW, you're not using it for your ng-repeat expression:
ng-repeat="option in question.options"
So you should simply use question instead of $parent.question: the scope of the ng-repeat block inherits from its parent scope, and the question is thus available through inheritance.
Note that, playing with your plunkr, it seems like the ion-radio directive that you're using doesn't correctly set the name attribute of the input it generates. This is probably why the CSS classes don't appear.
ng-form is the solution to my problem.But the problem that I am facing is in the name attribute of my ion-radio directive. For radio hence i used angular ng-invalid-required.And for other html tag i used ng-form.$invalid
Like in this question, I want to add .error on a form field's parent .control-group when scope.$invalid is true.
However, hardcoding the form name like in ng-class="{ error: formName.fieldModel.$invalid }" means that I can't reuse this in different forms, plus I'd rather not repeat this declaration everywhere.
I figured that a directive that looks something like this could work:
<div class="control-group" error-on="model1, model2">
<input ng-model="model1">
<input ng-model="model2">
</div>
So when either model1 or model2 is not valid, .control-group gets .error added.
My attempt here. Is it possible to access the models from the directive, given the model names?
If there's a better approach, I'd love to hear it too.
I don't think that writing a custom directive is necessery for this use-case as the ng-form directive was created exactly for situations like those. From the directive's documentation:
It is useful to nest forms, for example if the validity of a sub-group
of controls needs to be determined.
Taking your code as an example one would write:
<div class="control-group" ng-class="{ error: myControlGroup1.$invalid }>
<ng-form name="myControlGroup1">
<input ng-model="model1">
<input ng-model="model2">
</ng-form>
</div>
By using this technique you don't need to repeat expressions used in ng-model and can reuse this fragment inside any form.
You can also change the markup in the accepted answer to do without the nesting, since ng-form is also a class directive:
<div class="control-group ng-form" name="controlGroup11" ng-class="{ error: controlGroup1.$invalid }>
<input ng-model="model1">
<input ng-model="model2">
</div>
Final solution Fiddle
Inside your link function, you can get access to the formController. It has all of the controls. So the following will give your directive access to .$valid:
el.controller('form')[attrs.errorOn].$valid
However, I don't know how to watch that for changes. I tried watching attrs.errorOn (i.e., watch the ng-model property), but the watch doesn't trigger unless a valid value is input (because of the way Angular forms work... unless that value is valid, it is not assigned to the scope property set by ng-model.)
Fiddle.
Maybe someone can take this further...