formArray and valueChange : only one element returned

570 Views Asked by At

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...

0

There are 0 best solutions below