First of all, I changed your fn record value a little, because it did not work for me in case of null:
public writeValue(a: string) { if (a && a !== '') { this.parts.setValue({ days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')), hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')), minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M')) }); } }
The custom component template remains the same. I use this component in sample form as follows:
Test form
<div> <form #form="ngForm" [formGroup]="productForm"> <mat-form-field> <product-team-input formControlName="productTeam" placeholder="P12D" ></product-team-input> </mat-form-field> </form> {{ form.value | json }} </div>
A simple AppComponent sets the default value for our control (point 1 solution), and also contains a simple click method that emulates the situation when downloading data from the server.
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; data: string; productForm: FormGroup; constructor(private fb: FormBuilder) { this.productForm = this.fb.group({ productTeam: [null]
With this setting, you can already work with your component, and the default value will be set, but you will not get any changes yet.
To receive changes in your parent form, you need to distribute them using the propagateChange callback that is registered in your component (to solve point 2). Thus, the main change in your component code will be a subscription to changes to the internal group of component forms, from which you will distribute it to the upper level:
this.parts = fb.group({ 'days': '', 'hours': '', 'minutes': '', }); this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => { this.propagateChange(value); }));
And I will also leave here the full product code-team-field.component.ts and the Duration class just in case:
duration.ts
class Duration { constructor(public days: number, public hours: number, public minutes: number) { } toString() { return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) + 'H' + (this.minutes || 0) + 'M'; } }
product-team-field.component.ts
@Component({ selector: 'product-team-input', templateUrl: './product-team-field.component.html', styleUrls: ['./product-team-field.component.css'], providers: [{ provide: MatFormFieldControl, useExisting: ProductTeamControl }, { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ProductTeamControl), multi: true }] }) export class ProductTeamControl implements OnInit, OnDestroy, ControlValueAccessor, MatFormFieldControl<Duration> { static nextId = 0; ngControl = null; parts: FormGroup; focused = false; stateChanges = new Subject<void>(); errorState = false; controlType = 'product-team-input'; private _disabled = false; private _required = false; private _placeholder: string; @Input() get required() { return this._required; } set required(req) { this._required = coerceBooleanProperty(req); this.stateChanges.next(); } @Input() get disabled() { return this._disabled; } set disabled(dis) { this._disabled = coerceBooleanProperty(dis); this.stateChanges.next(); } @Input() get placeholder() { return this._placeholder; } set placeholder(plh) { this._placeholder = plh; this.stateChanges.next(); } @Input() get value(): Duration | null { const n = this.parts.value; if (n.days && n.hours && n.minutes) { return new Duration(n.days, n.hours, n.minutes); } return null; } set value(duration: Duration | null) { duration = duration || new Duration(0, 0, 0); this.writeValue(duration.toString()); this.stateChanges.next(); } onContainerClick(event: MouseEvent) { if ((event.target as Element).tagName.toLowerCase() !== 'input') { this.elRef.nativeElement.querySelector('input').focus(); } } @HostBinding() id = `${this.controlType}-${ProductTeamControl.nextId++}`; @HostBinding('class.floating') get shouldPlaceholderFloat() { return this.focused || !this.empty; } @HostBinding('attr.aria-describedby') describedBy = ''; setDescribedByIds(ids: string[]) { this.describedBy = ids.join(' '); } private subs: Subscription[] = []; constructor( private fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef, renderer: Renderer2) { this.subs.push(fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => { this.focused = !!origin; this.stateChanges.next(); })); this.parts = fb.group({ 'days': '', 'hours': '', 'minutes': '', }); this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => { this.propagateChange(value); })); } ngOnInit() { } ngOnDestroy() { this.stateChanges.complete(); this.subs.forEach(s => s.unsubscribe()); this.fm.stopMonitoring(this.elRef.nativeElement); } get empty() { const n = this.parts.value; return !n.area && !n.exchange && !n.subscriber; } private propagateChange = (_: any) => { }; public writeValue(a: string) { if (a && a !== '') { this.parts.setValue({ days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')), hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')), minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M')) }); } } public registerOnChange(fn: any) { this.propagateChange = fn; } public registerOnTouched(fn: any): void { return; } public setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } }