validation not propagated with custom input component - Angular 4 - javascript

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

Related

Angular click event not fired when custom input is focused

I created simple custom input eu-input element using 'ControlValueAccessor'. Now I have something like that:
<eu-input></eu-input>
<div *ngFor="let item of items" (click)="choose(item)">
<span>{{item.name}}</span>
</div>
But there is problem I can't figure out. When eu-input is focused, click event doesn't get fired, it's fired only after eu-input is blured (on second attempt).
so what could be a problem?
this is html:
<input [(ngModel)]="value"/>local: {{val}}
and this is ts file:
import { Component, OnInit, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'eu-input',
templateUrl: './eu-input.component.html',
styleUrls: ['./eu-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EuInputComponent),
multi: true,
},
],
})
export class EuInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
ngOnInit(): void {}
onChange: any = () => {};
onTouch: any = () => {};
val = '';
set value(val) {
if (val !== undefined && this.val !== val) {
this.val = val;
this.onChange(val);
this.onTouch(val);
}
}
writeValue(value: any) {
this.value = value;
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouch = fn;
}
}
choose() method is just console.log('choose clicked')
Change eu-input component html to
<input [(ngModel)]="value" (focus)="emitFocused(true)" (blur)="emitFocused(false)"/>local: {{val}}
In eu-input TypeScript
#Output() focus = new EventEmitter();
public emitFocused(value) {
this.focus.emit(value);
}
Can be used like
<eu-input (focus)="myMethod($event)"></eu-input>

Angular: custom input with ControlValueAccessor

I'm not sure how can I use a custom component if it's wrapper under another component.
Like:
ComponentA_withForm
|
--ComponentA1_withWrapperOfCustomInput
|
--ComponentA11_withCustomInput
if I have a structure like this:
ComponentA_withForm
|
--ComponentA11_withCustomInput
Everything's fine
But for my case (tons of async data) I need a wrapper... Is it possible somehow to do this?
Here is my fiddle code:
ComponentA:
import { Component } from '#angular/core';
import { FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
ComponentA1:
import { Component } from '#angular/core';
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input></custom-input>',
})
export class CustomInputWrapperComponent {
constructor() { }
}
ComponentA11:
import { Component, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `Hey there! <button (click)="inc()">Value: {{ value }}</button>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
}],
})
export class CustomInputComponent implements ControlValueAccessor {
private value = 0;
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
inc() {
this.value = this.value + 1;
this.onChangeFn(this.value);
}
onChangeFn = (_: any) => { };
}
And here I have a working sample:
https://stackblitz.com/edit/angular-qmrj3a
so: basically removing & refactoring code not to use CustomInputWrapperComponent makes my code working. But I need this wrapper and I'm not sure how to pass formControlName then.
I don't want a dirty solution with passing parent formGroup :)
Since you don't want a dirty solution ;) , you could just implement ControlValueAccessor in the CustomInputWrapperComponent also. That way any change in the parent will be reflected in the child, any change in the child will be reflected in the parent as well with just few lines of code.
Wrapper Component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="value"></custom-input>',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputWrapperComponent),
multi: true,
}]
})
export class CustomInputWrapperComponent implements AfterViewInit, ControlValueAccessor {
public value = new FormControl();
constructor() { }
ngAfterViewInit() {
this.value.valueChanges.subscribe((x) => {
this.onChangeFn(x);
});
}
writeValue(value: number): void {
this.value.setValue(value);
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
onChangeFn = (_: any) => { };
}
Parent Template
<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>
I have made a stackbitz demo here - https://stackblitz.com/edit/angular-csaxcz
you cannot use formControlName on custom-input-wrapper because it doesn't implement ControlValueAccessor. implementing ControlValueAccessor on custom-input-wrapper might be a solution but it seems to be overkill. Instead pass the control from formGroup to custom-input-wrapper as an #Input() and pass the inputed formControl to custom-input
app.component
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper [formCtrl]="form.get('someInput')"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
custom-input-wrapper.component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="formCtrl"></custom-input>',
})
export class CustomInputWrapperComponent {
#Input() formCtrl: AbstractControl;
constructor() { }
}
here is a working demo https://stackblitz.com/edit/angular-3lrfqv

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" />?

RadioButtons acting strange Angular 4

I'm trying to create a custom component that groups some Radio Button this way
<group>
<radio></radio>
<radio></radio>
<radio></radio>
</group
Inside of the component I add dynamically a name for the inputs, so they all have the same name and change the selected one when I click on another.
It works good if I only have one component, if i have more than one, it extends the values like if it were only a group of RadioButtons with only one name.
This is the code I'm using:
import {AfterViewInit, Component, ContentChildren, ElementRef, Input, NgModule, QueryList} from "#angular/core";
import {CommonModule} from "#angular/common";
import {ControlValueComponent} from "../shared/ControlValueComponent";
import {SysSharedModule} from "../shared/SysSharedModule";
#Component({
selector: 'sys-radio-button',
styleUrls: ['sysRadioButton.css', '../shared/sys.css'],
providers: ControlValueComponent.providerValueAcessor(SysRadioButton),
template: `
<input type="radio" id="rb{{randomId}}" [value]="val" [(ngModel)]="value">
<label for="rb{{randomId}}">{{label}}</label>
`
})
export class SysRadioButton extends ControlValueComponent {
constructor (public elem: ElementRef) {
super();
}
#Input() groupName = 'radiobutton';
#Input() val: any;
#Input() label: string;
randomId = (Math.floor(Math.random() * (1 - 10000 + 1)) + 1) * -1;
}
#Component({
selector: 'sys-radio-group',
styleUrls: ['sysRadioButton.css', '../shared/sys.css'],
providers: ControlValueComponent.providerValueAcessor(SysRadioGroup),
template: `
<div class="t{{tam}}">
<label class="header">{{header}}</label>
<div class="radioButtonContainer"></div>
</div>
`
})
export class SysRadioGroup extends ControlValueComponent implements AfterViewInit {
#Input() name: string;
#Input() header: string;
#Input() tam = '3-of-10';
#ContentChildren(SysRadioButton) radioButtons: QueryList<SysRadioButton>;
constructor (public elem: ElementRef) {
super();
}
ngAfterViewInit() {
this.addNameToInputs();
}
addNameToInputs() {
const container = this.elem.nativeElement.getElementsByClassName('radioButtonContainer')[0];
this.radioButtons.forEach(item => {
const input = item.elem.nativeElement;
input.getElementsByTagName('input')[0].name = this.name;
container.appendChild(input);
});
}
}
#NgModule({
imports: [CommonModule, SysSharedModule],
declarations: [SysRadioButton, SysRadioGroup],
exports: [SysRadioButton, SysRadioGroup]
})
export class SysRadioButtonModule {
}
And i use it like this:
<sys-radio-group header="Select your destiny" name="name1">
<sys-radio-button val="hola1" label="Label 1"></sys-radio-button>
<sys-radio-button val="hola2" label="Label 2"></sys-radio-button>
<sys-radio-button val="hola3" label="Label 3"></sys-radio-button>
<sys-radio-button val="hola4" label="Label 4"></sys-radio-button>
</sys-radio-group>
<sys-radio-group header="Select your destiny" name="name2">
<sys-radio-button val="hola1" label="Label 1"></sys-radio-button>
<sys-radio-button val="hola2" label="Label 2"></sys-radio-button>
<sys-radio-button val="hola3" label="Label 3"></sys-radio-button>
<sys-radio-button val="hola4" label="Label 4"></sys-radio-button>
</sys-radio-group>
Here are some images of how it works
This is how it is when i don't click on anything
and this is how it looks when i click on one with the same value but a different name
If i check the elements in the chrome's console, i can see how the name's are different, so i don't understand why this is happening
EDIT
The ControlValueComponent class that extends the main classes,is just the one for the custom form. This is the code:
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "#angular/forms";
import {forwardRef, Input} from "#angular/core";
export class ControlValueComponent implements ControlValueAccessor {
#Input() disabled: boolean;
innerValue: any = '';
static providerValueAcessor( ref: any): any {
return [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ref), multi: true }
];
}
onTouchedCallback: () => void = () => {};
onChangeCallback: (_: any) => void = () => {};
constructor() {
}
get value(): any {
return this.innerValue;
}
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any): void {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedCallback = fn;
}
}
So here is where the "value" variable of the [(ngModel)] comes from
change ngModel to :
[(ngModel)]="val"

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.

Categories

Resources