Invalidate form by custom component | AngularJS - forms

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.

Related

Angular 2++ | NgForm: Form.Dirty is Always Dirty

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.

grails: custom validation of just one field/property

I have a "Thing" domain class, where each Thing has an record number (which is not the automatically generated id), that the user will use to access a Thing:
class Thing {
...
String recordNumber
...
}
There is a form to look for a Thing, knowing its recordNumber:
<g:form action="search">
<input name="recordNumber">
<g:submitButton name="btn" value="go to this Thing"/>
</g:form>
I would like to use a validation process in this form: if the recordNumber is not found (Thing.findByRecordNumber(recordNumber) == null), then the input field must turn in red, and a tooltip must show the error message "record number not found".
As far as I know/read (I'm a grails rookie), this has to be written as a constraint in the Thing class:
static constraints = {
recordNumber validator: { n -> Thing.findByRecordNumber(recordNumber) }
}
The problem is: I do not have in this form all the "Thing" properties to populate, just the recordNumber one, so I just can't call
new Thing(params).validate()
How to call validation on just one field, not on the whole object ?
If this is your main question, although I see others there:
"How to call validation on just one field, not on the whole object ?"
You can pass a list of values to validate and it will only validate those properties
new Thing(params).validate(["recordNumber"])
http://grails.org/doc/latest/ref/Domain%20Classes/validate.html
Validation is for constraints for domain class properties. You need an action in your controller:
def search = {
if(params.recordNumber && Thing.findByRecordNumber(params.recordNumber)){
redirect(action: "show", params:[id:Thing.findByRecordNumber(params.recordNumber).id])
}else{
flush.message = "No record found"
render(view:'VIEW_WITH_SEARCH_FORM')
}
}
If you want to validate without refreshing page, write a javascript code.

How to use ng-class in select with ng-options

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');
});

Zend_Form - How to addValidator after the form has been submitted

I have 2 text fields in my form.
TextFieldA - not required
TextFieldB - not required
After user submitted the form,
How to add validator / setRequired(true) to TextFieldB if the value of TextFielA is not empty?
I see two approaches in addition to #Marcin's idea.
Conditionally call setRequired() on the relevant elements by creating a preValidate() method on the form and calling it in your controller. [Really the same idea as #Marcin, but pushed down into the form itself, keeping the controller a bit leaner.]
Create a custom validator called something like ConditionallyRequired that accepts as an option the fieldname of the "other field". Then attach this validator to each element, configuring it with the name of the "other" element. Then in the validator's isValid($value, $context) method, conditionally test $value if $context['otherfield'] is non-empty.
You could do as follows:
if ($this->getRequest()->isPost()) {
$textFieldA = $yourForm->getElement('TextFieldA');
$textFieldB = $yourForm->getElement('TextFieldB');
if (!empty($_POST['TextFieldA'])) {
$textFieldB->setRequired(true);
}
if (!empty($_POST['TextFieldB'])) {
$textFieldA->setRequired(true);
}
if ($mainForm->isValid($_POST)) {
// process the form
}
}
Basically, you add the validators after the post, but before the form is validated.
Hope this helps.

Zend Avoid submit button value in GET parameters in url

I have a form, created with Zend_Form, with method = GET used for searching records with elements as below:
[form]
user name [input type="text" name="uname"]
[input type="submit" value="Search" name="search"]
[/form]
After form is submitted all the GET parameters along with submit button value are appearing in the url.
http://mysite.com/users/search?uname=abc&search=Search
How to avoid submit button value appearing in the url? is custom routing the solution ?
When you create your element, you can simply remove the name attribute that was automatically set at creation
$submit = new Zend_Form_Element_Submit('search')->setAttrib('name', '');
Or inside a Zend_Form
// Input element
$submit = $this->createElement('submit', 'search')->setAttrib('name', '');
// Or Button element
$submit = $this->createElement('button', 'search')->setAttribs(array
(
'name' => '', 'type' => 'submit',
);
When a form gets submitted, all of its elements with their names and values become a part of a GET / POST - query.
So, if you don't want an element to appear in your GET - query, all you need to do is to create this element without a name. That's probably not the best approach, but since we're talking about the 'submit' element, I guess it doesn't matter that much.
Looking at Zend_View_Helper_FormSubmit helper, you can see that it's creating the 'submit' element and setting its name. So, the possible solution would be to create your own view helper and use it for rendering the 'submit' element instead of the default helper.
You can set a custom helper with
$element->setAttribs( array('helper' => 'My_Helper_FormSubmit') );
Then build your own form element class and remove the name attribute from the element with preg_replace. The beauty of it is, it will not interfere with the other decorators.
So the something like this:
class My_Button extends Zend_Form_Element_Submit
{
public function render()
{
return preg_replace('/(<input.*?)( name="[^"]*")([^>]*>)/', "$1$3", parent::render(), 1);
}
}
You can remove name attribute for submit button in javascript.
jQuery example:
$('input[name="submit"]').removeAttr('name');
In the controller that represents the form's action, redirect to another (or the same controller) only including the relevant params.
Pseudocode:
$params = $this->getRequest()->getParams();
if isset($params['search'])
unset($params['search']);
return $this->_helper->Redirector->setGotoSimple('thisAction', null, null, $params);
handle form here
This is basically the same idea as Post/Redirect/Get except that you want to modify the request (by unsetting a parameter) in between the different stages, instead of doing something persistent (the images on that Wiki-page shows inserting data into a database).
If I were you, I would leave it in. IMO it's not worth an extra request to the webserver.