I'm curious if it's possible to use the myForm.controls property in an Angular Reactive form instead of defining an AbstractControl for every instance?
This method is suggested in NG-Book but it's not used with nested forms and FormGroupName.
Here is my code.
HTML:
<form [formGroup]="myForm"
(ngSubmit)="onSubmit(myForm.value)">
<div>
<input type="text"
id="nameInput"
placeholder="Product"
[formControl]="myForm.controls['name']">
<div *ngIf="myForm.controls['name'].hasError('required')">Name is
required</div>
</div>
<div formGroupName="address">
<input type="text"
id="streetInput"
placeholder="Enter Street"
[formControl]="myForm.controls['street']">
<div *ngIf="myForm.controls['street'].hasError('required')">Street is required</div>
</div>
<button type="submit" class="ui button">Submit</button>
JS:
export class App {
myForm: FormGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'name': ['', Validators.required],
'address': fb.group({
'street': ['', Validators.required],
});
}
}
}
I realize I can set formControlName="street" on the input but then I think I would have to use AbstractControl to determine it's validation state. Where as in my non-nested formGroup, I can just use myForm.controls['name'].hasError('required').
I don't know, i understand your question well.
Do you want to get validation about nested from control?
Then, you can get myForm.get('address').control['street'].hasError('required') or myForm.get('address').get('street').hasError('required')
And you can change hasError('required') to errors.required.
These link is description about form validation.
https://angular.io/api/forms/FormGroup#example-4
https://angular.io/guide/form-validation#reactive-form-validation
If I misunderstand your question, comment.
Related
I have a [formGroup] in Angular version8 app.
I am trying to validate the form based on these simple rules:
if a file is uploaded (includes just choosing any file with the button) then the Reason input is NOT required, else
if a file has not been chosen (ie 'choose file' has no value) then a Reason must be provided for form validation to pass.
File upload is optional if a Reason text is provided.
How do I check inside the Reason textArea that a file has been chosen? I simply can't seem to write a validation statement using that ngClass
template:
<form [formGroup]="dialogForm">
<div class="row">
<div class="lbl-col">
<label for="reason">Reason<span class="required-star">*</span></label>
</div>
<div class="col">
<textarea
id="reasonForChange"
formControlName="reasonForChange"
name="reasonForChange"
type="text"
[(ngModel)]="selectedReasonText"
[ngClass]="{invalid: dialogForm.get('File').invalid}"
>
</textarea>
</div>
</div>
<div class="row">
<div class="lbl-col"></div>
<div class="col">
<input
type="file"
id="reasonFile"
formControlName="File"
(change)="onFileChange($event)"
/>
</div>
</div>
</form>
[...]
// submit button
<button
class="primary-button"
label="update status"
(click)="update()" // a POST nothing more.
[disabled]="dialogForm.invalid"
></button>
and in my controller:
constructor(
private fb: FormBuilder
) {
this.dialogForm = this.fb.group({
chooseStatus: [null, Validators.required],
reasonForChange: [null, Validators.required],
File: [null],
});
Using [ngClass]="{invalid: dialogForm.get('File').invalid}" or variations of it, it doesn't appear to check formControlName="File" but instead checks itself, 'reasonforChange'.
I have also tried the crossValidation template from Ang docs.
Extend the Formbuilder:
constructor(
private fb: FormBuilder
) {
this.dialogForm = this.fb.group({
chooseStatus: [null],
reasonForChange: [null],
File: [null],
}, {validators: crossFormValidator}
);
and provide
export const crossFormValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const reasonForChange = control.get('reasonForChange');
const file= control.get('File');
return file && reasonForChange && attachment.value !== '' ? { crossValid: true } : null;
};
but that errors on this line in my template
<div *ngIf="dialogForm.errors?.['crossValid'] && (dialogForm.dirty)" class="form-validation-warning">
<span class="warning-label">* Required</span>
</div>
at Parser Error: Unexpected token [, expected identifier or keyword at column 20 in [dialogForm.errors?.['crossValid'] && (dialogForm.dirty)]
Even though that is taken almost verbatiim from the angular docs at https://angular.io/guide/form-validation#adding-cross-validation-to-template-driven-forms
Well, there is a simple approach for that, to validate different fields in angular.
Create a getter function in .ts file as follow:
get If() {
return this.dialogForm.controls;
}
This function is returning the controls of each field in the form.
Now, apply this function on the form fields as follow:
<form [formGroup]="dialogForm">
<div class="row">
<div class="lbl-col">
<label for="reason">Reason<span class="required-star">*</span></label>
</div>
<div class="col" *ngIf="If.File.valid || !If.File.untouched">
<textarea
id="reasonForChange"
formControlName="reasonForChange"
name="reasonForChange"
type="text"
[(ngModel)]="selectedReasonText"
[ngClass]="{invalid: dialogForm.get('File').invalid}"
>
</textarea>
</div>
</div>
<div class="row">
<div class="lbl-col"></div>
<div class="col">
<input
type="file"
id="reasonFile"
formControlName="File"
(change)="onFileChange($event)"
/>
</div>
</div>
// submit button
<button
class="primary-button"
label="update status"
(click)="update()" // a POST nothing more.
[disabled]="dialogForm.invalid"
></button>
</form>
This way, it is going to work as expected
first of all apologize for my English.
I have a problem when trying to add an array field to a formGroup.
I'm trying to add a formArray using the push method to my rate formGroup, and the error I have I think is due to the formControlName.
As I searched and read the problem it is there, but I can not solve it, can someone help me please?
I created this stackblitz so you can see the error it gives me.
https://stackblitz.com/edit/angular-mlk2mh
To work with FormArray you need to understand this:
Need form group
\/\/\/
<form [formGroup]="rates">
<input type="text" placeholder="credit_card" formControlName="credit_card" />
Need form array name
\/\/\/
<div formArrayName="servicesRates" *ngFor="let item of rates.get('servicesRates').controls; let i = index;">
<div [formGroupName]="i"> <-- this is important
<input type="text" placeholder="id" formControlName="id" />
<input type="text" placeholder="Servicio" formControlName="service" />
<input type="text" placeholder="Price" formControlName="price" />
</div>
</div>
</form>
To work above HTML your TS must be:
rates: FormGroup;
servicesRates: FormArray;
this.rates = this._formBuilder.group({
credit_card: [null, Validators.compose([Validators.required, Validators.minLength(8)])],
servicesRates: this._formBuilder.array([
this._formBuilder.group({
id: 0,
service: '',
price: 0,
})
])
});
and addNew function:
addField() {
this.servicesRates = this.rates.get('servicesRates') as FormArray;
this.servicesRates.push(this.servicesRates);
}
I have a component with a reactive form in it like so...
form component.html
<form [formGroup]="form" class="do-form">
<div formGroupName="dot">
<div class="do-form__container">
<div class="do-form__group">
<label for="day">Day</label>
<input id="day" type="number" placeholder="XX" class="do-form__group__control" formControlName="day" />
</div>
<div class="do-form__group">
<label for="month">Month</label>
<input id="month" type="number" placeholder="XX" class="do-form__group__control" formControlName="month" />
</div>
<div class="do-form__group">
<label for="year">Year</label>
<input id="year" type="number" placeholder="XXXX" class="do-form__group__control" formControlName="year" />
</div>
</div>
<div class="do-form__errors" *ngIf="isValid()">
<p>Please enter a valid date</p>
</div>
</div>
</form>
and in my form.component.ts
form = this.fb.group({
dot: this.fb.group({
day: ['',
[
Validators.required,
Validators.min(1),
Validators.max(31)
]],
month: ['',
[
Validators.required,
Validators.min(1),
Validators.max(12)
]],
year: ['2018',
[
Validators.required,
Validators.min(1918),
Validators.max(2018)
]]
})
});
isValid() {
return (
!this.form.valid &&
this.form.get('dot.day').touched &&
this.form.get('dot.month').touched &&
this.form.get('dot.year').touched
);
}
Now I have a separate page (app.component.html) like so
<app-form #formTool></app-form>
<button class="full-width-btn" (click)="next(); form.sendResult();">SEND</button>
app.component.ts
import { formComponent } from '../form-component/form-component.ts'
export ...
#ViewChild(formComponent) form;
Now basically I want to disable the send button until the form in the app form component is valid.
I'm not entirely sure how to do this. I thought about storing a valid event on a shared server but I'm not sure how I can store a valid event in a service. I saw that with non-reactive forms you can just have a button that uses the same ngModel but once again not sure if that would work in this case.
Any help would be appreciated!
EDIT
I have tried [disabled]="form.invalid" and [disabled]="!isValid()" but I am still able to click the button
I have also tried using [disabled]=!form.isValid() and [disabled]=!form.form.isValid()
Thanks
You are really close. Here is the only thing you need to add:
<app-form #formTool></app-form>
<button class="full-width-btn" [disabled]="!isValid()" (click)="next(); form.sendResult();">SEND</button>
In form component you could define an event #Output formValid = new EventEmitter().
Then you can listen to changes in input fields (on keypress or so) and on any change check the validity and if the form is valid, emit an event: formValid.emit().
In the app component define formIsValid = false, and on the app-form element you can listen to the formValid event:
< app-form (formValid)="formIsValid = true">
(or some function in app.component.ts instead of the inline code).
And finally on button
< button [disabled]="!formIsValid">
Please see the following examples. I have loaded jquery and jquery-steps into the project already and it is working. However after rendering the view, changing the data in the input boxes doesn't update the values in the form group mainForm. I believe the reason is that jquery-steps dynamically removed and reconstructed the html for the form, and so the form group doesn't link to the DOMs anymore.
Is there any way to re-bind FormGroup mainForm to the DOMs after jquery-steps reconstructed the html?
I read about ComponentResolver and ViewContainerRef, is it where it should use those? Could you give me an example how to use those in this situation?
Thank you!
<pre>{{ mainForm.value | json }}</pre>
<form [formGroup]="mainForm" id="mainForm" action="#">
<h1>Account</h1>
<div>
<label for="userName">User name *</label>
<input formControlName="userName" type="text" class="required">
<label for="password">Password *</label>
<input formControlName="password" type="text" class="required">
<label for="confirm">Confirm Password *</label>
<input formControlName="confirm" type="text" class="required">
<p>(*) Mandatory</p>
</div>
<h1>Finish</h1>
< div>
<input formControlName="acceptTerms" type="checkbox" class="required">
<label for="acceptTerms">I agree with the Terms and Conditions.</label>
</div>
</form>
import {Component, AfterContentInit} from "#angular/core";
import {FormBuilder, Validators, FormGroup} from "#angular/forms";
#Component({
templateUrl: 'main-view.template.html'
})
export class MainViewComponent implements AfterContentInit {
private mainForm: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.mainForm = this.formBuilder.group({
userName: ['', Validators.required],
password: ['', Validators.required],
confirm: ['', Validators.required],
acceptTerms: ['', Validators.required],
});
}
ngAfterContentInit() {
$("#mainForm").steps();
}
}
The main reason why that is not working is that jquery-steps plugin removes your html markup.
Using jquery in angular2 is bad idea but if you want to get it working i can offer you slightly modify the library
jquery.steps.js
function render(wizard, options, state) {
+ var contentWrapper = $('<{0} class=\"{1}\"></{0}>'.format(options.contentContainerTag, "content " + options.clearFixCssClass));
+ contentWrapper.append(wizard.children());
// Create a content wrapper and copy HTML from the intial wizard structure
var wrapperTemplate = "<{0} class=\"{1}\">{2}</{0}>",
orientation = getValidEnumValue(stepsOrientation, options.stepsOrientation),
verticalCssClass = (orientation === stepsOrientation.vertical) ? " vertical" : "",
- //contentWrapper = $(wrapperTemplate.format(options.contentContainerTag, "content " + options.clearFixCssClass, wizard.html())),
See also Plunker Example
I'm trying to validate that the password and ConfirmPassword fields are the same in my form, but when I add the validation provided by other posts on SO (aside from changing ControlGroup to FormGroup) I keep getting
ERROR: Cannot find control with unspecified name attribute
BUT, when I don't validate using the "matchingPassword" group below and just use the Validators.required syntax, it works just fine.
I don't know why it's throwing that error. Has anyone else worked through this? I'm currently building on Angular 4 Distro.
constructor(
private accountService: accountService,
fb: FormBuilder,
) {
this.changePWForm = fb.group({
'CurrentPassword' : [null, Validators.required],
'SecurityQuestions' : [null, Validators.required],
'SecurityQuestionAnswer' : [null, Validators.required],
'matchingPassword': fb.group({
'NewPassword' : [null, Validators.compose([Validators.pattern(this.strongPW), Validators.required])],
'ConfirmPassword' : [{disabled: true}, Validators.required],
}, {validator: this.equalPasswords})
})
}
equalPasswords(group: FormGroup){
//When I use the syntax above, it never makes it here.
var valid = false;
for (var name in group.controls) {
var val = group.controls[name].value
}
if (valid) {
return null;
}
return {
areEqual: true
};
}
Here's My HTML Template
<form [formGroup]="changePWForm" (ngSubmit)="updatePW(changePWForm.value)" *ngIf="securityQuestions">
<div class="form-group">
<label>Current Password:</label>
<input type="text" [(ngModel)]="changePWData.CurrentPassword" [formControl]="changePWForm.controls['CurrentPassword']">
</div>
<div class="form-group">
<label>New Password:</label>
<input type="text" [(ngModel)]="changePWData.NewPassword" [formControl]="changePWForm.controls['NewPassword']">
<small *ngIf="!changePWForm.controls.NewPassword.valid && !changePWForm.controls.NewPassword.pristine">You need a secure password.</small>
</div>
<div class="form-group" >
<label>Confirm New Password:</label>
<input type="text" [(ngModel)]="changePWData.ConfirmPassword" [formControl]="changePWForm.controls['ConfirmPassword']">
</div>
<div class="form-group">
<label>Security Question:</label>
<select #select type="text" [(ngModel)]="selectedSecurityQuestion" [formControl]="changePWForm.controls['SecurityQuestions']" class="select">
<option *ngFor="let question of securityQuestions" [ngValue]="question">{{question.SecurityQuestionText}}</option>
</select>
</div>
<div class="form-group">
<label>Security Question Answer: </label>
<input type="text" [(ngModel)]="securityQuestionAnswer" [formControl]="changePWForm.controls['SecurityQuestionAnswer']">
</div>
<div *ngIf="processing">
<div class="spinner">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
</div>
<button *ngIf="!processing" type="submit" [disabled]="!changePWForm.valid">Change Address</button>
</form>
The problem is that you've a nested FormGroup (matchingPassword).
So, you must wrap the controls of this nested group using a <div>, for example.
Wrap your password controls (NewPassword and ConfirmPassword) in a element, like this:
<form [formGroup]="changePWForm" ...>
...
<div formGroupName="matchingPassword">
<!-- you can also use [formGroup] if you want -->
<!-- Put the content of password controls here -->
</div>
...
</form>
Besides having the problem stated by developer033, that you are missing the formGroupName:
<div formGroupName="matchingPassword">
<!-- you can also use [formGroup] if you want -->
<!-- Put the content of password controls here -->
</div>
.. I also didn't really understand the custom validator and it didn't function correctly for me. Also noticed for some reason the validator don't even fire when marking form controls like:
[formControl]="changePWForm.controls['NewPassword']"
I can't really say why, but I also prefer the more "cleaner" version:
formControlName="NewPassword"
So changing those would make the custom validator fire.
Then to the custom validator, this is the way I do it. Also notice I've changed areEqual: true to notEqual:true to better describe what is actually going on, since when we return null it means that the passwords match, and if we return something else than null, it means that we want to mark that the passwords do not match.
equalPasswords = (control: AbstractControl): {[key: string]: boolean} =>{
const newPassword = control.get('NewPassword');
const confirmPassword = control.get('ConfirmPassword');
if (!newPassword || !confirmPassword) {
return null;
}
return newPassword.value === confirmPassword.value ? null : { notEqual: true };
}
Here's a DEMO with a shortened version of your code.