I a working on angular 4.4 + material beta12 custom component and not able to figure out what is wrong in my implementation
I am trying to achieve the below custom input
Task:
- set value to formControl, once I got data from server(data.productTeam is data-can see in code)
- on edit, formcontrol should be updated with values (eg:P12DT2H231M)
Issues:
- I am not able to bind default value to formcontrol.
- Without ngDefaultControl (No value accessor for form control with name: 'productTeam' error occuring)
dashboard.component.js
this.CRForm = this.fb.group({
productTeam: [data.productTeam || '']
});
In Dashboard.html
<mat-form-field floatPlaceholder="always" >
<app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ></app-mat-custom-form-field>
<!--<app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ngDefaultControl></app-mat-custom-form-field> -->
</mat-form-field>
{{custref.value}} -- gives value eg:[P12DT1H2M] and only if ngDefaultControl
{{CRForm['controls']['productTeam']['value']}} --not giving any
mat-custom-form-field.ts
import {
Component,
OnInit,
OnDestroy,
Input,
HostBinding,
Optional,
Renderer2,
Self,
forwardRef,
ElementRef
} from '@angular/core';
import {
MatFormFieldControl
} from '@angular/material';
import {
ControlValueAccessor,
FormGroup,
FormBuilder,
NgControl,
NG_VALUE_ACCESSOR
} from '@angular/forms';
import {
coerceBooleanProperty
} from '@angular/cdk/coercion';
import {
FocusMonitor
} from '@angular/cdk/a11y';
import {
Subject
} from 'rxjs/Subject';
class Duration {
constructor(public days: number, public hours: number, public minutes:
number) {}
getDuration() {
return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) + 'H' +
(this.minutes || 0) + 'M';
}
setDuration() {}
}
@Component({
selector: 'app-mat-custom-form-field',
templateUrl: './mat-custom-form-field.component.html',
styleUrls: ['./mat-custom-form-field.component.scss'],
providers: [{
provide: MatFormFieldControl,
useExisting: MatCustomFormFieldComponent
},
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MatCustomFormFieldComponent),
multi: true
}
]
})
export class MatCustomFormFieldComponent implements OnInit,
MatFormFieldControl < Duration > , ControlValueAccessor, OnDestroy {
parts: FormGroup;
focused = false;
stateChanges = new Subject < void > ();
errorState = false;
controlType = 'my-tel-input';
private _disabled = false;
private _required = false;
private _placeholder: string;
static nextId = 0;
@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();
}
/* code for placeholder property */
@Input()
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
@Input()
get value(): Duration | null {
let 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.parts.setValue({
days: duration.days,
hours: duration.hours,
minutes: duration.minutes
});
this.writeValue('P' + (duration.days || 0) + 'DT' + (duration.hours || 0) +
'H' + (duration.minutes || 0) + 'M');
this.stateChanges.next();
}
onContainerClick(event: MouseEvent) {
if ((event.target as Element).tagName.toLowerCase() != 'input') {
this.elRef.nativeElement.querySelector('input').focus();
}
}
/* code to get id and set id*/
@HostBinding() id = `mat-custom-form-
field-${MatCustomFormFieldComponent.nextId++}`;
@HostBinding('class.floating')
get shouldPlaceholderFloat() {
return this.focused || !this.empty;
}
@HostBinding('attr.aria-describedby') describedBy = '';
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef:
ElementRef,
renderer: Renderer2, public ngControl: NgControl, ) {
fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
ngControl.valueAccessor = this;
this.parts = fb.group({
'days': '',
'hours': '',
'minutes': '',
});
}
ngOnInit() {}
ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef.nativeElement);
}
get empty() {
let n = this.parts.value;
return !n.area && !n.exchange && !n.subscriber;
}
private propagateChange = (_: any) => {};
public writeValue(a: any) {
if (a !== undefined) {
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;
}
// not used, used for touch input
public registerOnTouched() {}
// change events from the textarea
}
mat-custom-form-field.html
< div[formGroup]="parts">
< input class="area" formControlName="days" size="3">
< span> & ndash; < /span>
< input class="exchange" formControlName="hours" size="3">
< span> & ndash; < /span>
< input class="subscriber" formControlName="minutes" size="3">
< /div>
First of all i modified your write value fn a bit cause it didn't work for me in case of null:
Custom component template stays the same. I consume this component in a sample form like this:
Form for tests
Simple AppComponent sets up the default value for our control (solving point 1) and also contains a simple click method which emulates the situation when you load your data from the server.
With this setup you are already able to work with your component and the default value will be set but you won't receive any changes yet.
In order to receive changes in your parent form you need to propagate them using propagateChange callback which is registered in your component(to solve point 2). So the main change to your component code will be a subscription to changes of the component internal form group from which you will propagate it to the upper level:
And i will also leave here the full code of the product-team-field.component.ts and Duration class just in case:
duration.ts
product-team-field.component.ts
Those who are not using form builder or reactive forms, please use "ngDefaultControl" as an attribute in your input field.