I have a form embeding a form Array I use to dynamically add /remove subcomponents :
this.mainForm = this.formBuilder.group({
foos: this.formBuilder.array([], [Validators.required]),
)};
get foos() {
return this.quoteForm.get('foos') as FormArray;
}
getfoos(): FormArray {
return this.quoteForm.get('foos') as FormArray;
}
onAddfoo() {
this.getfoos().push(this.formBuilder.control(''));
}
onDelfoo(event) {
this.getfoos().removeAt(event.currentTarget.id.replace('fooRmv_', ''));
}
I subcribe to form change for real time form data treatment, but when I log the data value sent by the event, I only have a single item of the formArray that is valid, other formArray items are {}.
by default, it is the last formArray item that is correct, but if I change a value in another item than the last one, then the modified one is correct and others are {}
this.mainForm.valueChanges.takeUntil(this.destroy$).subscribe(val => {
console.log('VALUE CHANGE');
console.log('VAL '+JSON.stringify(val));
});
console.log: {"foos":[{},{"id":1,"bar":1,"lorem":13,...}]}
what did I miss?
EDIT 1 :
Here is the template :
<nb-card>
<nb-card-header>{{ 'FOOS' | translate }}</nb-card-header>
<nb-card-body>
<div class="row">
<div class="col-sm-12">
<ng-container formArrayName="foos"
*ngFor="let ctr of foos.controls; let i=index">
<div class="fooRmv" style="text-align:right;"><button type="button" id="fooRmv_{{ i }}" nbButton ghost size="tiny" status="danger" (click)="onDelFoo($event)"><nb-icon icon="close-square-outline"></nb-icon></button></div>
<ngx-foo id="foo_{{ i }}" [formControlName]="i" [inErrors]=""></ngx-foo>
</ng-container>
<div>
<button type="button" nbButton outline size="small" status="primary" (click)="onAddFoo()" outline style="margin-top: 10px; margin-left: 15px;">{{ 'ADD' | translate }}</button>
</div>
</div>
</div>
</nb-card-body>
</nb-card>
NB: I have another formArray in the real Form, which works great BUT, If I modify it (add/remove or modify items), the foos fromArray in val (the other one) becomes [{},{},...] (all empty objects)
Here is my sub component :
// describes what the return value of the form control will look like
export interface FooFormValues {
id: number,
bar: number,
...
}
@Component({
selector: 'ngx-foo',
templateUrl: './foo.component.html',
styleUrls: ['./foo.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FooComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FooComponent),
multi: true
}
]
})
export class FooComponent implements OnInit, OnDestroy, ControlValueAccessor {
@ViewChild(ZtCardComponent) ZtCardComp: ZtCardComponent;
@Input() inErrors: any[];
destroy$: Subject<boolean> = new Subject<boolean>();
fooForm: FormGroup;
zorgs: Zorgs;
showCards: boolean = false;
cardsSettings: CardSettings[]= [];
ftAlert: boolean = false;
zorgAlert: string = null;
constructor(
private formBuilder: FormBuilder,
private messagesService: MessagesService,
public translate: TranslateService,
) {
}
get f() { return this.fooForm.controls; }
/////////////////////////////////////////////////////////
////// OnInit & onDestroy
/////////////////////////////////////////////////////////
ngOnInit(): void {
this.initForm();
this.fooForm.valueChanges.takeUntil(this.destroy$).subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.unsubscribe();
}
ngAfterViewInit(){
setTimeout(()=>{
this.fooForm.updateValueAndValidity();
})
}
//////////////////////////////////////////////////////////////////////////////
///// Control Value Accessor
//////////////////////////////////////////////////////////////////////////////
get value(): FooFormValues {
return this.fooForm.value;
}
set value(value: FooFormValues) {
//if( value !== undefined && this.value !== value){
if( value !== undefined ){
if (value.zorgs) {
this.zorgs=value.zorgs;
var val = value;
delete val.zorgs;
if (Object.keys(val).length !== 0) {this.fooForm.patchValue(val, { emitEvent: false })};
this.configResult();
this.configCard();
}
else {
this.fooForm.patchValue(val);
this.onChange(value);
this.onTouched();
}
}
}
onChange: any = () => {}
onTouched: any = () => {
}
// this method sets the value programmatically
writeValue(value) {
if (value) {
this.value = value;
}
if (value === null) {
this.fooForm.reset();
}
}
// upon UI element value changes, this method gets triggered
registerOnChange(fn) {
this.onChange = fn;
}
// upon touching the element, this method gets triggered
registerOnTouched(fn) {
this.onTouched = fn;
}
// communicate the inner form validation to the parent form
validate(_: FormControl) {
return this.fooForm.valid ? null : { profile: { valid: false } };
}
get errors() {
return this.fooForm.errors ? null : this.fooForm.errors;
}
//////////////////////////////////////////////////////////////////////////////
///// Form Functions
//////////////////////////////////////////////////////////////////////////////
initForm() {
this.fooForm = this.formBuilder.group({
id: null,
bar: ['10', [Validators.required, Validators.min(1)]],0)]],
});
this.zorgs=null;
}
onSubmitForm() {}
//////////////////////////////////////////////////////////////////////////////
///// Config Functions
//////////////////////////////////////////////////////////////////////////////
configResult() {
if(this.zorgs && this.zorgs.pfzorgs.length>0) {
var sfid = this.fooForm.get('selectedztid').value;
var sf = null;
if (sfid && sfid!=='') { sf = this.zorgs.pfzorgs.find(item => item.ztid==sfid); }
else {sf = this.zorgs.pfzorgs.find(item => item.aiselected==true); }
if (!sf) {
this.fooForm.patchValue({
selectedztid: null,
zoomzorg: null,
},{ emitEvent: false });
this.ftAlert = false;
this.zorgAlert = null;
}
else {
this.fooForm.patchValue({
selectedztid: sf.ztid,
zoomzorg: sf.zoomzorg,
},{ emitEvent: false });
this.ftAlert = sf.ftalert;
this.zorgAlert = (sf.zorgalert)? sf.zorgalert : null;
}
}
else {
this.fooForm.patchValue({
selectedztid: null,
toolingzorg: null,
},{ emitEvent: false });
this.ftAlert = false;
this.zorgAlert = null;
}
}
configCard() {
console.log('CONFIGCARD');
if(this.zorgs && this.zorgs.pfzorgs.length>0) {
this.cardsSettings=[];
this.zorgs.pfzorgs.forEach(pfp => {
this.cardsSettings.push({
ztid: pfp.ztid,
title: pfp.label,
iconClass: 'nb-lightbulb',
type: (!pfp.valid)? 'danger' : (pfp.exlcuded)? 'warning' : 'info',
selected: (pfp.aiselected)? true : false,
infos: (!pfp.valid)? null : [
{key: 'LOREM', val: pfp.lorem},
...
],
});
});
this.showCards = true;
}
else {
this.showCards = false;
this.cardsSettings= [];
}
}
//////////////////////////////////////////////////////////////////////////////
///// Event Functions
//////////////////////////////////////////////////////////////////////////////
onCardClick(event) {
if (event) {
const card = this.cardsSettings.find(item => item.ztid==event.ztid);
if (card.selected===false) {
const sf = this.zorgs.pfzorgs.find(item => item.ztid==event.ztid);
if (!sf.valid) {this.messagesService.showToaster('error', this.translate.instant('SELINVALIDFORMATERROR'));}
else {
this.cardsSettings.forEach(cs => {
cs.selected=(cs.ztid==event.ztid)? true : false;
});
this.fooForm.patchValue({
...
},{ emitEvent: false });
this.ftAlert = sf.ftalert;
this.zorgAlert = (sf.zorgAlert)? sf.zorgAlert : null;
if (!sf.excluded) {this.messagesService.showToaster('warning', this.translate.instant('SELEXCLUDEDFORMATWARN'));}
}
}
}
}
}
NB: I have another little issue on this sub component, if it can help solving, the cards compoentns needs sometime a page scroll to display...
EDIT 2 :
I remade the full sub component :
@Component({
selector: 'ngx-foo',
templateUrl: './foo.component.html',
styleUrls: ['./foo.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FooComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FooComponent),
multi: true
}
]
})
export class FooComponent implements OnInit, OnDestroy, ControlValueAccessor {
@Input() inErrors: any[];
destroy$: Subject<boolean> = new Subject<boolean>();
fooForm: FormGroup;
constructor(
private formBuilder: FormBuilder,
public translate: TranslateService,
) {
}
get f() { return this.fooForm.controls; }
/////////////////////////////////////////////////////////
////// OnInit & onDestroy
/////////////////////////////////////////////////////////
ngOnInit(): void {
this.initForm();
this.fooForm.valueChanges.takeUntil(this.destroy$).subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.unsubscribe();
}
ngAfterViewInit(){
setTimeout(()=>{
this.fooForm.updateValueAndValidity();
})
}
//////////////////////////////////////////////////////////////////////////////
///// Control Value Accessor
//////////////////////////////////////////////////////////////////////////////
get value(): FooFormValues {
return this.fooForm.value;
}
set value(value: FooFormValues) {
//if( value !== undefined && this.value !== value){
if( value !== undefined ){
const keys = Object.keys(value);
if (!keys.includes('id') && ...)
{
this.fooForm.patchValue(value,{ emitEvent: false });
}
else {
this.fooForm.patchValue(value);
this.onChange(value);
this.onTouched();
}
this.configResult();
}
}
onChange: any = () => {}
onTouched: any = () => {
}
// this method sets the value programmatically
writeValue(value) {
if (value) {
this.value = value;
}
if (value === null) {
this.fooForm.reset();
}
}
// upon UI element value changes, this method gets triggered
registerOnChange(fn) {
this.onChange = fn;
}
// upon touching the element, this method gets triggered
registerOnTouched(fn) {
this.onTouched = fn;
}
// communicate the inner form validation to the parent form
validate(_: FormControl) {
return this.fooForm.valid ? null : { profile: { valid: false } };
}
get errors() {
return this.fooForm.errors ? null : this.fooForm.errors;
}
//////////////////////////////////////////////////////////////////////////////
///// Form Functions
//////////////////////////////////////////////////////////////////////////////
initForm() {
this.fooForm = this.formBuilder.group({
id: null,
bar: null,
});
}
}
but when in main component I do
this.getFoos().controls.forEach((foo, index) => {
qtp.patchValue({
bar: toto,
}, { emitEvent: false });
});
the valueChange event function return val.foos = [{bar: toto}]. all other fields are removed...