Home > OS >  Angular custom form control (Required) not working
Angular custom form control (Required) not working

Time:11-15

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

  • Related