Reputation: 109
Stackblitz link: https://stackblitz.com/edit/angular-ivy-bafyye?file=src/app/components/user-details/user-details.component.ts
I have created nested reactive form for user's car details as below:
user-details.component.ts
export interface User {
name: string;
car: Cars[];
}
export interface Cars {
id: Number;
company: CarCompany;
model: CarModel;
parts: CarPartName[];
registrationAndBillingDate: RegistrationAndBillingDate[];
}
export interface RegistrationAndBillingDate {
id: Number;
registrationDate: Date;
billingDate: Date;
}
export class CarCompany {
id: number;
name: string;
}
export class CarModel {
id: number;
name: string;
}
export class CarPartName {
id: number;
name: string;
}
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.css'],
})
export class UserDetailsComponent implements OnInit {
userDetailsForm: FormGroup;
submitted = false;
company: CarCompany[] = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Ferrari' },
{ id: 3, name: 'Toyota' },
];
model: CarModel[] = [
{ id: 1, name: 'SUV' },
{ id: 2, name: 'SEDAN' },
];
partName: CarPartName[] = [
{ id: 1, name: 'WHEELS' },
{ id: 2, name: 'FILTERS' },
];
registrationDate: Date;
expiryDate: Date;
userDetailsFormJson: any;
constructor(private fb: FormBuilder) {
this.userDetailsForm = new FormGroup({});
}
ngOnInit() {
this.createUserDetailsForm();
}
createUserDetailsForm() {
this.userDetailsForm = this.fb.group({
name: [null, Validators.required],
cars: this.fb.array([this.createCarsForm()]),
});
}
//car form
createCarsForm(): FormGroup {
return this.fb.group({
carCompany: this.createCarCompnayForm(),
carModel: this.createCarModelForm(),
carParts: this.fb.array([this.createCarPartsForm()]),
carRegistartaionAndBillingDate: new FormArray([
this.createRegistrationAndBillingDateForm(),
]),
});
}
createCarCompnayForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
createCarModelForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, Validators.required),
});
}
//form creation for car parts
createCarPartsForm(): FormGroup {
return this.fb.group({
partName: this.createCarPartNameForm(),
available: new FormControl(null),
});
}
createCarPartNameForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
//form creation for registration and billing cycle
createRegistrationAndBillingDateForm() {
return this.fb.group({
registrationDate: new FormControl(null, Validators.required),
billingDate: new FormControl(null, Validators.required),
});
}
get form() {
return this.userDetailsForm.controls;
}
get cars() {
return this.userDetailsForm.get('cars') as FormArray;
}
addCars() {
this.cars.push(this.createCarsForm());
}
removeCars(k: Required<number>) {
this.cars.removeAt(k);
}
getRegistrationAndBillingDate(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).controls;
}
addRegistrationAndBillingDate(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).push(this.createRegistrationAndBillingDateForm());
}
removeRegistrationAndBillingDate(index, j: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).removeAt(j);
}
addParts(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).push(this.createCarPartsForm());
}
getPartsForm(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).controls;
}
removeParts(index, l: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).removeAt(l);
}
onSubmit() {
this.submitted = true;
this.userDetailsFormJson = this.userDetailsForm.getRawValue();
}
}
user-details.component.html(ui portion)
<div>
<div>
<div>
<div [formGroup]="userDetailsForm">
<fieldset>
<legend>User Details</legend>
<div>
<table>
<tr></tr>
<tr>
<td>
<p>Name</p>
</td>
<td>
<input
size="35"
type="text"
formControlName="name"
style="width: min-content"
placeholder="enter user name"
/>
</td>
</tr>
</table>
</div>
</fieldset>
<fieldset>
<legend>Cars</legend>
<button class="btn btn-outline-primary" (click)="addCars()">
Add New Car
</button>
<ng-container formArrayName="cars">
<div class="row mt-2">
<div class="table-responsive">
<table class="table-bordered table_car">
<thead>
<tr>
<th>Company</th>
<th>Model</th>
<th>Registration and Billing</th>
<th>Parts</th>
<th></th>
</tr>
</thead>
<tbody *ngFor="let o of cars.controls; let k = index">
<tr class="table_car-tr" [formGroupName]="k">
<td>
<div formGroupName="carCompany">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Company
</option>
<option
*ngFor="let comp of company"
[ngValue]="comp.id"
>
{{ comp.name }}
</option>
</select>
</div>
</td>
<td>
<div formGroupName="carModel">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Model
</option>
<option
*ngFor="let mod of model"
[ngValue]="mod.id"
>
{{ mod.name }}
</option>
</select>
</div>
</td>
<td>
<table
class="table-responsive exp"
style="display: block"
formArrayName="carRegistartaionAndBillingDate"
>
<thead>
<tr>
<th>Registration Date</th>
<th>Billing Date</th>
<th>
<button
style="height: 24px"
(click)="addRegistrationAndBillingDate(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="
let reg of getRegistrationAndBillingDate(k);
let j = index
"
>
<tr
class="registration_and_billing_date-table-tr"
[formGroupName]="j"
>
<td>
<input
formControlName="registrationDate"
type="date"
/>
</td>
<td>
<input
formControlName="billingDate"
type="date"
/>
</td>
<td>
<button
style="height: 24px"
(click)="
removeRegistrationAndBillingDate(k, j)
"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<table class="table table-sm" formArrayName="carParts">
<thead>
<tr>
<th>Part Name</th>
<th>Available</th>
<th>
<button
style="height: 24px"
(click)="addParts(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="let part of getPartsForm(k); let l = index"
>
<tr [formGroupName]="l">
<td>
<div formGroupName="partName">
<select formControlName="id">
<option [ngValue]="null" disabled>
Select Part
</option>
<option
*ngFor="let pName of partName"
[ngValue]="pName.id"
>
{{ pName.name }}
</option>
</select>
</div>
</td>
<td>
<input
type="checkbox"
id="licensed"
formControlName="available"
(value)="(true)"
selected="true"
/>
</td>
<td>
<button
style="height: 24px"
(click)="removeParts(k, l)"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<button style="height: 24px" (click)="removeCars(k)">
x
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container>
</fieldset>
</div>
<div class="card-footer text-center">
<button (click)="onSubmit()" class="btn btn-primary">Submit</button>
</div>
</div>
<div class="card mt-2">
<div class="card-header">Result</div>
<div class="card-body">
<code>
<pre>
{{ userDetailsFormJson | json }}
</pre>
</code>
</div>
</div>
</div>
</div>
I have also included json output in the end of the form so that it could be easier to understand how the form data is structured.
JSON structure is as below:
{
"name": "User",
"cars": [{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
},
{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
}
]
}
I want to have validation in a cars table for company and model such that one company can have one car model in a row. If same car have same model in next row as well then the row needs to show message saying duplicate value or something similar.
Example: If car table has company value of Ford and model value of SUV and if in second row again if car company value is Ford and model value is SUV, it needs to say duplicate.
Also I want to validate dates section as well where registration date should be smaller than billing date. If billing date is smaller than registration date needs to throw validation error. I have tried it within html form as below:
<div style="color: red;font-size: 10px;margin-left: 10px;text-align: center;"
*ngIf=" reg.controls.registrationDate.value >reg.controls.billingDate.value">
Invalid Billing Date
</div>
Is there better way of doing this? And also I want to have validation in table in Part column car table for Part Name column. If part names are repeated I want to have validation text thrown. For example: I have Wheels and Filter as parts name. If Wheels is already there and if user again selects wheels validation should be kicked in. I am not able to figure out how validation should be done properly. So any kind of solution or suggestion would be great.
Upvotes: 2
Views: 2281
Reputation: 31225
Validations that imply several components can be handled with a custom validator, that I would place on the FormArray
A validator is a function that takes an AbstractControl
(usually, a FormControl
but here, it will be the FormArray
) and returns a ValidationErrors
if something goes wrong or null
if everything is fine.
ValidationErrors
is any key/value object you want so you can pass information about the constraint violation. For example, it can be :
{
minLength: 2,
maxLength: 20
}
You can get the error via formControl.errors
/ formArray.errors
. Typically, you want to use the name of the validator as a key and some detail about the constraint violation as the value.
Here is a proposition of implementation :
FormArray
to the IDs of the modelsnull
elementsSet
(which inherently deletes the duplicates) and compare its size to the size of the arrayValidationErrors
duplicateCarValidator(control: AbstractControl): ValidationErrors {
const modelIds = control.value
.map((car) => car.carModel?.id)
.filter((id) => id !== null && id !== undefined);
if (new Set(modelIds).size !== modelIds.length) {
return {
duplicates: true,
};
} else {
return null;
}
}
And add it like any validator :
cars: this.fb.array(
[this.createCarsForm()],
[this.duplicateCarValidator]
),
Here is a StackBlitz with some added console.log
: here
For registration date and billing date, this is another validator that imply 2 fields so it can be handled by another custom validator.
You can decide to place it :
FormGroup
The logic remains the same
Upvotes: 1