I have an array of Person objects
var persons = [
{Name:'John',Eligible:true},
{Name:'Mark',Eligible:true},
{Name:'Sam',Eligible:false},
{Name:'Edward',Eligible:false},
{Name:'Michael',Eligible:true}
];
and i am using select with ng-options like this:
<select ng-model="Blah" ng-options="person.Name for person in persons"></select>
I want to show the record with Eligible:false in red color.
So the problem is how do i use the ng-class in select inorder to achieve this? Since we are not using any option tag it wont work if i simply add ng-class in the select element itself.
You could create a directive that processed the options after the ngOptions directive is processed that updated them with the appropriate classes.
Update: The old code had a few bugs, and I've learned a bit since I answered this question. Here is a Plunk that was redone in 1.2.2 (but should work in 1.0.X as well)
Here is updated (Nov 30 '13 at 3:17) the Code:
app.directive('optionsClass', function ($parse) {
return {
require: 'select',
link: function(scope, elem, attrs, ngSelect) {
// get the source for the items array that populates the select.
var optionsSourceStr = attrs.ngOptions.split(' ').pop(),
// use $parse to get a function from the options-class attribute
// that you can use to evaluate later.
getOptionsClass = $parse(attrs.optionsClass);
scope.$watch(optionsSourceStr, function(items) {
// when the options source changes loop through its items.
angular.forEach(items, function(item, index) {
// evaluate against the item to get a mapping object for
// for your classes.
var classes = getOptionsClass(item),
// also get the option you're going to need. This can be found
// by looking for the option with the appropriate index in the
// value attribute.
option = elem.find('option[value=' + index + ']');
// now loop through the key/value pairs in the mapping object
// and apply the classes that evaluated to be truthy.
angular.forEach(classes, function(add, className) {
if(add) {
angular.element(option).addClass(className);
}
});
});
});
}
};
});
Here's how you'd use it in your markup:
<select ng-model="foo" ng-options="x.name for x in items"
options-class="{ 'is-eligible' : eligible, 'not-eligible': !eligible }">
</select>
It works like ng-class does, with the exception that it's on a per-item-in-the-collection basis.
In this scenario you can only apply ng-class only if you use ng-repeat with option tags:
<select ng-model="Blah">
<option ng-repeat="person in persons" ng-class="{red: person.Eligible}">
{{person.Name}}
</option>
</select>
This will give custom class to your 'Eligible' persons, but CSS won't work consistently across bowsers.
Plunker.
I wanted to comment on the accepted answer, but because I don't have enough reputation points, I must add an answer.
I know that this is an old question, but comments where recently added to the accepted answer.
For angularjs 1.4.x the proposed directive must be adapted to get it working again.
Because of the breaking change in ngOptions, the value of the option isn't anymore the index, so the line
option = elem.find('option[value=' + index + ']');
won't work anymore.
If you change the code in the plunker to
<select ng-model="foo" ng-options="x.id as x.name for x in items"
options-class="{ 'is-eligible' : eligible, 'not-eligible': !eligible }">
</select>
As result the value of the option tag will now be
value="number:x" (x is the id of the item object)
Change the directive to
option = elem.find('option[value=\'number:' + item.id + '\']');
to get it working again.
Of course this isn't a generic solution, because what if you have not an id in your object?
Then you will find value="object:y" in your option tag where y is a number generated by angularjs, but with this y you can't map to your items.
Hopes this helps some people to get their code again working after the update of angularjs to 1.4.x
I tried also to use the track by in ng-options, but didn't get it to work.
Maybe people with more experience in angularjs then me (= my first project in angularjs)?
The directive is one way, but I used a custom filter.
If you know how to select your element, you should be fine here. The challenge was to find the current option element inside the select. I could have used the "contains" selector but the text in the options may not be unique for items. To find the option by value, I injected the scope and the item itself.
<select ng-model="foo" ng-options="item.name|addClass:{eligible:item.eligible,className:'eligible',scope:this,item:item} for item in items"></select>
and in the js:
var app = angular.module('test', []);
app.filter('addClass', function() {
return function(text, opt) {
var i;
$.each(opt.scope.items,function(index,item) {
if (item.id === opt.item.id) {
i = index;
return false;
}
});
var elem = angular.element("select > option[value='" + i + "']");
var classTail = opt.className;
if (opt.eligible) {
elem.addClass('is-' + classTail);
elem.removeClass('not-' + classTail);
} else {
elem.addClass('not-' + classTail);
elem.removeClass('is-' + classTail);
}
return text;
}
})
app.controller('MainCtrl', function($scope) {
$scope.items = [
{ name: 'foo',id: 'x1',eligible: true},
{ name: 'bar',id: 'x2',eligible: false},
{ name: 'test',id: 'x3',eligible: true}
];
});
Here you can see it work.
The accepted answer did not work for me, so I found an alternative without a custom directive using track by :
<select ng-model="foo" ng-options="x.name for x in items track by x.eligible"></select>
Each option now gets the value x.eligible. In CSS you can style options with value = true (I think true has to be a string). CSS:
option[value="true"]{
color: red;
}
In case you not only want to show them in red color but prevent the user from selecting the options, you can use disable when:
<select
ng-model="Blah"
ng-options="person.Name disable when !person.Eligible for person in persons">
</select>
You can then use CSS to set the color of disabled options.
I can't write this as a comment, due to reputation, but I have updated the plunker for the accepted answer to work with Angular 1.4.8. Thanks to Ben Lesh for the original answer, it helped me a lot. The difference seems to be that newer Angular generates options like this:
<option class="is-eligible" label="foo" value="object:1">foo</option>
so the code
option = elem.find('option[value=' + index + ']');
wouldn't be able to find the option. My change parses ngOptions and determines what field of item was used for the label, and finds the option based on that instead of value. See:
http://plnkr.co/edit/MMZfuNZyouaNGulfJn41
I know I am a bit late to the party, but for people who want to solve this with pure CSS, without using a directive you can make a css class like this:
select.blueSelect option[value="false"]{
color:#01aac7;
}
This css rule says : Find all elements with value = false with tag name 'option' inside every 'select' that has a class "blueSelect" and make the text color #01aac7; (a shade of blue)
In your case your HTML will look like this:
<select class="form-control blueSelect" name="persons" id="persons1"
ng-options="person as person.name for person in $ctrl.persons track by person.Eligible"
ng-model="$ctrl.selectedPerson" required>
<option disabled selected value="">Default value</option>
</select>
The track by inside the ng-options is what will hold what to track the options by, or the "value" field of each option. Notice that depending on your project needs , you might have to do some tweaking to make this work as per your requirements.
But that's not going to work right when there's multiple options with the same value for the Eligible field. So to make this work, we create a compound expression to track by, that way we can have unique values to track by in each option. In this case we combine both fields Name and Eligible
So now our html will look like this
<select class="form-control blueSelect" name="persons" id="persons2"
ng-options="person as person.name for person in $ctrl.persons track by (person.name + person.Eligible)"
ng-model="$ctrl.selectedPerson" required>
<option disabled selected value="">Default value</option>
</select>
and our css :
select.blueSelect option[value*="False"]{
color:#01aac7;
}
Notice the * next to value, this is a regular expression which means to find the word "False" somewhere in the value field of the option element.
Quick Edit
You can also choose to disable the options with Eligible = False using the "disable when" in the ng-options expression , for example:
label disable when disable for value in array track by trackexpr
I'll leave how to use that in your case for you to find out ;-)
This works for simple css modifications, for more complex stuff you might need a directive or other methods. Tested in chrome.
I hope this helps someone out there. :-)
I've found another workaround that was easier than adding a directive or filter, which is to add a handler for the onfocus event that applies the style.
angular.element('select.styled').focus( function() {
angular.element(this).find('option').addClass('myStyle');
});
Related
I have a parent record with multiple child records, all shown together on the ViewParentWithChildren and EditParentWithChildren screens. I want to write a cypress test that adds a new child record to an existing parent record. Each child record is in a <tr> of course.
The problem is, the <select> element has many <option disabled> invalid options in it. I need to select a valid, enabled one, and I don't know ahead of time what the names/values in that option are going to be. I don't care what they are, I just need to select any non-disabled option.
I try a standard-ish:
cy.contains('button', /Add Another Child Record/i).click();
cy.get('[name=child_id_name][value=""]') // newly added has nothing in the required field
.parents('tr')
.within(tr => {
cy.get('input[name=child_id_name]').type(randomAlpha());
cy.get('input[name=description]').type(randomAlpha());
cy.get('select[name=type]').select(?????); // TODO
});
Cypress only allows selecting an <option> via name, value, or index. Attempting to .select a valid <option> directly doesn't work, by design.
Here's a couple of other ways you may or may not find easier
Example page for POC
<select>
<option value="1" disabled>three</option>
<option value="2">two</option>
<option value="3" disabled>three</option>
<option value="4">four</option>
</select>
Method 1: Expose the <select> first
cy.get('select')
.should('have.value', '2') // by default the selected value is the first enabled
.then($select => {
cy.wrap($select)
.find('option:enabled:last')
.then($lastEnabledOption => {
cy.wrap($select).select($lastEnabledOption.val())
})
})
.should('have.value', '4') // check new value
Method 2: Set the selected attribute with jQuery
cy.get('select')
.should('have.value', '2') // initial value
.find('option:enabled:last')
.invoke('attr', 'selected', true)
cy.get('select')
.should('have.value', '4') // new value
The solution was kind of inside-out. Get all non-disabled options from that select first, then go within one of them, and "escape hatch" back out again to the select, feeding the .text() to the .select
cy.contains('button', /Add Another Child Record/i).click();
cy.get('[name=child_id_name][value=""]') // newly added has nothing in the required field
.parents('tr')
.within(tr => {
cy.get('input[name=child_id_name]').type(randomAlpha());
cy.get('input[name=description]').type(randomAlpha());
cy.get('select[name=type] option:not([disabled])') // get all its non-disabled options
.last() // the first option is usually blank for un-selecting, so, .last
.within(option => {
cy.root().closest('select').select(option.text());
});
});
Determine if NgForm Looks Exactly As It Did Before Any User-Input
It seems that form.dirty doesn't redact its value after it has been changed, and form.touched seems to always be false no matter what: dirty is touched, and touched is tetched.
template.html
<form #form="ngForm" (ngSubmit)="handleSubmission($event, {}, form)">
...
<input
#input
type="text"
[name]="item.title"
[(ngModel)]="item.estimate"
(ngModelChange)="handleEstimateChange(item, item.estimate, input, form)"
/>
...
</form>
component.ts
export class LeComponent {
#Input('data') public data: any;
public handleEstimateChange: Function;
constructor(private $: Sandbox) {
this.handleEstimateChange = $.debounce(this.handleEstimate.bind(this), (1000*0.2));
}
handleEstimate(item: any, estimate: number, input: HTMLInputElement, form: NgForm) {
if (!estimate) delete item.esitmate;
(this, item, estimate, input, form);
// Why does form.dirty never change back to pristine again???
}
}
In the TypeScript, I'm debouncing the ngModelChange handler to give Angular a chance to change the form.dirty value before I check it. This is because ngModelChange gets triggered before the NgForm object has been modified.
If !estimate, because estimate === "", then set it back to its original value of undefined. In this case, the form should look exactly like it did before any user-input had occurred.
However, when I put a breakpoint on the line right above the comment and I output form.dirty to the console, the NgForm never changes dirty back to false.
Is it possible to determine if the form looks exactly like it did before any user-input?
Obviously, I can write my own dirty logic, but wouldn't that mean that NgForm is kind of useless? There's got to be something I'm missing, right? How could dirty not mean dirty?
I've taken a look at some other SO questions -- the first one being similar but definitely not the question I am asking. They are asking if this is intentional -- I don't care; I'd like to know how to accomplish the goal above.
Close, but no cigar:
angular2 formcontrol stays dirty even if set to original value
Block routing if form is dirty [ Angular 2 ]
Angular 2 getting only the dirty values in a controlgroup
How do I programmatically set an Angular 2 form control to dirty?
Angular 2.x/4.x & bootstrap: patchValue does not alter dirty flag. Possible bug?
With template-driven forms and a very flat data model, I implemented it like this:
private currentProduct: IProduct;
private originalProduct: IProduct;
get isDirty(): boolean {
return JSON.stringify(this.originalProduct) !== JSON.stringify(this.currentProduct);
}
get product(): IProduct {
return this.currentProduct;
}
set product(value: IProduct) {
this.currentProduct = value;
// Clone the object to retain a copy
this.originalProduct = Object.assign({}, value);
}
But this only works for very simple cases.
As I mentioned in the comments, using reactive forms gives you more flexibility in managing your data model separate from your user entries.
What Was Most Useful
template.html
<form #form="ngForm" (ngSubmit)="handleSubmission($event, {}, form)">
...
<input
#input
type="text"
[name]="item.title"
[attr.name]="item.title"
[(ngModel)]="item.estimate"
(ngModelChange)="handleEstimateChange(item, item.estimate, input, form)"
/>
...
</form>
component.ts
export class LeComponent {
#Input('data') public section: any;
public handleEstimateChange: Function;
private resetFormControl = (input: HTMLInputElement, form: NgForm) => {
var name = input.name, control = form.controls[name];
control.reset();
// control.markAsPristine();
// control.setValue(undefined);
// control.updateValueAndValidity();
};
constructor(private $: Sandbox) {
this.handleEstimateChange = $.debounce(this.handleEstimate.bind(this), (1000*0.2));
}
handleEstimate(item: any, estimate: number, input: HTMLInputElement, form: NgForm) {
if (!estimate) this.resetFormControl(input, form);
(this, item, estimate, input, form);
// Why does form.dirty never change back to pristine again???
}
}
Note
[attr.name]="..." (template.html)
resetFormControl
Basically, simply deleteing the value was not enough because it was still present on the FormControl object (form.controls). To clear it properly, invoke control.reset() for the individual control -- this in-turn invokes .markAsPristine() which communicates to the parent NgForm. Also, input.name was empty as it was only represented by ng-reflect-name unless [attr.name] elucidated the same value -- [name] is really just there because its required by Angular.
Now, anytime an <input /> value changes -- and its falsey -- we reset the input ensuring that if all are falsey, Angular will automatically handle the NgForm's dirty-state correctly.
I want to add Id and Class attribute on specific input type in our contact form 7.
When I am adding with below example, it apply Id and Class on SPAN tag just above the input Type
[radio amount id:amount class:amount-select default:1 "50" "100" "200" "500" "Other"]
Here is the screenshot of source which is generated from above code:
Thanks in advance
There is no way to do so through the contact form 7 plugin. You'll need to add some custom javascript to do so, for example for your specific [radio] tag you would do something like this inside the cf7 edit page,
<label> My radio button
[radio amount id:amount class:amount-select default:1 "50" "100" "200" "500" "Other"]</label>
[submit]
<script>
(function( $ ) {
$(document).ready( function(){
$('form.wpcf7-form input').each(function(){
var span = $(this).parent('span');
if(span){
var idAttr = span.attr('id');
$(this).attr('id',idAttr);
span.attr('id','');
}
//or you could also do this which is even less maintenance
var name = $(this).attr('name');
var type = $(this).attr('type');
switch(type){
case 'radio':
case 'checkbox':
name += '-'+$(this).attr('value');
}
$(this).attr('id',name);
});
});
})( jQuery );
</script>
this would move all the span ids to the input elements when the page loads, you could use the same logic to move non-wpcf7 classes to the input element too.
[Edit] I added an additional method which needs less maintenance, although keep in mind that for radio/checkbox elements you'll need to append the value to the id to make it unique.
If you are using SASS style rules why not extend the Contact form 7 classes to your own classes? With this approach there is need to write JavaScript.
See my example below:
.wpcf7-form label {
#extend .control-group__label;
}
.wpcf7-form-control {
#extend .control-group__control;
}
.wpcf7-submit {
#extend .btn;
#extend .btn-submit;
}
Here is my fiddle: http://jsfiddle.net/mwrLc/12/
<div ng-controller="MyCtrl">
<select ng-model="searchCountries" ng-options="cc.country for cc in countriesList | orderBy:'country'">
<option value="">Country...</option>
</select>
<select ng-model="searchCities" ng-options="ci.city for ci in citiesList | filter:searchCountries | orderBy:'city'">
<option value="">City...</option>
</select>
<ul>
<li ng-repeat="item in list | filter:searchCountries | filter:searchCities">
<p>{{item.country}}</p>
<p>{{item.city}}</p>
<p>{{item.pop}}</p>
<br>
</li>
</ul>
The first select filters the second one but also the result list.
The second select filters only the result list.
It works great until a country and a city are chosen. Then, when you choose another country, second select got reseted but the scope seems to be stuck with the old value.
i cant find a way to have it works properly... Please help!
The best solution is to reset the city model when a change to country model is detected via $scope.$watch():
function MyCtrl($scope) {
// ...
$scope.$watch('searchCountry', function() {
$scope.searchCity = null;
});
}
Note that I changed your model names to singular form ("searchCountry" and "searchCity") which is more appropriate considering the value of those models is set to a single country or city.
Here is the full working example: http://jsfiddle.net/evictor/mwrLc/13/
Ezekiel Victors solution is excellent. However, I ran into a bit of a dependency injection issue while trying to get this to work when referencing the controller from an ng-include. Thanks to the help from Ben Tesser I was able to resolve the issue. Ive attached a jsfiddle that details the fix:
http://jsfiddle.net/uxtx/AXvaM/
the main difference is that you need to wrap the searchCountry/searchCity as an object and set the default value to null. Ditto for the watch.
$scope.test = {
searchCountry: null,
searchCity: null
};
/// And your watch statement ///
$scope.$watch('test.searchCountry', function () {
$scope.test.searchCity = null;
});
In your template you'll change all references from searchCountry/searchCity to test.searchCountry.
I hope this saves someone some time. It's trixy.
I've searched my way, but can't figure this out. I made a directive manyToOneSelect (custom component) that loads items from the server, shows them to the user and lets the user pick one. That works well, but I cannot figure out how to prevent the form from being submitted if no item is picked by the user, i.e. how to invalidate the form.
Below is pretty much the directive:
angular.module('myApp.directives').
directive('manyToOneSelect', function(entityService) {
return {
restrict:'E',
templateUrl:'partials/control/n21select.html',
scope:{
entityName:'#',
entityField:'#',
bindVariable:'='
},
compile:function (tElement, tAttrs, transclude) {
return function (scope, element, attrs) {
var inner = element.children("#n21select");
scope.entities = [];
scope.$watch('entityName', function ($new, $old) {
entityService.getList(scope.entityName, function (data) {
scope.entities = data;
}, []);
}, true);
scope.lookup = function(uuid) {
for(var i in scope.entities) {
if(scope.entities[i].uuid == uuid) {
return scope.entities[i];
}}}}}}});
Here is the corresponding partial partials/control/n21select.html:
<select ng-hide="disable" ng-options="entity.uuid as entity[entityField] for entity in entities" ng-model="bindVariable" required></select>
<span ng-show="disable">{{lookup(bindVariable)[entityField]}}</span>
Here is how I use the directive:
<form ng-href="#/" ng-submit="save()">
<many-to-one-select entity-name="customer" entity-field="name"
bind-variable="entity.customerUuid"></many-to-one-select>
...
My problem seems lack of strategy, rather than "not entirely getting it to work", hence you don't see any attempt in the code I posted above. Let this be then a fairly open question: how to do it? :) Much appreciated already!
There's a few ways to do this.
Considering how you've already built out the directive, one way is to add a scope attribute for the form itself. something like:
scope: {
form: '='
}
Then you'd pass your form element in like so:
<form name="myForm" ng-submit="whatever()">
<my-directive-name form="myForm"></my-directive-name>
</form>
And in circumstance in your directive you wish to invalidate your form, you'd just call $setValidity on it:
link: function(scope, elem, attr) {
if(somethingIsWrong) scope.form.$setValidity('reason', false);
}
That's ONE way to do it, here's a BETTER way to do it if you can re-engineer your directive:
The other way, which is probably preferred, is to have your directive require ngModel. Then you'll have more grainular control over your validation, as ngModel's controller will be passed in and you can use that to invalidate both your form, and a singular field on your form:
app.directive('bettererWay', function() {
return {
require: 'ngModel',
restrict: 'E',
link: function(scope, elem, attr, ngModel) {
if(somethingIsBad()) ngModel.$setValidity('somethingIsBad', false);
}
};
});
And that's how you do it, in a nutshell. Hopefully that gets you started in the right direction.
EDIT: Weird issue with submission regardless of validity (in comments)
This is apparently an issue caused by Angular trying to adhere to the HTML specs.
From the comments in their code approx. line 214 here:
* To prevent double execution of the handler, use only one of ngSubmit or ngClick directives. This
* is because of the following form submission rules coming from the html spec:
*
* - If a form has only one input field then hitting enter in this field triggers form submit
* (`ngSubmit`)
* - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter
* doesn't trigger submit
* - if a form has one or more input fields and one or more buttons or input[type=submit] then
* hitting enter in any of the input fields will trigger the click handler on the *first* button or
* input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
So, given the above, it might be a good idea to have your directive tied to an input element of type hidden on the page rather than being it's own element. If you have more than one element on the form, invalidity prevents submission just fine.