disable in Angular Material custom field component not working - javascript

I have a custom slide toggle component created using Angular Material. I followed this guide: https://material.angular.io/guide/creating-a-custom-form-field-control
Everything seems to be working fine except when I dynamically disable the custom component like this:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disabled]="true"
></custom-slide-toggle>
The component is disabled but I get the console warning It looks like you're using the disabled attribute with a reactive form directive. ....
To solve it I tried setting disabled the recommended way like this: activeUser: new FormControl([{value:false, disabled: true}]) in the parent component but the custom component was not disabled.
I also tried the same thing but in the custom component itself but didn't have any effect on making the field disabled or not.
UPDATE:
I tried adding a formGroup binding to my custom component per #DKidwell suggestion but I still get the same warning It looks like you're using the disabled attribute.... I added the formGroup using FormBuilder to more closely match the Angular Material example.
UPDATE 2:
The solution I found was to create a custom directive in conjunction with adding the FormGroup binding per #DKidwell's answer. The custom directive I created was based on this post: https://netbasal.com/disabling-form-controls-when-working-with-reactive-forms-in-angular-549dd7b42110
I implemented the custom directive like so and removed the [disabled] decorator:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disableControl]="isEditable"
></custom-slide-toggle>
Here's my custom component typescript:
#Component({
selector: 'custom-slide-toggle',
templateUrl: './custom-slide-toggle.component.html',
styleUrls: ['./custom-slide-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomSlideToggleComponent)
},
{
provide: MatFormFieldControl,
useExisting: CustomSlideToggleComponent
}
],
host: {
'[id]': 'id'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSlideToggleComponent implements OnInit, OnDestroy, DoCheck, ControlValueAccessor, MatFormFieldControl<boolean> {
private static nextId = 0;
private _placeholder: string;
private _disabled = false;
private _required = false;
private _readlonly = false;
public stateChanges = new Subject<void>();
public errorState = false;
public focused = false;
public ngControl: NgControl;
public toggleFormGroup: FormGroup;
#HostBinding() public id = `custom-slide-toggle-${CustomSlideToggleComponent.nextId++}`;
#HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
#HostBinding('attr.aria-describedby') describedBy = '';
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
#Input() public toolTip: string = '';
#Input() public isChecked: boolean;
#Input() public toggleText: string = '';
#Input() public tabNumber: number = null;
#Input() public toggleId: string = '';
#Input() public toggleClass: string;
#Input()
public get disabled(): boolean {
return this._disabled;
}
public set disabled(value: boolean) {
console.log('set disabled trigged');
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.toggleFormGroup.disable() : this.toggleFormGroup.enable();
this.stateChanges.next();
}
#Input()
public get required() {
return this._required;
}
public set required(req: boolean) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
#Input()
public get readonly(): boolean {
return this._readlonly;
}
public set readonly(value: boolean) {
this._readlonly = coerceBooleanProperty(value);
this._readlonly ?
this.toggleFormGroup.get('toggleFormControl').disable() :
this.toggleFormGroup.get('toggleFormControl').enable();
this.stateChanges.next();
}
#Input()
public get value(): boolean {
let n = this.toggleFormGroup.value;
if (n.toggleFormControl !== null){
return n.toggleFormControl.value;
}
return null;
}
public set value(val: boolean) {
this.toggleFormGroup.setValue({toggleFormControl: val});
this.stateChanges.next();
this.onTouched();
}
#Input()
public get placeholder(): string {
return this._placeholder;
}
public set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
#Output() selectionChange: EventEmitter<boolean> = new EventEmitter<boolean>();
public constructor(private injector: Injector, fb: FormBuilder) {
this.toggleFormGroup = fb.group({
'toggleFormControl': ''
});
}
ngOnInit(): void {
this.ngControl = this.injector.get(NgControl);
if (this.ngControl != null) { this.ngControl.valueAccessor = this; }
}
ngOnDestroy(): void {
this.stateChanges.complete();
}
ngDoCheck(): void {
if(this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public toggleClick($event: MatSlideToggleChange) {
this.onChange($event.checked);
this.selectionChange.emit($event.checked);
}
public onChanged = (val: boolean) => {};
public onTouched = () => {};
writeValue(value: any) {
console.log('writeValue triggered, incoming value is: ' + value);
if (value !== this.inputControl.value) {
this.inputControl.setValue(value);
this.onChanged(value);
this.stateChanges.next();
}
}
get empty() {
if (this.inputControl?.pristine || this.inputControl?.untouched) return true;
else return false;
}
onBlur() {
this.onTouched();
}
onChange(val: boolean) {
this.writeValue(val);
}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
Here is my custom component's html:
<mat-slide-toggle
[formControl]="inputControl"
[id]="toggleId"
[class]="toggleClass"
color="primary"
labelPosition="after"
[checked]="isChecked"
[disabled]="disabled"
[required]="required"
(change)="toggleClick($event)"
[tabIndex]="tabNumber"
[matTooltip]="toolTip"
>{{toggleText}}</mat-slide-toggle>
How do I dynamically disable my custom component without getting the warning?

You need to add a formGroup binding to your custom component,
<div [formGroup]="yourFormGroup">
<mat-slide-toggle ...>
{{toggleText}}
</mat-slide-toggle>
</div>
You will also need to define that formGroup in your component,
FormGroup yourFormGroup = new FormGroup({
inputControl: new FormControl()
});
Once that is setup properly you shouldn't need to bind to the [disabled] property in your custom control's template.

Related

Angular 8.3.15 custom validator with parameter not working as expected

I'm having some trouble getting a custom validator working. I have other custom and non-custom validators working, but this one that I am passing a parameter to does not work as expected. Within the validator, it is recognizing that the validation code is working, but when looking at the form within field-validation-error, it is returning that the form is valid. Any help would be appreciated, thanks!
Within password.component.ts
this.passwordFormGroup = new FormGroup({
hintQuestionFormControl1: new FormControl(this.currentQuestions[0], Validators.required),
hintAnsFormControl1: new FormControl(this.currentAnswers[0], [Validators.required, EditAccountValidators.checkQuestionsDontContainAnswer('hintQuestionFormControl1')]),
});
Within edditAccountValidators.ts
export class EditAccountValidators {
public static checkQuestionsDontContainAnswer(correspondingQuestion: string): ValidatorFn {
return (control: FormControl) => {
if (control.parent) {
const question = control.parent.get(correspondingQuestion).value;
const answer = control.value;
if (question && answer) {
question.split(" ").forEach(function (wordInQuestion) {
answer.split(" ").forEach(function (wordInAnswer) {
if (wordInQuestion.toLowerCase().includes(wordInAnswer.toLowerCase())) {
console.log('same');
return {answerDoesntContainQuestion : true};
}
});
});
}
}
return null;
}
}
Within field-validation-error.component.ts
import {Component, Input, OnInit} from '#angular/core';
#Component({
selector: 'field-validation-error',
templateUrl: './field-validation-error.component.html',
styleUrls: ['./field-validation-error.component.css']
})
export class FieldValidationErrorComponent implements OnInit {
#Input() validatorName: string;
#Input() form: any;
errorMessage: string;
displayError: boolean;
ngOnInit(): void {
this.errorMessage = this.getValidatorErrorMessage();
this.form.valueChanges.subscribe(() => {
this.displayError = this.form.hasError(this.validatorName);
console.log(this.form);
});
}
private getValidatorErrorMessage() {
return this.validatorName;
}
}
calling Within password.component.html
<field-validation-error
[form]="passwordFormGroup.get('hintAnsFormControl1')"
[validatorName]="'answerDoesntContainQuestion'">
</field-validation-error>

Angular ControlValueAccessor with default value from internal causes dirty

I have a directive to build dynamic Input components for a template driven form.
The default value is set by the Input component itself.
The problem is that setting a default value causes that the form is marked as dirty.
How is it possible to archieve setting a default value from inside of the Directive without marking the form as dirty?
#Directive({
selector: '[myFormInputFactory]',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyFormFactoryDirective), multi: true }
]
})
export class MyFormFactoryDirective implements OnChanges, OnDestroy, ControlValueAccessor {
#Input() myFormInputFactory: DialogItem;
private componentRef: any;
private value: any;
private valueSubscription: Subscription;
private disabled = false;
constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
private _renderer: Renderer,
private _elementRef: ElementRef
) { }
onChange = (_: any) => { };
onTouched = () => { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
ngOnChanges(changes: SimpleChanges) {
if ('myFormInputFactory' in changes) {
const config = changes['myFormInputFactory'].currentValue as IConfigItem;
const factories = Array.from(this.componentFactoryResolver['_factories'].values());
const comp = factories.find((x: any) => x.selector === config.selector) as ComponentFactory<{}>;
const componentRef = this.viewContainerRef.createComponent(comp);
if (this.componentRef) {
this.componentRef.destroy();
}
this.componentRef = componentRef;
this.valueSubscription = this.componentRef._component.valueChange.subscribe(value => {
this.value = value;
this.onChange(this.value);
});
}
}
ngOnDestroy() {
if (this.valueSubscription) {
this.valueSubscription.unsubscribe();
}
}
writeValue(value: string): void {
if (this.value !== null) {
this.onChange(this.value);
}
if (value !== undefined && value !== null) {
this.value = value;
}
}
}
UPDATE
I have created a StackBlitz
Could you create StackBlitz post for better debugging?
I think part of the problem might be accessing the input rather than accessing the FormControl itself. Accessing the input itself directly then triggers the onChange event and marks the input as dirty which, in my opinion, might be a problem.
How are you using the directive? Would it be possible to do the following?
Create FormGroup in parent component
When using myFormInputFactory directive, pass appropriate FormControl reference to the directive and assign the value to the control itself:
this.formgroup.setValue({
key: value
},{emitEvent: false})
For what it's worth, I think the issue is because you call this.onChange within writeValue.
writeValue is used to notify the control that value has been changed externally by the form, so calling onChange to notify the form about the the change is needless.

Custom ControlValueAccessor in template-driven forms

I have a custom <data-input-text> component which has two modes: regular and disabled. Here is the template (I've simplified it a bit for demo case):
<label *ngIf="!disabled"
class="field-label"
[ngClass]="{'focused' : isFocused, 'with-errors' : errors}">
<input class="field-value"
[type]="type"
[required]="required"
(focus)="onFocus()"
(blur)="onBlur()"
[(ngModel)]="value"
#fieldInput="ngModel">
</label>
<div class="field-label" *ngIf="disabled">
<span class="field-value">{{ value }}</span>
<span class="field-name">{{ label }}</span>
</div>
In the parent form, I use this component in the following way:
<form #profileForm="ngForm">
<data-text-input
label="lastName"
[required]="true"
[disabled]="userIsRegistered"
name="lastName"
ngModel></data-text-input>
</form>
userIsRegistered returns a boolean, which should switch between the input field or spans within the component. It all works fine until here.
I set the form in the parent component to match the BehaviorSubject like this:
this._sub = this.dl.selectedEmployee.subscribe( u => {
if ( u.id ) {
this.isLoading = false;
setTimeout( () => {
this.profileForm.setValue(u);
this.profileForm.control.markAsPristine();
}, 10);
}
});
Here is the custom ControlValueAccessor component:
import { Component, Input, ViewChild, forwardRef,
AfterViewInit, OnInit, OnChanges,
NgModule } from '#angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS,
ControlValueAccessor, FormControl,
Validator, NgForm } from '#angular/forms';
#Component({
selector: 'data-text-input',
template: `
<label *ngIf="!disabled"
class="field-label">
<input class="field-value"
[type]="type"
[required]="required"
(blur)="onBlur()"
[(ngModel)]="value"
#fieldValue="ngModel">
<span class="field-name">{{ label }}</span>
</label>
<div class="field-label" *ngIf="disabled">
<span class="field-value">{{ value }}</span>
<span class="field-name">{{ label }}</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef( ()=> DataTextInputComponent ),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef( ()=> DataTextInputComponent ),
multi: true
}
]
})
export class DataTextInputComponent implements OnChanges, ControlValueAccessor, Validator {
#Input() public disabled: boolean = false;
#Input() public label: string;
#Input() public required: boolean = false;
#Input() public type: string = 'text';
#ViewChild('fieldValue') public fieldValue: FormControl;
// infrastructure
public registerOnChange(fn: any) { this.propagateChange = fn; }
public registerOnTouched(fn: any) { this.propagateTouch = fn; }
private propagateChange = (_: any) => { };
private propagateTouch = (_: any) => { };
/**
* inner value
*/
private innerValue: any = null;
/**
* on changes hook
*/
public ngOnChanges(): void {
if ( this.disabled ) {
this.propagateChange(this.innerValue);
}
}
/**
* input events
*/
public onBlur(): void {
this.propagateChange(this.innerValue);
this.propagateTouch(true);
}
/**
* value accessor setter and getter
*/
public get value(): any {
return this.innerValue;
};
public set value(value: any) {
if ( value !== 'undefined' ) {
this.innerValue = value;
this.propagateChange(value);
this.propagateTouch(true);
}
}
/**
* value accessor implementation
* #param value
*/
public writeValue(value: any): void {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
/**
* validation
* #param c
*/
public validate(c: FormControl) {
return this.errors = (this.disabled) ? null : this.customValidate(c);
}
private customValidate(c: FormControl): {} {
if ( c.touched ) {
// some validation logic which is not relevant here;
return null;
}
return null;
}
}
There are other components used in the form, too (like a color picker and a ng-select).
So the weird part is this. The form value is set alright. No errors. The values are displayed correctly (for both, disabled and !disabled) within data-text-input components, as well as other components in the form). The weird part is that when I inspect the this.profileForm object with the debugger, the controls property has all of the controls with their respective values, but the value property of the form misses those, where disabled property (aka no input field) is set to true.
Here is the Plunker: https://plnkr.co/edit/nbWQZzQjhGae622CanGa?p=preview
Any ideas?
Well, this was not obvious, until I have traced down the way of setting of a value down to AbstractControl.prototype.updateValueAndValidity and it turned out, that using the variable name disabled was a bad idea here:
<form #profileForm="ngForm">
<data-text-input
label="lastName"
[required]="true"
[disabled]="userIsRegistered"
name="lastName"
ngModel></data-text-input>
</form>
I have renamed the disabled property to isReadOnly – 'cause readonly is also an attribute which might be checked elsewhere and also a TypeScript interface – and, tada, it works.

Shared Service and asynchronous data in Angular 2

I have a problem with getting data via Shared Service
I have a Shared Service
#Injectable()
export class SharedService {
public title;
constructor() {
this.title = "";
}
public setData(val: string): void {
this.title = val;
}
public getUrlHistoryObj(): string {
return this.title;
}
}
A component FillComponent in which I get data from DataService (it works, it gets data and it actually sets data, tested it with console.log)
export class FillComponent implements OnInit {
#Input() title: any;
constructor(public info: InfoComponent, public shared: SharedService) {
}
ngOnInit() {
this.shared.setData(this.title); }
}
I get data from PartComponent
export class Part2Component implements OnInit {
#ViewChild(FillInBlankComponent) private fill: FillComponent;
#ViewChild(InfoComponent) private info: InfoComponent;
public title: string;
constructor(public dataService: DataService, public shared: SharedService) {
this.dataService.get().subscribe(data => {
const d = this.dataService.convert(data, 2);
this.title = d[0];
});
}
Till now everything works fine.
But the problem is here, in InfoComponent, when I try to get data it gives me empty result.
#Injectable()
export class InfoComponent implements OnChanges, OnInit, OnDestroy {
public title: string;
constructor(public shared: SharedService) {
this.title = this.shared.getUrlHistoryObj();
}
ngOnInit() {
console.log('i am title from info and i am boos')
console.log(this.title)
}
}
I guess the problem is in asynchronous loading. How could I fix it?
On info component, move this line outside the constructor, to the ngOnInit:
this.title = this.shared.getUrlHistoryObj();

How can I execute action after data-bind in Angular 2?

I'm developing an Angular 2 SPA. My application is composed by:
One component
One directive
I've builded one directive that format text input using onfocus and onblur events. On focus event remove dots to text value, on blur event add thousand dots to text value.
Following component's code:
<div>
<input id="input" [(ngModel)]="numero" InputNumber />
</div>
Following component's TypeScript code:
import { Component } from '#angular/core';
#Component({
selector: 'counter',
templateUrl: './counter.component.html'
})
export class CounterComponent {
numero: number;
public incrementCounter() {
}
ngOnInit() {
this.numero = 100100100;
}
}
Following directive's TypeScript code:
import { Directive, HostListener, ElementRef, OnInit } from "#angular/core";
#Directive({ selector: "[InputNumber]" })
export class InputNumber implements OnInit, OnChanges {
private el: HTMLInputElement;
constructor(private elementRef: ElementRef) {
this.el = this.elementRef.nativeElement;
}
ngOnInit(): void {
// this.el.value is empty
console.log("Init " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
ngOnChanges(changes: any): void {
// OnChanging value this code is not executed...
console.log("Change " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
#HostListener("focus", ["$event.target.value"])
onFocus(value: string) {
this.el.value = this.replaceAll(value, ".", "");
}
#HostListener("blur", ["$event.target.value"])
onBlur(value: string) {
this.el.value = this.numberWithCommas(value);
}
private numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
private escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
private replaceAll(str, find, replace) {
return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
}
}
The following code works except that I need lost focus for show my number like "100.100.100". How can I perform this action on init data loading?
I add one example at this link: Plnkr example
Thanks
You can do this by using a Pipe which takes a boolean parameter that represents your focus/no focus action.
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({name: 'dots'})
export class DotsPipe implements PipeTransform {
transform(value: number, hasFocus:boolean): any {
if(hasFocus){
return value.toString().replace(/\./g,'');
}else{
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
}
}
Then you have to apply the Pipe on your [ngModel] and use Angular events (focus) and (focusout) to change your variable.
<input [ngModel]="numero | dots : hasFocus" (focus)="hasFocus=true" (focusout)="hasFocus=false" (ngModelChange)="numero=$event" />
I think that your directive should implement ControlValueAccessor interface https://angular.io/docs/ts/latest/api/forms/index/ControlValueAccessor-interface.html
It is needed for writing model in your directive. ControlValueAccessor interface has writeValue(value: any) method that will be initially called.
So your writeValue method will be something like this:
private onChange: (_: any) => {};
...
writeValue(val) {
const editedValue = this.numberWithCommas(val);
this._onChange(val);
}
registerOnChange(fn: any) : void {
this._onChange = fn;
}

Categories

Resources