Custom ControlValueAccessor in template-driven forms - javascript

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.

Related

disable in Angular Material custom field component not working

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.

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.

Pass required to custom component Angular

I have a custom component to use for phone numbers
I need to use the required flag for it
Here is HTML of component
<form #phoneForm="ngForm" novalidate name="PhoneForm">
<div class="form-row">
<div class="form-group col-md-3">
<p-dropdown
#phoneCodeInput = ngModel
[disabled]="!countrycodes.length"
[options]="countrycodes"
autoWidth="false"
[(ngModel)]="phoneCode"
(ngModelChange)="onNumberChange()"
[style]="{ width: '100%', height: '100%'}"
name="countryCodes"
[autoWidth]="true"
></p-dropdown>
</div>
<div class="form-group col-md-9">
<input
[readonly] = "isReadOnly"
#phoneNumberInput = ngModel
number-directive
class="form-control"
placeholder="Enter phone number"
[required] = "isFieldRequired"
[(ngModel)]="phoneNumber"
(ngModelChange)="onNumberChange()"
class="form-control"
type="text"
name="name"
maxlength="11"
/>
</div>
</div>
<validation-messages [formCtrl]="phoneNumberInput"></validation-messages>
</form>
Here is a typescript code of the component, where I use the Input parameter to make validation
import { AppComponentBase } from '#shared/common/app-component-base';
import {
Component,
OnInit,
Injector,
AfterContentChecked,
ViewChild,
forwardRef,
Input,
} from '#angular/core';
import * as lookup from 'country-telephone-data';
import { SelectItem } from 'primeng/api';
import { ControlValueAccessor, ValidationErrors, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '#angular/forms';
#Component({
selector: 'phone-number',
templateUrl: './phone-number.component.html',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: PhoneNumberComponent, multi: true },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PhoneNumberComponent),
multi: true
}
]
})
export class PhoneNumberComponent extends AppComponentBase
implements OnInit, ControlValueAccessor, AfterContentChecked {
#Input() isRequired: boolean;
#ViewChild('phoneForm') phoneForm;
constructor(injector: Injector) {
super(injector);
}
countrycodes: SelectItem[] = [];
phoneCode: string;
phoneNumber: string;
required: string | boolean;
isFieldRequired: boolean = false;
isReadOnly: boolean = false;
private changed = [];
private touched = [];
disabled: boolean;
ngAfterContentChecked(): void {
this.checkValidity();
}
checkValidity(): void {}
propagateChange = (_: any) => {};
get phoneNumberResult(): string {
const result = `${this.phoneCode ? this.phoneCode : ''} ${
this.phoneNumber ? this.phoneNumber : ''
}`;
return result;
}
set phoneNumberResult(value: string) {
if (this.phoneNumberResult !== value) {
const [phoneCode, phoneNumber] = value.split(' ');
this.phoneCode = phoneCode;
this.phoneNumber = phoneNumber;
this.changed.forEach(f => f(value));
}
}
writeValue(obj: string): void {
this.phoneNumberResult = obj ? obj : '+44';
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.touched.push(fn);
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
ngOnInit(): void {
if (this.isRequired === true) {
this.isFieldRequired = true;
}
lookup.allCountries.forEach(element => {
this.countrycodes.push({
label: `+${element.dialCode}`,
value: `+${element.dialCode}`,
});
});
}
onNumberChange(): void {
this.propagateChange(this.phoneNumberResult);
}
validate(): ValidationErrors {
if (!this.phoneForm.valid) {
return { message: 'custom error' };
}
return null;
}
registerOnValidatorChange(fn: () => void): void {
this.checkValidity = fn;
}
}
Now I use input parameters to implement the required functionality
here is how I use my component now
<phone-number [isRequired] =" isMobileNumberRequired" id="" #mobileEdit name="mobile" [(ngModel)]="tenant.mobileNumber" (ngModelChange)="onMobileChanged()"></phone-number>
I need to use just required flag at component call instead of passing parameters. How I can do it?
you can use <mat-form-field> component. then you can control required and also error message
<mat-form-field>
<input matInput placeholder="Enter Phone Number" [formControl]="phoneNumber" required>
<mat-error *ngIf="phoneNumber.invalid">{{getErrorMessage()}}</mat-error>
</mat-form-field>
for better understand you can follow this link and for example.
Maybe ngRequired and <input ng-model="required" id="required" />?

Angular 4: reactive form control is stuck in pending state with a custom async validator

I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components. I am trying to implement this validation as a custom async validator that I can use with reactive forms. Currently, I can get the API response, but the control status is stuck in pending state. I get no errors so I am a bit confused. Please tell me what I am doing wrong. Here is my code.
Component
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}
Validator class
import { AbstractControl } from '#angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
export class CustomValidators {
static briteVerifyValidator(service: EmailValidationService) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return Observable.of(null);
} else {
return control.valueChanges
.debounceTime(1000)
.distinctUntilChanged()
.switchMap(value => service.validateEmail(value))
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
});
}
}
}
}
Service
import { Injectable } from '#angular/core';
import { HttpClient,
HttpParams } from '#angular/common/http';
interface EmailValidationResponse {
address: string,
account: string,
domain: string,
status: string,
connected: string,
disposable: boolean,
role_address: boolean,
error_code?: string,
error?: string,
duration: number
}
#Injectable()
export class EmailValidationService {
public emailValidationUrl = 'https://briteverifyendpoint.com';
constructor(
private http: HttpClient
) { }
validateEmail(value) {
let params = new HttpParams();
params = params.append('address', value);
return this.http.get<EmailValidationResponse>(this.emailValidationUrl, {
params: params
});
}
}
Template (just form)
<form class="email-form" [formGroup]="emailForm" (ngSubmit)="sendEmail()">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<fieldset class="form-group required" [ngClass]="{ 'has-error': email.invalid && formSubmitted }">
<div>{{ email.status }}</div>
<label class="control-label" for="email">Email</label>
<input class="form-control input-lg" name="email" id="email" formControlName="email">
<ng-container *ngIf="email.invalid && formSubmitted">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter valid email address.
</ng-container>
</fieldset>
<button type="submit" class="btn btn-primary btn-lg btn-block">Send</button>
</div>
</div>
</form>
There's a gotcha!
That is, your observable never completes...
This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.
You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.
So you can use first()
UPDATE TO RXJS 6:
briteVerifyValidator(service: Service) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return of(null);
} else {
return control.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(),
switchMap(value => service.getData(value)),
map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
).pipe(first())
}
}
}
A slightly modified validator, i.e always returns error: STACKBLITZ
OLD:
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
.first();
A slightly modified validator, i.e always returns error: STACKBLITZ
So what I did was to throw a 404 when the username was not taken and use the subscribe error path to resolve for null, and when I did get a response I resolved with an error. Another way would be to return a data property either filled width the username or empty
through the response object and use that insead of the 404
Ex.
In this example I bind (this) to be able to use my service inside the validator function
An extract of my component class ngOnInit()
//signup.component.ts
constructor(
private authService: AuthServic //this will be included with bind(this)
) {
ngOnInit() {
this.user = new FormGroup(
{
email: new FormControl("", Validators.required),
username: new FormControl(
"",
Validators.required,
CustomUserValidators.usernameUniqueValidator.bind(this) //the whole class
),
password: new FormControl("", Validators.required),
},
{ updateOn: "blur" });
}
An extract from my validator class
//user.validator.ts
...
static async usernameUniqueValidator(
control: FormControl
): Promise<ValidationErrors | null> {
let controlBind = this as any;
let authService = controlBind.authService as AuthService;
//I just added types to be able to get my functions as I type
return new Promise(resolve => {
if (control.value == "") {
resolve(null);
} else {
authService.checkUsername(control.value).subscribe(
() => {
resolve({
usernameExists: {
valid: false
}
});
},
() => {
resolve(null);
}
);
}
});
...
I've been doing it slightly differently and faced the same issue.
Here is my code and the fix in case if someone would need it:
forbiddenNames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value.toUpperCase() === 'TEST') {
resolve({'nameIsForbidden': true});
} else {
return null;//HERE YOU SHOULD RETURN resolve(null) instead of just null
}
}, 1);
});
return promise;
}
I tries using the .first(). technique described by #AT82 but I didn't find it solved the problem.
What I eventually discovered was that the form status was changing but it because I'm using onPush, the status change wasn't triggering change detection so nothing was updating in the page.
The solution I ended up going with was:
export class EmailFormComponent implements OnInit {
...
constructor(
...
private changeDetector: ChangeDetectorRef,
) {
...
// Subscribe to status changes on the form
// and use the statusChange to trigger changeDetection
this.myForm.statusChanges.pipe(
distinctUntilChanged()
).subscribe(() => this.changeDetector.markForCheck())
}
}
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}

validation not propagated with custom input component - Angular 4

I have a custom text-area component, with text-area input inside. I have created a custom validator to check the max length (not the html one) of the text.
All work fine, the problem is that the inner input is set to invalid (with ng-invalid) while che component itself don't and so also the form that contains the component remains valid.
It's seems to work with the built-it required validator, placed on both the component and the input.
How can I make the changes in a custom input to be reflected on the external form?
Thanks!
//sorry for my english!
Edit: I made a plunker: https://plnkr.co/edit/NHc25bo8K9OsgcxSWyds?p=preview
This is the custom text-area component html:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
<span *ngIf="input.errors && (input.dirty || input.touched)">
<span *ngIf="input.errors?.required" class="error-message">Required!</span>
<span *ngIf="input.errors?.customMaxLength" class="error-message">Max Length Reached({{maxLength}})</span>
</span>
This is the code of the component
import { Component, Input, forwardRef, ViewChild } from '#angular/core';
import { NgModel, ControlValueAccessor, NG_VALUE_ACCESSOR, AbstractControl } from '#angular/forms';
#Component({
selector: 'custom-text-area',
templateUrl: './custom-text-area.component.html',
styleUrls: ['./custom-text-area.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextAreaComponent),
multi: true
}
]
})
export class TextAreaComponent implements ControlValueAccessor{
#Input() required = false;
#Input() name;
#Input() data;
#Input() disabled;
#Input() placeholder = '';
#Input() errorMsg;
#Input() maxLength = null;
#ViewChild('input') input: NgModel;
constructor() {
this.data = '';
}
propagateChange = (_: any) => {};
writeValue(value: any): void {
if (value !== undefined) {
this.data = value;
} else {
this.data = '';
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInput(e) {
this.data = e.target.value || '';
this.propagateChange(this.data);
}
}
This is the validator
import { NG_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { Directive, forwardRef, Input } from '#angular/core';
#Directive({
selector: '[customMaxLength][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => MaxLengthValidatorDirective), multi: true}
]
})
export class MaxLengthValidatorDirective implements Validator{
#Input() customMaxLength: number;
ngOnInit(){
}
validate(c: FormControl): { [key: string]: any; } {
if(c.value && this.customMaxLength){
return c.value.length < this.customMaxLength ? null : { customMaxLength:{ valid: false } };
} else {
return null;
}
}
}
Aaand finally this is an use:
<form #form="ngForm">
<custom-text-area [maxLength]="3" required ngModel name="textArea"></custom-text-area>
</form>
The main problem is how you are using the NgModel. You are using it in both the custom component and inside your form. You should only be using it inside of your form. Meaning, textarea should not have an NgModel.
No:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
Yes:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[name]="name"
customMaxLength="{{maxLength}}"
></textarea>
Here is a working example:
https://plnkr.co/edit/lWZpEpPdnfG7YDiH21jB?p=preview

Categories

Resources