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.
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
]];
}
}