0

I'm creating a reactive form in Angular that involves users putting in a request for any number of parts, which display as rows of inputs.

For each part, certain fields are required and some should only be numbers. I've included validators for this, but no matter what happens the inputs don't show as invalid. I have validators outside of the parts that work just fine and display errors when the input is empty.

On the opposite end, I have an optional "Notes" field with no validators that is acting as if it has Validators.required and is invalid when empty.

Is there something I'm doing wrong that's causing these issues? I've tried looking for a solution but have had no luck so far.

This is the program running, showing an empty "Notes" input being invalid and a "Quantity" input with letters not being invalid.

create.component.html

<h1>New Request</h1>

<div>
  <form [formGroup]="reqForm" (ngSubmit)="onSubmit(reqForm.value)">
    <div class="form-row">
      <div class="form-group col-md-6">
        <label for="name">Name</label>
        <input type="text" name="name" class="form-control" id="name" placeholder="Name" formControlName="name"/>
        <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger">
          <div *ngIf="name.errors.required">
            Name is required
          </div>
        </div>
      </div>
      <div class="form-group col-md-6">
        <label for="reqNo">Req No.</label>
        <input type="text" class="form-control" id="reqNo" name="reqNo"
               placeholder="Numbers only, will auto-assign if blank" formControlName="reqNo"/>
      </div>
    </div>
    <div class="form-row">
      <div class="form-group col-md-9">
        <label for="source">Recommended Source</label>
        <input type="text" name="source" class="form-control" id="source" placeholder="Source" formControlName="source"/>
        <div *ngIf="source.invalid && (source.dirty || source.touched)" class="alert alert-danger">
          <div *ngIf="source.errors.required">
            Source is required
          </div>
        </div>
      </div>
      <div class="form-group col-md-3">
        <label for="dateRequired">Date Required</label>
        <input name="dateRequired" class="form-control dateRequired"
               placeholder="Date Required" matInput [matDatepicker]="dateRequired" formControlName="date">
        <div class="dateRequiredPicker">
          <mat-datepicker-toggle matSuffix [for]="dateRequired"></mat-datepicker-toggle>
          <mat-datepicker #dateRequired></mat-datepicker>
        </div>
        <div *ngIf="date.invalid && (date.dirty || date.touched)" class="alert alert-danger">
          Invalid date <br />(Ex. 1/1/2020)
        </div>
      </div>
    </div>
    <h2>Parts</h2>
    <div class="form-row">
      <div class="col-md-1">
        <h5>Quantity</h5>
      </div>
      <div class="col-md-1">
        <h5>Unit</h5>
      </div>
      <div class="col-md-2">
        <h5>Part Number</h5>
      </div>
      <div class="col-md-4">
        <h5>Description</h5>
      </div>
      <div class="col-md-2">
        <h5>Unit Cost</h5>
      </div>
      <div class="col-md-2">
        <h5>Total Cost</h5>
      </div>
    </div>
    <div formArrayName="parts">
      <div *ngFor="let part of reqForm.controls.parts.controls; let i = index" name="parts[i]" [formGroupName]="i" ngDefaultControl>
        <div class="form-row partRow" id="partRow{{i}}">
          <div class="col-md-1">
            <input type="text" class="form-control quantity" name="parts[{{i}}].quantity"
                   id="quantity{{i}}" placeholder="Quantity" formControlName="quantity"/>
          </div>
          <div class="col-md-1">
            <select name="parts[{{i}}].unit" id="unit{{i}}" class="form-control unit" formControlName="unit">
              <option *ngFor="let unitOption of unitOptions" [value]="unitOption">{{unitOption}}</option>
            </select>
          </div>
          <div class="col-md-2">
            <input type="text" class="form-control supplierPN" name="parts[{{i}}].supplierPN"
                   id="supplierPN{{i}}" placeholder="Part Number" formControlName="supplierPN"/>
          </div>
          <div class="col-md-4">
            <input type="text" class="form-control description" name="parts[{{i}}].description"
                   id="description{{i}}" placeholder="Description" formControlName="description"/>
          </div>
          <div class="col-md-2">
            <input type="text" class="form-control unitCost" name="parts[{{i}}].unitCost"
                   id="unitCost{{i}}" placeholder="Unit Cost" formControlName="unitCost"/>
          </div>
          <div class="col-md-2 totalCost">
            <span *ngIf="isNumber(reqForm.value.parts[i].quantity) && isNumber(reqForm.value.parts[i].unitCost); else noTotalCost">
              {{reqForm.value.parts[i].quantity * reqForm.value.parts[i].unitCost | currency : "$"}}
            </span>
            <ng-template #noTotalCost>N/A</ng-template>
          </div>
        </div>
      </div>
    </div>
    <div class="form-row buttons">
      <button type="button" mat-mini-fab color="primary" id="addRow" (click)="addRow()">+</button>
      <button type="button" mat-mini-fab color="warn" id="delRow" (click)="delRow()">-</button>
      <button type="button" mat-raised-button color="primary" (click)="copyPasteDialog()" id="pasteParts">Copy/Paste Parts From Excel</button>
    </div>
    <hr />
    <div class="form-row">
      <div class="col-md-2 form-check">
        <input type="checkbox" id="taxExempt" class="form-check-input" name="taxExempt" formControlName="taxExempt"/>
        <label class="form-check-label" for="taxExempt">Tax Exempt</label>
      </div>
      <div class="col-md-6"></div>
      <div class="col-md-2">
        <h5>Overall Cost:</h5>
      </div>
      <div class="col-md-2 overallCost">
        <span *ngIf="checkOverallCost(); else noOverallCost">{{overallCost | currency : "$"}}</span>
        <ng-template #noOverallCost>N/A</ng-template>
      </div>
    </div>
    <div class="form-row">
      <label for="notes">Notes</label>
      <input required type="text" class="form-control" id="notes" placeholder="Notes" name="notes" formControlName="notes"/>
    </div>
    <div class="form-row form-buttons">
      <button mat-raised-button color="warn" type="reset">Clear</button>
      <button mat-raised-button color="primary" [disabled]="reqForm.invalid" id="submit" type="submit">Submit</button>
    </div>
  </form>
</div>

create.component.ts

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { CreateService } from '../create.service';
import { PurReqPart } from '../interface';
import { CopyPasteDialogComponent } from '../copy-paste-dialog/copy-paste-dialog.component';
import { FormBuilder, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-create',
  templateUrl: './create.component.html',
  styleUrls: ['./create.component.css']
})
export class CreateComponent implements OnInit {
  unitOptions = ["Case", "Carton", "Dozen", "Each", "/Hour"]; // Used in an *ngFor for Unit select
  count: number = 0;
  overallCost;

  reqForm;
  get parts(): FormArray {
    return this.reqForm.get('parts') as FormArray;
  }

  constructor(private createService: CreateService, public dialog: MatDialog, private formBuilder: FormBuilder) {
    this.reqForm = this.formBuilder.group({
      name: ["", [
        Validators.required,
      ]],
      reqNo: [""],
      source: ["", [
        Validators.required
      ]],
      date: [new Date(), [
        Validators.required
      ]],
      taxExempt: [false],
      notes: [""],
      parts: this.formBuilder.array([
        this.formBuilder.group(new PurReqPart()),
      ]),
    });
  }
  
  ... // Other code is unrelated, validators are not set outside of initializing form controls

}

interface.ts (PurReqPart)

import { Validators } from "@angular/forms";

export class PurReqPart {
  numbersOnlyRegEx = '^[0-9]*$';
  constructor(
    public quantity?,
    public unit?,
    public supplierPN?,
    public description?,
    public unitCost?
  ) {
    quantity: [quantity, [
      Validators.required,
      Validators.pattern(this.numbersOnlyRegEx)
    ]];
    unit: [unit, [
      Validators.required
    ]];
    supplierPN: [supplierPN, []];
    description: [description, []];
    unitCost: [unitCost, [
      Validators.required
    ]];
  }
}
James E
  • 50
  • 8
  • I thought to change the `:`s in `interface.ts` to `=`s but this did not change anything – James E Oct 13 '20 at 13:26
  • Welcome to the horrors of Forms and Form Controls! I did a 7 month series on Form Controls and came to the conclusion that using NgModel is far superior. Here's the article I wrote on that: https://dev.to/jwp/angular-ngmodel-model-and-viewmodel-5m – JWP Oct 13 '20 at 13:33
  • @JohnPeters, NOT, really not, perhafs no-body explain you the Reactive Forms propertly. (Really that there're two ways to create a FormGroup -using FormBuilder or directly don't help-) but only the validators makes that Reactive forms are well worth. The posibility of subscribe to valueChange -with the powerfull of the Observables-, or don't check else on submit or on blur are anothers interesting capacities. Sure you need a new work-around to Reactive Forms – Eliseo Oct 13 '20 at 14:44
  • Actually you're off base, I spent 7 months investigating the fallacies of the Reactive Forms, and came to the conclusion that the ngModel if far superior. In that article I show how to keep changes synched between the model and the view. Just three lines of html code do it. How easy is that compared to what's shown above. Also validation is still able to be done, in the html no in code behind. – JWP Oct 13 '20 at 16:29
  • @JohnPeters, make a control that call to an api each change (but you need a debounceTime), or validate a control not equal to another one, or make an asyncValidation and indicate the control is pending validate... I can not imagine how make something without Reactive Forms.By the way, check https://netbasal.com/why-its-time-to-say-goodbye-to-angular-template-driven-forms-350c11d004b and the Ward Bell's comment -more closer to your opinion- or this SO https://stackoverflow.com/questions/39142616/what-are-the-practical-differences-between-template-driven-and-reactive-forms – Eliseo Oct 14 '20 at 17:16
  • Basal's article is about 2 years old. The other link also trumpets the value of using explicit form controls. The information I show below doesn't need form groups or form controls because the form control is implicit to the ngModel. Three lines of code in HTML and one PropertyChanged function is all that's needed. – JWP Oct 14 '20 at 17:24
  • I've figured out that the reason "Notes" was always acting like it was required was because I included `required` in the html. Doi. – James E Nov 03 '20 at 15:00

2 Answers2

1

James, I'am afraid that formBuilder methods don't "destructure" the arguments, so you can not do

//this NOT work
const obj=['',[Validators.maxLength(10)]]
this.control=this.formBuilder.control(obj)

So you can not do what you want it. You can make a function, some like

getPurReqPart(data:any=null)
{
   data=data || {quantity:0,unit:''...}
   return formBuilder.group({
      quantity:[data.quantity,[Validators.required,Validators.pattern(this.numbersOnlyRegEx)],
      ...
   })
}

But not in an interface. Angular has not some like DataAnotations of ASP.NET -I think that you are trying some similar-

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thank you! This solves the issue of parts' validators not working correctly, I did not know that formBuilder would not destructure its arguments. Unfortunately I'm still confused as to why my "Notes" section is still acting like it should be required despite not having validators. – James E Oct 15 '20 at 12:36
0

What is your addRow() function ? You also need to explicit your validators there I think.

  • `addRow()` pushes another `PurReqPart` object onto the `parts` array, validators are included in the constructor of `PurReqPart` – James E Oct 13 '20 at 13:46