I've been working on adding a custom control to a form, I would like to do this because I know we have multiple components that may make up a form.
So for example you may have something like in app.components.ts:
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder, FormControl, Validators, AbstractControl } from '@angular/forms';
import { Model } from './cool-array/cool-array.component';
@Component({
selector: 'app-root',
styleUrls: ['./app.component.css'],
template:`
<!--The content below is only a placeholder and can be replaced.-->
<div class="col-md-6">
<form [formGroup]="form" >
<div class="form-group">
<label >Name</label>
<input formControlName="name" type="text" class="form-control" >
</div>
<app-cool-array formControlName="items"></app-cool-array>
</form>
</div>
<div class="col-md-6">
<div class="row">
IsValid Form: <strong> {{form.valid}}</strong>
Total Errors: <strong>{{form.errors? form.errors.length:0}}</strong>
</div>
<div class="row">
<table class="table table-stripped">
<thead>
<tr>
<th>Error</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let error of form.errors">
</tr>
</tbody>
</table>
</div>
</div>
`
})
export class AppComponent implements OnInit, AfterViewInit {
/**
*
*/
constructor(private formBuilder:FormBuilder) {
}
form:FormGroup;
model:MyModel;
ngOnInit(): void {
this.model = new MyModel();
this.form = this.formBuilder.group({ });
this.form.addControl('name', new FormControl('', Validators.required));
this.form.addControl('items', new FormControl([],[(control)=>MyValidator.MustHaveOne(control) ]))
}
ngAfterViewInit(): void {
console.log(this.form.errors)
}
}
export class MyValidator{
static MustHaveOne(control:AbstractControl){
if(control.value.length === 0) return {'length':'Items Must Have at least 1 item'};
return null;
}
}
export class MyModel{
name:string='';
items:Model[]=[];
}
You also may want to add the child component:
import { Component, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-cool-array',
styleUrls: ['./cool-array.component.css'],
providers:[
{
provide:NG_VALUE_ACCESSOR,
multi:true,
useExisting:CoolArrayComponent
}
],
template:`
<button (click)="onAdd()" class="btn btn-primary" >Add</button>
<button (click)="onRemove()" class="btn">Remove</button>
<table class="table table-striped table-responsive">
<thead>
<tr>
<th>Something Required</th>
<th>Something Not Requred</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items" (click)="onSelectedItem(item)" [ngClass]="{'selected-item':item === selectedItem}">
<td><input class="form-control" required [(ngModel)]="item.somethingRequired"/></td>
<td><input class="form-control" [(ngModel)]="item.somethingNotRequired"/></td>
</tr>
</tbody>
</table>
`
})
export class CoolArrayComponent implements OnInit, ControlValueAccessor {
onChange:any = ()=>{};
onTouched: any = () => { };
writeValue(obj: any): void {
this.items = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
disabled:boolean=false;
items:Model[]=[];
selectedItem:Model;
constructor() { }
ngOnInit() {
}
onAdd():void{
this.items.push(new Model());
}
onRemove():void{
let index = this.items.indexOf(this.selectedItem);
if(index>-1)
this.items.splice(index, 1);
}
onSelectedItem(event){
this.selectedItem = event;
}
}
export class Model{
somethingRequired:string;
somethingNotRequired:string;
get isValid():boolean{
return this.somethingRequired !==undefined && this.somethingRequired.length>0;
}
}
When the child component becomes invalid this should set the form to be invalid. I have tried adding a CustomValidator, however, it is never fired when the values change in the underlying array.
Can someone please explain why this is?
Okay, so I had to do a few things here to get this to work I will not post the entire code just the relevant parts.
I had to do is add a new custom Validator
static ArrayMustBeValid(control:AbstractControl){ if(control.value){ if(control.value.length>0){ let items:Model[] = control.value;
}
Then I had to add an update event on the input this should fire on keyup
Had to add the validator to
FormControlinapp.component.tsthis.form.addControl('items', new FormControl([],[(control)=>MyValidator.MustHaveOne(control), (control)=>MyValidator.ArrayMustBeValid(control) ]))