Angular custom form control (Required) not working

472 Views Asked by At

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="input-container"
    [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 class="mat-form-field-center">
            <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>
2

There are 2 best solutions below

0
Parth M. Dave On

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") });
0
Julien BEAUDOUX On

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" class="MatHintFormulaire">
                    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