0

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

5
  • Can you share your template ? Commented Jul 27, 2020 at 6:37
  • Could you upload a demo of your code (Stackblitz) Commented Jul 27, 2020 at 6:39
  • Thgank you, please see edit ! Commented Jul 27, 2020 at 7:11
  • please see edit 2 Commented Jul 27, 2020 at 10:27
  • Still on it and really need help guys ! here is a staclBlitz :stackblitz.com/edit/angular-ivy-roekhu. Not working that well, but the error is present : in console log, first line is ok with all subForm fields in val. after patchValue, only the modified value is present Commented Jul 28, 2020 at 7:07

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.