Testing whether certain elements are visible or not - angularjs

How do I find out if an element is visible or hidden in testacular (jasmine)?
My DOM looks like:
<div class="span5 value-entry">
<input type="text" ng-model="query.value" placeholder="Enter value" class="input-large" ng-show="genericInput(criteria.attribute)">
<select ng-model="query.value" ng-options="entry for entry in filteredValue(criteria.attribute)" class="input-medium" ng-show="!genericInput(criteria.attribute)">
<option value="">-- Select Value --</option>.
</select>
</div>
Either the select is shown or the input box, but not both. I wish to check which element is visible (based on some other criteria), but I can't seem to figure out how to get the code working. I have written the following code:
expect(element('.value-entry input').is(':visible')).toBe(true);
But I get an error:
TypeError: Object #<Object> has no method 'is'
How do I check if the input is visible and the select is hidden at the same time (and vice versa)?
EDIT : I wish to add here that this is an end to end test

This behavior has changed in Angular 1.2 because of ng-animate.
The code for ngShow is:
var ngShowDirective = ['$animate', function($animate) {
return function(scope, element, attr) {
scope.$watch(attr.ngShow, function ngShowWatchAction(value){
$animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
});
};
}];
Which means that it will add/remove class ng-hide to hide/show the element.
Thus, as an example, the right way to test if the element is hidden would be:
expect(element.find('.value-entry input').hasClass('ng-hide')).toBe(true);

You were close. However, here is how you should test visibility:
expect(element('#some-id:visible').count()).toBe(1);

Visibility Test
By default, display is set to inline for input, and inline-block for select. Therefore, you can determine if either are currently shown by testing for the existence of the default CSS property.
expect(element('.value-entry input').css('display')).toBe('inline');
expect(element('.value-entry select').css('display')).toBe('inline-block');
To check if either are hidden, replace inline and inline-block with a check for none, which is how ngShow hides an element.
expect(element('.value-entry input').css('display')).toBe('none');
expect(element('.value-entry select').css('display')).toBe('none');

Related

Carry over attributes from directive to sub-element

I've got a directive that renders a simple searchbox - its HTML looks as follows:
<div class="search input-group">
<input type="text"
ng-model="text"
ng-change="onChange()"
placeholder="Search here..."
class="form-control">
<span class="input-group-btn">
<button class="btn btn-default glyphicon glyphicon-search"></button>
</span>
</div>
All is well and working, I'm able to use it like this:
<searchbox ng-model="search" />
However, now I would like the searchbox to have autofocus in some cases, and in some cases not, for that, it would be neat to just be able to do:
<searchbox ng-model="search" autofocus />
and have that result in having the autofocus attribute carried over to the <input> tag within the directive. Is this possible? How would I go about doing that? Is there a way to carry over specific attributes over to a specific sub-element?
This is a way: from your directive's link function, read the autofocus attribute and, if it is defined, write it to the <input> using DOM manipulation. (DOM manipulation is OK inside the link function):
link: function(scope,elem,attrs) {
if( angular.isDefined(attrs.autofocus) ) {
var inp = elem[0].querySelectorAll('input');
inp[0].setAttribute('autofocus','autofocus');
}
}
A fiddle demonstrating the principle: http://jsfiddle.net/5yhp2xa0/
Possible catch: I am not sure if HTML's autofocus would work for templates that are inserted to the page "later" (i.e. after Angular route change, when a ng-if is shown etc). If this is the case, then a different solution should be used (could be easy, just call inp[0].focus() instead of inp[0].setAttribute('autofocus','autofocus');).
Since the title of the question is "Carry over attributes from directive to sub-element", let me address the general issue as well:
Attributes are not transferred automatically
If the attribute is non-directive, then techniques similar to the answer above can be used, i.e. manipulate the DOM from the link function. Things can get more complex if the attribute value is dynamic, but the general idea is the same.
If the attribute is a directive things are more difficult. Most probably you will have to use the compile function and manipulate the template of the DOM. In this case however, I would prefer to make the directives cooperate directly using the require configuration, especially with the optional modifier, e.g. require: '?otherAttributeDirective'. Of course this is possible only if you control both directives.
You can use the tab index. I m listing some of the behaviors of tab index as under
The tabindex value can allow for some interesting behaviors .
If given a value of "-1", the element can't be tabbed to but focus can be given to the element programmatically (using element.focus()).
If given a value of 0, the element can be focused via the keyboard and falls into the tabbing flow of the document.
Values greater than 0 create a priority level with 1 being the most important.
Or you can use following javascript code for that.
document.getElementById('txtId').focus();
I would do that programmatically. It feels like you are asking too much of angular to carry the attributes in automatically. The attributes of the directive are available as arguments to the link and compile functions, it should be easy to use the directive template to apply the attribute inside when it's on the outside.
For example, try this:
... directive code
link: function(scope, elem, attrs) {
console.log(attrs.autofocus);
}
You can check the value of autofocus from the attrs like that

What is the best way to conditionally apply attributes in AngularJS?

I need to be able to add for example "contenteditable" to elements, based on a boolean variable on scope.
Example use:
<h1 attrs="{'contenteditable=\"true\"': editMode}">{{content.title}}</h1>
Would result in contenteditable=true being added to the element if $scope.editMode was set to true.
Is there some easy way to implement this ng-class like attribute behavior? I'm considering writing a directive and sharing if not.
Edit:
I can see that there seems to be some similarities between my proposed attrs directive and ng-bind-attrs, but it was removed in 1.0.0.rc3, why so?
I am using the following to conditionally set the class attr when ng-class can't be used (for example when styling SVG):
ng-attr-class="{{someBoolean && 'class-when-true' || 'class-when-false' }}"
The same approach should work for other attribute types.
(I think you need to be on latest unstable Angular to use ng-attr-, I'm currently on 1.1.4)
You can prefix attributes with ng-attr to eval an Angular expression. When the result of the expressions undefined this removes the value from the attribute.
<a ng-attr-href="{{value || undefined}}">Hello World</a>
Will produce (when value is false)
<a ng-attr-href="{{value || undefined}}" href>Hello World</a>
So don't use false because that will produce the word "false" as the value.
<a ng-attr-href="{{value || false}}" href="false">Hello World</a>
When using this trick in a directive. The attributes for the directive will be false if they are missing a value.
For example, the above would be false.
function post($scope, $el, $attr) {
var url = $attr['href'] || false;
alert(url === false);
}
I got this working by hard setting the attribute. And controlling the attribute applicability using the boolean value for the attribute.
Here is the code snippet:
<div contenteditable="{{ condition ? 'true' : 'false'}}"></div>
In the latest version of Angular (1.1.5), they have included a conditional directive called ngIf. It is different from ngShow and ngHide in that the elements aren't hidden, but not included in the DOM at all. They are very useful for components which are costly to create but aren't used:
<h1 ng-if="editMode" contenteditable=true>{{content.title}}</h1>
To get an attribute to show a specific value based on a boolean check, or be omitted entirely if the boolean check failed, I used the following:
ng-attr-example="{{params.type == 'test' ? 'itWasTest' : undefined }}"
Example usage:
<div ng-attr-class="{{params.type == 'test' ? 'itWasTest' : undefined }}">
Would output <div class="itWasTest"> or <div> based on the value of params.type
<h1 ng-attr-contenteditable="{{isTrue || undefined }}">{{content.title}}</h1>
will produce when isTrue=true :
<h1 contenteditable="true">{{content.title}}</h1>
and when isTrue=false :
<h1>{{content.title}}</h1>
Regarding the accepted solution, the one posted by Ashley Davis, the method described still prints the attribute in the DOM, regardless of the fact that the value it has been assigned is undefined.
For example, on an input field setup with both an ng-model and a value attribute:
<input type="text" name="myInput" data-ng-attr-value="{{myValue}}" data-ng-model="myModel" />
Regardless of what's behind myValue, the value attribute still gets printed in the DOM, thus, interpreted. Ng-model then, becomes overridden.
A bit unpleasant, but using ng-if does the trick:
<input type="text" name="myInput" data-ng-if="value" data-ng-attr-value="{{myValue}}" data-ng-model="myModel" />
<input type="text" name="myInput" data-ng-if="!value" data-ng-model="myModel" />
I would recommend using a more detailed check inside the ng-if directives :)
Also you can use an expression like this:
<h1 ng-attr-contenteditable="{{ editMode ? true : false }}"></h1>
I actually wrote a patch to do this a few months ago (after someone asked about it in #angularjs on freenode).
It probably won't be merged, but it's very similar to ngClass: https://github.com/angular/angular.js/pull/4269
Whether it gets merged or not, the existing ng-attr-* stuff is probably suitable for your needs (as others have mentioned), although it might be a bit clunkier than the more ngClass-style functionality that you're suggesting.
For input field validation you can do:
<input ng-model="discount" type="number" ng-attr-max="{{discountType == '%' ? 100 : undefined}}">
This will apply the attribute max to 100 only if discountType is defined as %
Edit: This answer is related to Angular2+! Sorry, I missed the tag!
Original answer:
As for the very simple case when you only want to apply (or set) an attribute if a certain Input value was set, it's as easy as
<my-element [conditionalAttr]="optionalValue || false">
It's the same as:
<my-element [conditionalAttr]="optionalValue ? optionalValue : false">
(So optionalValue is applied if given otherwise the expression is false and attribute is not applied.)
Example: I had the case, where I let apply a color but also arbitrary styles, where the color attribute didn't work as it was already set (even if the #Input() color wasn't given):
#Component({
selector: "rb-icon",
styleUrls: ["icon.component.scss"],
template: "<span class="ic-{{icon}}" [style.color]="color==color" [ngStyle]="styleObj" ></span>",
})
export class IconComponent {
#Input() icon: string;
#Input() color: string;
#Input() styles: string;
private styleObj: object;
...
}
So, "style.color" was only set, when the color attribute was there, otherwise the color attribute in the "styles" string could be used.
Of course, this could also be achieved with
[style.color]="color"
and
#Input color: (string | boolean) = false;
Was able to get this working:
ng-attr-aria-current="{{::item.isSelected==true ? 'page' : undefined}}"
The nice thing here is that if item.isSelected is false then the attribute simply isn't rendered.
Just in case you need solution for Angular 2 then its simple, use property binding like below, e.g. you want to make input read only conditionally, then add in square braces the attrbute followed by = sign and expression.
<input [readonly]="mode=='VIEW'">

AngularJS required radio buttons needs two click events to be valid

I have a very simple form where a radio button is required to be selected in order for a form to be valid. The radio buttons are generated by ngRepeat.
As you can see from this fiddle, while the desired behavior is that when the radio button is clicked for the first time, that should validate the form (being the only element), however notice that it takes an additional click (on the same radio button or any other) to validate the form:
http://jsfiddle.net/Xsk5X/3/
What am I missing?
All the other solutions are work-arounds: All you have to do is remove the name attribute, when you use the ng-model attribute you don't need it and they conflict.
Specifying the name causes Angular to get confused because it changes the value once for the angular model and another time for the form element name.
I had this problem because a colleague had copied the radio buttons in the same page and hidden them for temporary reference, so duplicate radio inputs with the same name
Try adding the ng-click attribute to your radio button input.
Credit to Manny D for noting this first. Yes, this is a little hackish, but it works. E.g.,
<input type="radio"
name="groupName"
ng-model="editObject.Property"
ng-value="someValue"
ng-click />
The reason why this is breaking - is because you're setting all radio boxes to be required. As a result, depending on how you write it - angularjs is saying it's invalid because not all have been selected at some point.
The way around this is to do something like the following:
Using checkboxes and required with AngularJS
(check the 1st and 2nd answers). This will resolve your problem.
Seems like an AngularJS 1.0.3 $scope.$apply update problem.
Tested your exact Fiddle in 1.0.2 (check it out yourself) and it works the way you expect it too.
It doesn't seem like there's anything wrong with your code, just that $scope.$apply(or $digest) isn't working as expected on the first select.
A very simple fix is to force the $scope to also update on every select, try changing the following line of code in your form:
<p>Favorite Beatle</p>
change it too:
<p>Favorite Beatle: {{name}}</p>
And you will see how myForm.$invalid is updated even after the first click.
I would try it out with the latest AngularJs version and let us know if that happens there too.
Another solution I can think of it setting the default selected radio, which will cause myForm.$invalid to be false from the beginning. you can do this by adding the following line in your controller code:
$scope.name = "John";
or any default name you want.
Some times the $digest cycle dosen't $apply(fn) because you have two o more instances.
For fix this you need $apply this trick manually, so put this in your directives:
angular.('myApp',[])
.directive('ngRadioExtend', ['$rootScope', function($rootScope){
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, iElm, iAttrs, controller) {
iElm.bind('click', function(){
$rootScope.$$phase || $rootScope.$apply()
});
}
};
}])
and use it as:
<input type="radio" name="input_name" ng-model="some" value="F" ng-required="true" ng-radio-extend>
<input type="radio" name="input_name" ng-model="some" value="M" ng-required="true" ng-radio-extend>
DONE it's the correct way!
The problem of the scope not getting updated still occurs in 1.1.5
A simple work around is to just add
<span ng-show="false"> {{name}} </span>
Fiddle: http://jsfiddle.net/jonyschak/xaQJH/
For IONIC v1,
add name="" to prevent ionic auto-generate attribute name.
Then, I can change the selected item with only one click.
<ion-radio class="label-ticket"
ng-repeat="topic in vm.listTopic track by $index"
ng-value="topic"
ng-model="vm.topicSupport"
name="">
{{ topic.title }}
</ion-radio>

How to get the NgModelController for input fields inside a ngRepeat directive?

Normally, with a form and input fields, the form controller is published into the related scope under the form name attribute. And, the NgModelController is published under the input name attribute.
So for an input field with an ngModel directive, the NgModelController for the input field can be retrieved like $scope.myFormName.myInputFieldName
The question is how to do the same thing (get the NgModelController) for input fields inside the ngRepeat directive?
I would like to name the input fields using $index as part of the name so each template instance is uniquely named. This renders OK, so
<input name="foo_{{$index}}" ...
renders the instance with $index == 3 to
<input name="foo_3" ...
But trying to get the ngModelController via the published names does not work (it's undefined), e.g.:
$scope.myFormName.foo_3
A plunker showing this is here: http://plnkr.co/edit/jYDhZfgC3Ud0fXUuP7To?p=preview
It shows successfully getting the ngModelController for a 'plain' input element and calling $setValidity, and also shows failing to get the ngModelController for an input element inside an ngRepeat directive.
Copied the relevant section of code from the plunker below:
<div ng-repeat="element in elements">
<div ng-class="{error: form['foo_{{$index}}'].$invalid}">
<input name="foo_{{$index}}" ng-model="element.a" type="number">
<span ng-show="form['foo_{{$index}}'].$error.bar">ngRepeat bar invalid</span>
</div>
</div>
{{form.foo_0.$setValidity('bar', false)}}
#Flek is correct that the new child scopes that ng-repeat creates are the root of the problem here. Since browsers do not allow nesting of <form> elements, ngForm must be used when nesting forms, or when you want to do form validation inside ngRepeat.
See Pawel's answer on the google group thread, which shows how to use ng-form to create inner forms, and/or #blesh's SO answer.
If i understand your question correctly, you are trying to have access to the form elements created inside the ng-repeat.
Please have a look at this fiddle http://jsfiddle.net/EF5Jp/. Inside the button click handler you will have the access to the element with id myForm.foo_2. You can notice that the element is retrieved by myForm.foo_2 and not $scope.myForm.foo_2. Second thing is, changing the value using its scope and not using its value property like angular.element(element).scope().foo = 6;.

Using {{$index}} in compiled directive within ng-repeat (jQuery UI buttonset)

In an ng-repeat list, I'm having a terrible time putting an ON/OFF button (using JQ UI wrapping radio buttons) for each item in the list.
When using radio buttons, it seems JQ UI buttonset needs both the "input" and "label" tags plus also the 'for' of the label must match the 'id' of the input.
I can use {{$index}} to make them unique, like this:
<label for='algoOn{{$index}}'>ON</label>
<input type='radio' [... blah blah ..] id='algoOn{{$index}}'>
The problem is calling $().buttonset() once the DOM is ready. I've tried various things (dom.ready, link function etc), but had to resort to calling it after a delay [ $('.buttonme').buttonset() ] to trigger all buttons on the page. Hacky.
However, I'd like to wrap the on/off button in a directive. Still have the same problems with needing unique IDs. (If you don't have unique IDs the buttons get bigger and bigger on each successful call in the directive's link function)
BUT... using {{$index}} in the template gives me a mysterious syntax error:
Syntax error, unrecognized expression: [for=on{{$index}}] <onoffbtn prop="win.runstate" class="ng-isolate-scope ng-scope">
(even though code doesn't have 'for=on{{$index}}' in it!)
The directive is the preferred approach but can't figure out how to get around this one.
Secondly, in the directive, all radio buttons are in sync after the first click, but when the page first loads the buttons in the directive are both blank. It doesn't set itself to the model right away. I thought to do that in the link function (eg. element -> find the input -> set the value) but angular has re-written all of the 'names' and 'ids'.
Plunker showing both issues is here: http://plnkr.co/edit/DTy8dGsRDVVDnWZBYlqQ
Thanks!
Like you said this is doable from a directive. Using your html, I just added buttonset to the wrapping div:
<div id='A{{$index}}' buttonset>
<label for='algoOn{{$index}}'>ON</label>
<input class="buttonme" type='radio' name='onoff{{$index}}' ng-model='win.runstate' ng-name='onoff' value='running' id='algoOn{{$index}}'>
<label for='algoOff{{$index}}'>OFF</label>
<input class="buttonme" type='radio' name='onoff{{$index}}' ng-model='win.runstate' ng-name='onoff' value='stopped' id='algoOff{{$index}}'>
</div>
Here is how the buttonset directive looks
angular.module('button', [])
.directive('buttonset', function() {
return function(scope, elm, attrs) {
$(function(){
$(elm).buttonset();
});
};
});
Here is the plunker, no more hacks :)
Update:
The errors you are getting have to do with the fact that the dwbuttonset directive is executing before the code is compiled by angular. Therefore, what you need to do is to wait until this has been done. You can use $timeout with a 0 value (see this question) in order to queue your method until everything has been loaded.
Example:
.directive('dwbuttonset', function($timeout){
return function(scope, elm, attrs) {
$timeout(function(){
$(elm).buttonset();
});
}})

Resources