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