Pass required to custom component Angular - javascript

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

Related

Cannot Post in Angular 13

I'm making my first steps into Angular, so I decided to start with a CRUD.
I've been doing this following a guide, but I got crashed into a concrete wall, as I've checked everything and couldn't find a solution.
I'm trying to create a Project (model), which has 3 columns:
Title
Description (Nullable)
AccessCode (Nullable)
I need to make a POST request to add the data into my database (Backed by Node.js and working fine from Postman)
This is the code I have:
add-project-component.ts
import {
Component,
OnInit
} from '#angular/core';
import {
Project
} from 'src/app/models/project.model';
import {
ProjectService
} from 'src/app/services/project.service';
#Component({
selector: 'app-add-project',
templateUrl: './add-project.component.html',
styleUrls: ['./add-project.component.css']
})
export class AddProjectComponent implements OnInit {
project: Project = {
title: '',
description: '',
accessCode: '',
};
submitted = false;
constructor(private projectService: ProjectService) {}
ngOnInit(): void {}
saveProject(): void {
const data = {
title: this.project.title,
description: this.project.description,
accessCode: this.project.accessCode
};
this.projectService.create(data)
.subscribe({
next: (res) => {
console.log(res);
this.submitted = true;
},
error: (e) => console.error(e)
});
}
newProject(): void {
this.submitted = false;
this.project = {
title: '',
description: '',
accessCode: ''
};
}
}
add-project-component.html
<div class="new-project">
<mat-toolbar>
<span>New Project</span>
</mat-toolbar>
<mat-card>
<mat-card-content *ngIf="!submitted">
<p>
<mat-form-field appearance="outline">
<mat-label>Title</mat-label>
<input id="title" required [(ngModel)]="project.title" matInput name="title" placeholder="Title">
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<textarea id="description" rows="6" [(ngModel)]="project.description" name="description" matInput placeholder="Description"></textarea>
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label>Access Code</mat-label>
<input id="accessCode" [(ngModel)]="project.accessCode" name="accessCode" matInput placeholder="Access Code">
</mat-form-field>
</p>
<!-- FORM CONTENT -->
</mat-card-content>
<mat-card-actions *ngIf="!submitted">
<button mat-raised-button color="primary" (click)="newProject()">Create Project</button>
<!-- REGISTER BUTTON -->
</mat-card-actions>
</mat-card>
</div>
services/project.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { Project } from '../models/project.model';
const baseUrl = 'http://localhost:8080/api/projects';
#Injectable({
providedIn: 'root'
})
export class ProjectService {
constructor(private http: HttpClient) { }
getAll(): Observable<Project[]> {
return this.http.get<Project[]>(baseUrl);
}
get(id: any): Observable<Project> {
return this.http.get(`${baseUrl}/${id}`);
}
create(data: any): Observable<any> {
return this.http.post(baseUrl, data);
}
update(id: any, data: any): Observable<any> {
return this.http.put(`${baseUrl}/${id}`, data);
}
delete(id: any): Observable<any> {
return this.http.delete(`${baseUrl}/${id}`);
}
deleteAll(): Observable<any> {
return this.http.delete(baseUrl);
}
findByTitle(title: any): Observable<Project[]> {
return this.http.get<Project[]>(`${baseUrl}?title=${title}`);
}
}
Error 404
Can you guys help me out? What am I missing?
Thank you very much.
Your create function points to the wrong URL.
Your desired URL is /api/projects/create, but you are missing the /create part.
Change
create(data: any): Observable<any> {
return this.http.post(baseUrl, data);
}
to
create(data: any): Observable<any> {
return this.http.post(`${baseUrl}/create`, data);
}
given that const baseUrl = 'http://localhost:8080/api/projects'; is correct.
Note: For production you most likely want to change baseUrl to
const baseUrl = '/api/projects'; as the host will change (localhost vs your actual domain).

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

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