0

I am building a simple custom form control that requests data and populates a mat-select. I have followed the angular tutorial and also several others... These are my 2 favorite https://sreyaj.dev/custom-form-controls-controlvalueaccessor-in-angular https://blog.angular-university.io/angular-custom-form-controls/

The dropdown is being populated. So the service is fine. I can post that if you need it.

The initial value is pushed into the control using writeValue() so I believe the ContolValueAccessor is working.

The formGroup is setup to validate onBlur()

The registerOnChange is getting called.

If I select a new option from the dropdown, the event fires and I can see that data.Value has the new selected option. I call onChanged and onTouched, but neither seem to affect the formGroup.

I can not seem to spot what I am doing wrong, or failing to do??

The other fields (name, description, etc) do cause the changed() event. So I believe the subscription to myFormGroup.changed is working.

The question is : **Why is onListChange() not causing the formGroup to emit a changed() event in the parent component. **

Here is the custom component

import { Component, SimpleChanges,Input, OnInit, forwardRef, Provider } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors } from '@angular/forms';
import { Observable, of  } from 'rxjs';
import { tap , switchMap } from 'rxjs/operators';

import { COA_Header } from '../COA_Header';
import { COA_Account } from '../COA_Account';
import { ChartOfAccoutsService } from '../services/chart-of-accouts.service';


const  VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CoaDropdownComponent),
  multi: true,
};

@Component({
  selector: 'app-coa-dropdown',
  templateUrl: './coa-dropdown.component.html',
  styleUrls: ['./coa-dropdown.component.css'], 
  providers:[
    VALUE_ACCESSOR
  ]
})
export class CoaDropdownComponent implements OnInit, ControlValueAccessor {
  
  @Input() myprojectId : number; 
  @Input() includeBlankOption : boolean = true;
  value : string ; 

  data$ : Observable<COA_Account[]>;
  touched : boolean = false;
  disabled : boolean = false; 
  constructor(private coaService : ChartOfAccoutsService) { }

  ngOnInit(): void {}

  
 ngOnChanges(changes: SimpleChanges) {
  
    this.data$ = this.coaService.getCOAByProjectId(this.myprojectId).pipe(
         switchMap((data : COA_Header) =>{
          let l : COA_Account[] = data.Accounts;  //return just the array of accounts


          return of(l);
        })
       
    );
    
  }



onListChange( data){
  console.log('coa item selected', data, this.touched); //this fires as expected
  this.value = data.value;
  this.markAsTouched();
  this.onChanged (data.value);
}

//these are pointers to functions that will be passed to us by FG
  onChanged : Function;
  onTouched : Function;
  
  registerOnChange(fn: any) {
       console.log('rc called');  // this is happening
      this.onChanged = fn;
  }
  
  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }
  
  writeValue(value_: string): void {
    console.log('fg set value',value_); //this gets called as expected
    this.value = value_;
     
  }

  markAsTouched() {
      if (!this.touched) {
        this.onTouched();
        this.touched = true;
      }
  }
  
  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
  }

  itemSelected(value_ : string){
    this.markAsTouched();
    this.value = value_;
    this.onChanged(value_);
  }

 
}

Here is the component.html

<ng-container *ngIf ="data$| async as data">

    
    <mat-select appearance="fill" 
            style="background-color: white;border-radius:3px;padding:4px;"
            value="{{value}}"
            (selectionChange)="onListChange($event)"
            >
        <ng-container *ngIf = "includeBlankOption">
            <mat-option value="">---</mat-option>    
        </ng-container>
        <mat-option    *ngFor="let acct of data" [value]="acct.Name">[{{acct.AccountType}}] {{acct.Name}}</mat-option>
        
    </mat-select>
        
     
</ng-container>

Here is the parent component

import { Component, OnInit , AfterViewInit
        , Input, Output, SimpleChanges 
        , ElementRef, EventEmitter, ViewChild} from '@angular/core';
import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
import { AbstractControl  , FormBuilder, FormGroup   
         , Validators
         , RequiredValidator, MaxLengthValidator, MinLengthValidator
  } from '@angular/forms';
import { IMyDpOptions, IMyDateModel, IMyDate, MyDatePicker } from 'mydatepicker';
import { Router, ActivatedRoute } from '@angular/router';

import { MatIconRegistry } from '@angular/material/icon';
import { Observable,forkJoin, combineLatest } from 'rxjs';
import { distinctUntilKeyChanged, pluck
  , switchMap ,tap, map} from 'rxjs/operators';

import {CashFlow2 } from '../cash-flow2'
import {cashFlowService } from '../services/cash-flow.service';
import { sbList} from '../../list-management/sbList';
import { ListManagementService } from '../../list-management/list-management.service';
import { userDefinedSetting } from '../../udsForm/user-defined-setting';
 import {ListDropDownComponent } from '../../list-management/list-drop-down/list-drop-down.component'
import { UdsFormComponent } from 'src/app/udsForm/uds-form/uds-form.component';

@Component({
  selector: 'app-cashflow-detail',
  templateUrl: './cashflow-detail.component.html',
  styleUrls: ['./cashflow-detail.component.css']
})
export class CashflowDetailComponent implements OnInit {
  

    
   myFormGroup: FormGroup;
  myItem : CashFlow2;  
  
  public vm$ : Observable<any>;
    private myData$ : Observable<any>;  //combines proj and pRev in one observable
  
 

  public myDatePickerOptions: IMyDpOptions = {
    
    todayBtnTxt: 'Today',
    dateFormat: 'mm/dd/yyyy',
    firstDayOfWeek: 'su',
    sunHighlight: true,
    satHighlight: true,
    inline: false,
    height: '25px' 
  }; 
  
    
  // }
  
   
  
        constructor(private myFormBuilder: FormBuilder 
          , private activatedRoute: ActivatedRoute
          , private iconRegistry: MatIconRegistry 
          , private cashFlowService : cashFlowService
          , private listService : ListManagementService
           ) {
 
        }
        
        
        ngOnInit(): void {
          this.buildForm();
         
              
      

            this.myData$ = this.activatedRoute.params.pipe(
              pluck('id')
              //, tap(data => console.log('cf id', data))
              ,switchMap(itemId => this.cashFlowService.getItemById(itemId).pipe(
                    map(cf => {
                      //console.log('cf retrieved',cf);
                      this.myFormGroup.patchValue(<CashFlow2>cf );
                      this.myItem = cf;
                      return cf; 
                    })
                )
              )
            );

      } //end ngOnInit
  
 
      
      buildForm( ) {
        this.myFormGroup = this.myFormBuilder.group(
            this.myFormGroupDefinition()
            ,{ updateOn: "blur" }
        );


        this.myFormGroup.valueChanges.subscribe(data => {
            this.saveMainForm();
           
             
        });

    }  //end buildForm



    //build the form based on a cashflow
    myFormGroupDefinition() {
            
            
      return {
          id :['' ,
                {
                validators:[ Validators.required]
              } 
            ],
            COA:['Mortgage'],   //this is the coaDropdown component
          clientId :['' ,
                {
                validators:[ Validators.required]
              } 
            ], 
    
          Name :['' ,
                {
                validators:[ Validators.required]
              } 
            ], 
       
          }
    }//end myFormGroupDefinition

    get f(): { [key: string]: AbstractControl } {
      return this.myFormGroup.controls.get(key);
    }

    saveMainForm(){
          if(null == this.myItem){
            console.log('init form, no change needed');
            return;
          }

          const mergedItem : CashFlow2 = {...this.myItem, ...this.myFormGroup.value};
          //console.log('form values spread into myItem', mergedItem);
          this.myItem = mergedItem;
          this.saveToService(this.myItem);

    }

    

    saveToService(itemToSave_ : CashFlow2){
            let obs = new Observable<CashFlow2>();
            obs = this.cashFlowService.saveItem(itemToSave_);
            obs.subscribe(
                (data) => {
                    this.myItem = data;
                    this.itemUpdated.emit(this.myItem);
                     
        
                });
    }


}

here is the parent HTML

<ng-template #loading>
    <div   >loading...</div>
</ng-template>  
   

  <div *ngIf ="myData$ | async   as allMyData ; else loading ">

    <div  class="formgrid"   [formGroup]="myFormGroup"  > 

      
          <label class="fieldLabel" for="Name">coa :</label>
          <app-coa-dropdown
          class="formValue"
          myprojectId="3137"
          formControlName="COA"
          value="Mortgage"
          required></app-coa-dropdown>
          <span class="fieldErrorMessage" >coa message</span>

          <label class="fieldLabel" for="Name">Name :</label>
          <input class="fieldValue" type="text"
          formControlName="Name"
          name="Name"
          placeholder="Name"
          />
          <span class="fieldErrorMessage" >{{f['Name'].errors! |json}}</span>
          
      
         
        

        <input  class="fieldValue" type="text"
              formControlName="Description"
              name="Description"
              placeholder="Description"
              />
        <label class="fieldLabel" for="Description">Description :</label>
        <span class="fieldErrorMessage" >{{f['Description'].errors! |json}}</span>

        
    </div>

 
   
                
    <div  class="formgrid"   [formGroup]="myFormGroup"  > 
      
                <label class="fieldLabel">Save</label>
                
                <span class="actionButton"
                matTooltip="Save item"
                (click)="saveItem()"
                >
                <button [disabled]="!myFormGroup.valid">
                  <i class="material-icons md-24 ">save</i>
                </button>
              </span>
              <span class="fieldErrorMessage"></span>
              
              
              <label class="fieldLabel">Delete</label>
        
                   <span class="actionButton"
                        matTooltip="Delete this item"
                        (click)="deleteItem()"
                        >
                        <i class="material-icons md-24 ">delete</i>
                      </span>
                      <span class="fieldErrorMessage"></span>
                  
        
                    </div>
                     
 </div> <!-- end ngIf-->
        

1 Answer 1

0

I always forget you can actually step into angular code ....

form.js has this...

function setUpViewChangePipeline(control, dir) {
    dir.valueAccessor.registerOnChange(function (newValue) {
        control._pendingValue = newValue;
        control._pendingChange = true;
        control._pendingDirty = true;
        if (control.updateOn === 'change')
            updateControl(control, dir);
    });
}

my whole form is set up to trigger on blur. when I set the COA form element to updateOn:'change' then i do get it to fire correctly.

To make blur work, i need to capture the matSelect onBlur and call onTouche

this question has also been addressed here... Angular custom FormControl with updateOn blur

excerpt from childcomponent.ts

onBlur(data){
  console.log('in blur')
  this.onTouched();
  console.log('exit blur');
}

excerpt from childcomponent.html

  <mat-select appearance="fill" 
            style="background-color: white;border-radius:3px;padding:4px;"
            value="{{value}}"
            (selectionChange)="onListChange($event)"
            (blur)="onBlur($event)"
            >
Sign up to request clarification or add additional context in comments.

Comments

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.