Angular stuff 2 custom component with ng value accessor

I am working on angular 4.4 + beta-12 custom beta component and can't figure out what's wrong with my implementation

I am trying to do the following user input
enter image description here

Task:

  • set the value of formControl as soon as I get the data from the server (data.productTeam is the data that can be seen in the code)
  • when editing, formcontrol should be updated with values ​​(for example: P12DT2H231M)

Questions:

  • I cannot bind the default to formcontrol.
  • Without ngDefaultControl (There is no accessor value to control the form named: error "productTeam")

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 forms-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> 
+5
source share
2 answers

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] // can be value like P12DT2H231M as well }); } onClick() { this.productForm.controls['productTeam'].patchValue('P12DT2H231M'); } } 

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; } } 
+4
source

Those who do not use the form builder or reactive forms, please use "ngDefaultControl" as an attribute in the input field.

0
source

Source: https://habr.com/ru/post/1273005/


All Articles