I'm fairly new to custom form controls in Angular and have been struggling with setting required fields while using a mat-stepper. I am trying to make a reusable address template. I've set required in both HTML and Ts but when I click on the next
button it moves to the next stepper. Any suggestions would be greatly appreciated.
Address Class Model
export declare class Address {
unitNumber: string;
streetName: string;
suburb: string;
city: string;
province: string;
postalCode: string;
}
Address TS
import { Component, ElementRef, HostBinding, Inject,
Input, OnDestroy, Optional, Self } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { Address, Helper, SelectOption } from '@trade-up/common';
import { Subject } from 'rxjs';
import { MatFormField, MatFormFieldControl, MAT_FORM_FIELD } from '@angular/material/form-field';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{provide: MatFormFieldControl, useExisting: AddressFormComponent}
]
})
export class AddressFormComponent implements OnDestroy, ControlValueAccessor, MatFormFieldControl<Address> {
@HostBinding('attr.id')
externalId: string;
@HostBinding('class.floating')
get shouldLabelFloat(): boolean {
return this.focused || !this.empty;
}
@HostBinding('class.invalid')
get valid(): boolean {
return this.touched;
}
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id(): string {
return this._ID;
}
provinces: SelectOption[] = Helper.provinces;
address: Address;
stateChanges = new Subject<void>();
/*tslint:disable-next-line*/
private _ID: string;
/*tslint:disable-next-line*/
private _placeholder: string;
/*tslint:disable-next-line*/
private _required: boolean;
/*tslint:disable-next-line*/
private _disabled: boolean;
focused: boolean;
errorState: boolean;
controlType?: string;
autofilled?: boolean;
userAriaDescribedBy?: string;
touched: boolean;
get empty(): boolean {
return !this.address.unitNumber && !this.address.streetName && !this.address.suburb
&& !this.address.province && !this.address.city && !this.address.postalCode;
}
@Input()
get placeholder(): string {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
@Input()
get required(): boolean {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(dis) {
this._disabled = coerceBooleanProperty(dis);
this.stateChanges.next();
}
onChange = () => {};
onTouched = () => {};
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(add: Address): void {
this.address = add;
}
get value(): Address {
return this.address;
}
set value(add: Address) {
this.address = add;
this.stateChanges.next();
}
setDescribedByIds(ids: string[]): void {
this.userAriaDescribedBy = ids.join(' ');
}
onContainerClick(): void {
}
ngOnDestroy(): void {
this.stateChanges.complete();
}
/*tslint:disable-next-line*/
constructor(@Optional() @Self() public ngControl: NgControl, @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField,
/*tslint:disable-next-line*/
private _elementRef: ElementRef<HTMLElement>) {
if (ngControl !== null) {
ngControl.valueAccessor = this;
}
this.address = new Address();
}
}
Address Html
<div role="group"
[class.invalid]="valid"
[attr.aria-labelledby]="_formField?.getLabelId()">
<mat-form-field>
<mat-label>Unit Number</mat-label>
<input matInput
[(ngModel)]="address.unitNumber"
[class.invalid]="valid"
type="text"
maxlength="5"
required>
</mat-form-field>
<br>
<mat-form-field>
<input matInput
[(ngModel)]="address.streetName"
[class.invalid]="valid"
type="text"
placeholder="Street Name"
maxLength="45"
required>
</mat-form-field>
<br>
<mat-form-field>
<input matInput
[(ngModel)]="address.suburb"
[class.invalid]="valid"
type="text"
placeholder="Suburb"
maxLength="45"
required>
</mat-form-field>
<br>
<mat-form-field>
<mat-label>Province</mat-label>
<mat-select placeholder="Province"
[(ngModel)]="address.province"
[class.invalid]="valid"
required>
<mat-option *ngFor="let province of provinces" [value]="province.value">
{{province.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
<br>
<mat-form-field>
<input matInput
type="text"
[(ngModel)]="address.city"
[class.invalid]="valid"
placeholder="City"
maxLength="45"
required>
</mat-form-field>
<br>
<mat-form-field>
<input matInput
type="text"
[(ngModel)]="address.postalCode"
[class.invalid]="valid"
placeholder="Postal Code"
maxlength="4"
pattern="[0-9]*"
required>
</mat-form-field>
</div>
Collection Address TS
this.homeCollectionForm = this.formBuilder.group({
homeAddress: [new Address(), Validators.required],
});
Collection Address HTML
<mat-horizontal-stepper linear="true">
<mat-step [stepControl]="homeCollectionForm">
<form [formGroup]="homeCollectionForm">
<mat-card>
<mat-card-content>
<mat-form-field >
<app-address-form formControlName="homeAddress" required></app-address-form>
</mat-form-field>
<button
mat-raised-button
matStepperNext
color="warn">
Next
</button>
</mat-card-content>
</mat-card>
</form>
</mat-step>
</mat-horizontal-stepper>
CodePudding user response:
make custom validator as below:
export function atLeastOneDetailRequireValidator(detailName: string) {
return (control: AbstractControl): { [key: string]: any } | null => {
if (control.get(detailName) !== null) {
const detail: any = control.get(detailName)!.value;
return detail == null || detail == undefined ? true : null;
} else {
return null;
}
};
}
this.homeCollectionForm = this.formBuilder.group({
homeAddress: [new Address(), Validators.required],
}, { validators: atLeastOneDetailRequireValidator("homeAddress") });
CodePudding user response:
Im not sure you really understand how FormControl / FormGroup should work so let me explain it :)
When you create a form you can specify a FromGroup AND on each input, you have to specify the name of the formControl that you create in your FormGroup.
<form (ngSubmit)="validateData()" [formGroup]="PlanningFormGroup">
<!-- Some code for responsive -->
<mat-form-field appareance="fill">
<mat-label>Date</mat-label>
<input matInput [matDatepicker]="picker" formControlName="DateControl">
<mat-hint *ngIf="this.PlanningFormGroup.get('DateControl').valid">
DD/MM/YYYY
</mat-hint>
<mat-hint *ngIf="!this.PlanningFormGroup.get('DateControl').valid" >
Obligatoire
</mat-hint>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field appareance="fill">
<mat-label>Day</mat-label>
<mat-select formControlName="DayControl" required>
<mat-option *ngFor="let day of listday" [value]="day">
{{day.libelle}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appareance="fill">
<mat-label>Week</mat-label>
<input matInput type="number" required formControlName="WeekControl">
</mat-form-field>
<!-- Some code for responsive -->
</form>
this.MyFormGroup = new FormGroup({
DateControl: new FormControl(undefined,
[
Validators.required
]),
DayControl: new FormControl(undefined,
[
Validators.required
]),
WeekControl: new FormControl(undefined,
[
Validators.required,
Validators.min(1),
Validators.max(53)
]),
})
// Get Value
this.MyFormGroup.get('DateControl').value // Return date
// Set Value
this.MyFormGroup.set('DayControl').setValue(20);
// Check if your formControl is valid
this.MyFormGroup.get('WeekControl').valid
So now , all you have to do is to put Validators where you need it OR create you'r own validators like @Parth M. Dave say
Next : Remove all your [(NgModel)] and replace it by FormControl, NgModel is not recommended