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:
Issues:
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:
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'))
});
}
}
Custom component template stays the same. I consume this component in a sample form like this:
Form for tests
<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>
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.
@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] // can be value like P12DT2H231M as well
});
}
onClick() {
this.productForm.controls['productTeam'].patchValue('P12DT2H231M');
}
}
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:
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 code of the product-team-field.component.ts and 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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With