I am trying to create a form with inputs that only update onBlur ([ngFormOptions]="{updateOn: 'blur'}"
). But a side effect of this is that the form is no longer submitted once the user hits 'enter', since the model is only updated onBlur. (same as this question, but it has no answer)
As a result of this the form is marked invalid, since there is a value in the input field, but the model is not yet updated with the value.
Versions:
- Angular 6.0.7
- @angular/forms 6.0.7
HTML example
<form (ngSubmit)="login()" #loginForm="ngForm" [ngFormOptions]="{updateOn: 'blur'}" [validate-on-submit]="loginForm">
<input type="text" ([NgModel])="user.username"/>
<input type="password" ([NgModel])="user.password"/>
</form>
When entered a valid text in both fields and hitting 'enter', the form validates and marks password (the input that had the focus) as invalid, since the NgModel has not yet been updated, and thus the value is invalid.
What do I want
So what I am looking for is a way to validate on both onBlur
aswell as onSubmit
. I know that since Angular 5 we have the option [ngFormOptions]="{updateOn: 'blur'}"
, but is there a way to give 2 parameters to this object?
([ngFormOptions]="{updateOn: ['blur', 'submit']}"
doesn't seem to work)
Almost forgot, I do not want the model updated onChange
(creates annoying messages that are not very helpfull, since the user is still typing..)
Create a third, invisible input in your form, and give it a template variable. Before submitting, simply focus this input, which will trigger the onBlur
update :
<form (ngSubmit)="focuser.focus(); login()" #loginForm="ngForm" [ngFormOptions]="{updateOn: 'blur'}">
<input type="text" ([NgModel])="user.username"/>
<input type="password" ([NgModel])="user.password"/>
<input type="hidden" #focuser>
</form>
Note that this is one of the many workarounds you can use, not an actual solution
The solution - A form component
After following the advice of @trichetriche (creating a separate component that holds the logic of the form) and letting template driven forms go in favor of reactive forms I found the following solution:
@Component({
selector: "val-message-form",
templateUrl: "./message-form.html"
})
export class MessageFormComponent {
/**
* The form group that holds all the input fields and the logic of the input fields
* @type {FormGroup}
*/
@Input()
public formGroup: FormGroup;
/**
* The function that will be called when the form is valid
* The template that includes this component can listen to this event for the submit action
* @type {EventEmitter}
*/
@Output()
private onSubmit = new EventEmitter();
/**
* Notify all listeners that the form has been submitted
* NOTE: Notify will only work if the form is valid
*/
public submitForm(): void {
if (this.formGroup.valid) {
this.onSubmit.emit();
} else {
this.updateControls(this.formGroup);
}
}
/**
* Recursively update all form controls, since we are going to display error messages
*/
private updateControls(formGroup: FormGroup): void {
Object.keys(formGroup.controls).forEach(field => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsTouched({onlySelf: true}); // Otherwise validation color is not shown
control.updateValueAndValidity({onlySelf: true})
} else if (control instanceof FormGroup) {
this.updateControls(control);
}
});
}
}
Do note the formgroup that is injected, we can set it on the new form in this component so that the form controls are correctly transcluded (otherwise Angular does not recognize the form controls). This is the problem that stopped me from using template driven forms
When the form is submitted we now check if it is valid. If it isn't then we recursively loop trough all the FormControls (updateControls()
) and update the status to 'touched' (since we are going to set errors on them we can also mark them 'touched'). In order to make Angular send out an stateChange
event to it's listeners we need to use updateValueAndValidity()
. And since we are already looping trough every formControl we can set the onlySelf
flag to true
.
The template that belongs to the form:
<form (ngSubmit)="submitForm()"
[formGroup]="form"
class="no-validate">
<ng-content></ng-content>
</form>
By making sure Angular emits and event when the form is invalid we've created a hook that can be used for custom messages (remember ng-messages
from Angular 1.x?). More information on that can be found [here][2].
If you want to use this in a template this would look as follows:
<val-message-form (onSubmit)="login()" [formGroup]="form">
<fieldset>
<!-- Username -->
<div class="form-group">
<input type="email"
formControlName="username">
</div>
<!-- Password -->
<div class="form-group">
<input type="password"
formControlName="password">
</div>
<button type="submit" class="btn btn-success shadow-none">
{{'LOGIN_PAGE.LOGIN.SHORT' | translate}}
</button>
</fieldset>
</val-message-form>
Note the [formGroup]
tag, Angular uses the same tag to define a reactive form on a form. Removing the tag from the input results in an angular error telling you that it needs the tag in order to assign the formControlName
. We can make sure a programmer doesn't run into this problem by using the same tag for injecting the form into the directive. Angular will be happy and you can get the form inside the directive, win-win situation!