Angular 5, Angular Material: Datepicker validation not working - javascript

I'm using the latest Angular and latest Angular Material. I've got a datepicker and I want to add some validation. Documents say that the required attribute should work out of the box, but it doesn't seem to handle errors in the same way that other form elements do.
Here is my mark-up:
<mat-form-field class="full-width">
<input matInput [matDatepicker]="dob" placeholder="Date of birth" [(ngModel)]="myService.request.dob" #dob="ngModel" required app-validateAdult>
<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
<mat-datepicker #dob></mat-datepicker>
<mat-error *ngIf="dob.errors && dob.errors.required">Your date of birth is required</mat-error>
</mat-form-field>
This works on the happy-path, so when a date is picked, the date ends up in the expected property in myService.
The validation does not work in the way that I would expect however; in this case, if I click into the field and then out of the field without entering a date, then the input does get red styling, but the usual [controlName].errors object does not get populated. This means that showing an error message in the usual way (the way that works with other inputs that are not date pickers on the same page) does not work:
<mat-error *ngIf="dob.errors && dob.errors.required">Your date of birth is required</mat-error>
The *ngIf is never true because the datepicker never seems to update dob.errors, so the error message is never shown, even when the input is styled as invalid.
Is this right? Have I missed something?
I've also tried adding a custom directive to validate that the date selected with the datepicker indicates that the user is over 18:
export class AdultValidator implements Validator {
constructor(
#Attribute('app-validateAdult') public validateAdult: string
) { }
validate(control: AbstractControl): { [key: string]: any } {
const dob = control.value;
const today = moment().startOf('day');
const delta = today.diff(dob, 'years', false);
if (delta <= 18) {
return {
validateAdult: {
'requiredAge': '18+',
'currentAge': delta
}
};
}
return null;
}
}
In this case I'm trying to use a similar matError (except linked to dob.errors.validateAdult instead) to show the error when appropriate.
The interesting thing with this is that if I pick a date less than 18 years ago, the whole input, label, etc, gets the default red error styling, so something is happening, but I still don't see my error message.
Any suggestions would be much appreciated!
Exact versions:
Angular CLI: 1.6.3
Node: 6.11.0
OS: win32 x64
Angular: 5.1.3
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router
#angular/cdk: 5.0.4
#angular/cli: 1.6.3
#angular/flex-layout: 2.0.0-beta.12
#angular/material-moment-adapter: 5.0.4
#angular/material: 5.0.4
#angular-devkit/build-optimizer: 0.0.36
#angular-devkit/core: 0.0.22
#angular-devkit/schematics: 0.0.42
#ngtools/json-schema: 1.1.0
#ngtools/webpack: 1.9.3
#schematics/angular: 0.1.11
#schematics/schematics: 0.0.11
typescript: 2.4.2
webpack: 3.10.0

I use ErrorStateMatcher in my Angular Material Forms, it works perfectly.
You should have a code that looks like that:
<mat-form-field class="full-width">
<input matInput [matDatepicker]="dob" placeholder="Date of birth" formControlName="dob" required app-validateAdult>
<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
<mat-datepicker #dob></mat-datepicker>
<mat-error *ngIf="dob.hasError('required')">Your date of birth is required</mat-error>
</mat-form-field>
And typescript:
import { ErrorStateMatcher } from '#angular/material/core';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(
control: FormControl | null,
form: FormGroupDirective | NgForm | null
): boolean {
const isSubmitted = form && form.submitted;
return !!(
control &&
control.invalid &&
(control.dirty || control.touched || isSubmitted)
);
}
}
export class AdultValidator implements Validator {
dob = new FormControl('', [
Validators.required
]);
matcher = new MyErrorStateMatcher();
}
You can see here more about it: https://material.angular.io/components/input/overview

I managed to get this working without using the ErrorStateMatcher, although that did help me reach the solution. Leaving here for future reference or to help others.
I converted my form to a reactive form instead of a template-driven form, and I changed the custom validator directive to a simpler validator (non-directive-based).
Here is the working code:
my-form.component.html:
<div class="container" fxlayoutgap="16px" fxlayout fxlayout.xs="column" fxlayout.sm="column" *ngIf="fieldset.controls[control].type === 'datepicker'">
<mat-form-field class="full-width" fxflex>
<input matInput
[formControlName]="control"
[matDatepicker]="dob"
[placeholder]="fieldset.controls[control].label"
[max]="fieldset.controls[control].validation.max">
<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
<mat-datepicker #dob></mat-datepicker>
<mat-error *ngIf="myForm.get(control).hasError('required')">
{{fieldset.controls[control].validationMessages.required}}</mat-error>
<mat-error *ngIf="myForm.get(control).hasError('underEighteen')">
{{fieldset.controls[control].validationMessages.underEighteen}}
</mat-error>
</mat-form-field>
</div>
note: The above code is inside a couple of nested ngFor loops which define the value of fieldset and control. In this example control maps to the string dob.
over-eighteen.validator.ts:
import { ValidatorFn, AbstractControl } from '#angular/forms';
import * as moment from 'moment';
export function overEighteen(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
const dob = control.value;
const today = moment().startOf('day');
const delta = today.diff(dob, 'years', false);
if (delta <= 18) {
return {
underEighteen: {
'requiredAge': '18+',
'currentAge': delta
}
};
}
return null;
};
}
my-form.component.ts:
buildForm(): void {
const formObject = {};
this.myService.request.fieldsets.forEach((controlsGroup, index) => {
this.fieldsets.push({
controlNames: Object.keys(controlsGroup.controls)
});
for (const control in controlsGroup.controls) {
if (controlsGroup.controls.hasOwnProperty(control)) {
const controlData = controlsGroup.controls[control];
const controlAttributes = [controlData.value];
const validators = [];
if (controlData.validation) {
for (const validator in controlData.validation) {
if (controlData.validation.hasOwnProperty(validator)) {
if (validator === 'overEighteenValidator') {
validators.push(this.overEighteenValidator);
} else {
validators.push(Validators[validator]);
}
}
}
controlAttributes.push(Validators.compose(validators));
}
formObject[control] = controlAttributes;
}
}
});
this.myForm = this.fb.group(formObject);
}

You have the #dob duplicated. That can have a undesired behavior in angular validation.
You have
<input #dob='ngModel'
and
<mat-datepicker #dob></mat-datepicker>
Please fix the naming convention and see what happen.

Add this in your view page:
<mat-form-field>
<input matInput [matDatepicker]="dp" placeholder="Employement date" >
<mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle>
<mat-datepicker #dp></mat-datepicker>
</mat-form-field>
Just import MatDatepickerModule,MatNativeDateModule in your module

You can change name of the input reference like below..
Notice that input element #dobInput is referenced only in mat-error.
<mat-form-field class="full-width">
<input matInput [matDatepicker]="dob" placeholder="Date of birth" [(ngModel)]="myService.request.dob" #dobInput="ngModel" required app-validateAdult>
<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>
<mat-datepicker #dob></mat-datepicker>
<mat-error *ngIf="dobInput.errors && dobInput.errors.required">Your date of birth is required</mat-error>
The picker is referenced by #dbo
[matDatepicker]="dob"
<mat-datepicker-toggle matSuffix [for]="dob"></mat-datepicker-toggle>

Did you try to set the *ngIf to your custom validator like below:
<mat-error *ngIf="dob.errors && dob.errors.validateAdult">Your date of birth
is less than 18 ?</mat-error>
If that works, you can create another validator to simulate the required native validation behavior.
export class CustomRequireValidator implements Validator {
constructor(
#Attribute('app-validateRequired') public validateRequired: string
) { }
validate(control: AbstractControl): { [key: string]: any } {
let value = control.value;
if (!value || value == null || value.toString().length == 0) {
return requireValidator: {
'requiredAge': 'This field is required',
}
}
return null;
}
}
And then use the previous ngIf as below:
<mat-error *ngIf="dob.errors && dob.errors.requireValidator">Your date of
birth is less than 18 ?</mat-error>
I did not test it but logically it should work.

Take a look on this, and this. And please see a working javascript based example here.
You may also test this (angular based) datepicker:
HTML:
<div ng-app="example" ng-controller="AppCtrl">
<md-datepicker ng-model="myDate" md-placeholder="Enter date"></md-datepicker>
</div>
JS:
angular
.module('example', ['ngMaterial'])
.config(function($mdDateLocaleProvider) {
$mdDateLocaleProvider.formatDate = function(date) {
return moment(date).format('DD/MM/YYYY');
};
$mdDateLocaleProvider.parseDate = function(dateString) {
var m = moment(dateString, 'DD/MM/YYYY', true);
return m.isValid() ? m.toDate() : new Date(NaN);
};
})
.controller('AppCtrl', function($scope) {
$scope.myDate = new Date();
});

Related

Angular forms require one of two form groups to be valid

I am trying to implement a reactive angular form where either A or B has to be entered. A is a unique id and B is a set of values which identify the id. Now I try to validate a Form that is valid if either A is entered or B is entered including all the required values. I found several solutions that implement this behavior based on FormFields but was not able to get it working with the group of values.
<form class="container" [formGroup]="myForm" (ngSubmit)="onSubmit()">
<mat-form-field class="w-1/2">
<mat-label>ID</mat-label>
<input matInput type="number" formControlName="id">
</mat-form-field>
<div class="grid grid-cols-3 gap-4" formGroupName="innerGroup">
<mat-form-field>
<mat-label>First Name</mat-label>
<input matInput type="number" formControlName="firstName">
</mat-form-field>
<mat-form-field>
<mat-label>Last Name</mat-label>
<input matInput type="number" formControlName="lastName">
</mat-form-field>
</div>
</form>
My first idea was to override the default validator for the form but I could not figure out how to do that. Not even sure if it would be possible. I was trying to adjust https://stackoverflow.com/a/48714721 to work in my scenario but I had no idea how to get it to work because of the additional complexity with the inner form group.
Using angular 14 I was able to produce a similar result to what you are describing I am not sure it will 100% solve your issue however it might help.
Basically what I did was create a validator function that is to be applied at the group level. This validator will check the valid state of any given controls be they a FormGroup or a FormControl. However, this alone will not solve the problem as if you have a form group angular will see that any underling control or group that is invalid will also invalidate the parent. So what I did was call .disable() on any control that was being checked by the validator function. This will disable the UI element and disable validation checking by angular allowing the parent to be considered valid when one of the children is valid but the other is invalid effectively creating a one and only one validator.
My specific example I was trying to get the OnlyOneValidator to work for a MatStepper.
Validator
export function onlyOneValidator(controlKeys: string[]) {
return (control: AbstractControl): ValidationErrors | null => {
let countOfValidControls = 0;
for (let key of controlKeys) {
const controlToCheck = control.get(key);
if (controlToCheck === null || controlToCheck === undefined) {
throw new Error(`Error: Invalid control key specified key was ${key}`);
}
countOfValidControls += controlToCheck?.valid ? 1 : 0;
}
if (countOfValidControls !== 1) {
// the count is not exactly one
return {
onlyOneValid: {
actualValidCount: countOfValidControls,
expectedValidCount: 1
}
};
}
return null;
};
}
Controller
#Component({
selector: "app-equipment-creation-page",
templateUrl: "./equipment-creation-page.component.html",
styleUrls: ["./equipment-creation-page.component.scss"],
})
export class EquipmentCreationPageComponent implements OnInit, OnDestroy {
public categories = [null, "Tools", "Vehicles"];
constructor(private _formBuilder: FormBuilder) {}
public categoryInformationGroup = this._formBuilder.group({
existingCategory: this._formBuilder.group({
category: new FormControl(null, [ Validators.required ])
}),
newCategory: this._formBuilder.group({
name: new FormControl("", [Validators.required]),
description: new FormControl("", [Validators.required])
})
}, {
validators: [
onlyOneValidator(["existingCategory", "newCategory"])
],
});
public ngOnDestroy(): void {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
}
private subscriptions: Subscription[] = [];
public ngOnInit(): void {
this.subscriptions.push(this.categoryInformationGroup.controls.existingCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.newCategory.disable();
} else {
this.categoryInformationGroup.controls.newCategory.enable();
}
})
).subscribe());
this.subscriptions.push(this.categoryInformationGroup.controls.newCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.existingCategory.disable();
} else {
this.categoryInformationGroup.controls.existingCategory.enable();
}
})
).subscribe());
}
}
Template
<form [formGroup]="categoryInformationGroup.controls.existingCategory">
<mat-form-field>
<mat-label>Apply to existing category?</mat-label>
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
{{ category ?? "None" }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
OR
<form [formGroup]="categoryInformationGroup.controls.newCategory">
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="name" placeholder="Name">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.name.hasError('required')">This field
is required
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="description" placeholder="Description">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.description.hasError('required')">
This field is required
</mat-error>
</mat-form-field>
</form>
Hopefully this helps or at least gives you some ideas about how to approach this. If anyone else has any thoughts on this please let me know I would love to find a better way to do this.

Password Confirm Angular Material

I'm working to authenticate a user with Angular Material. I'm currently trying to get the proper mat-error to display when the confirmation password doesn't match the first entered pw.
Here is my html:
<mat-form-field hintLabel="Minimum 8 Characters" class="">
<mat-label>Password</mat-label>
<input
matInput
#input
type="password"
formControlName="password">
<mat-hint align="end">{{input.value?.length || 0}}/8</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Confirm</mat-label>
<input
matInput
required
type="password"
#confirm
formControlName="confirmPassword">
<mat-error *ngIf="form.get('confirmPassword').invalid || confirmPasswordMismatch">Password does not match</mat-error>
</mat-form-field>
The error displays once the user has focused on password confirmation and unfocuses without entering anything. Once the user enters anything, the error goes away even though the confirmation doesn't match the password.
Here is my TS file:
public get confirmPasswordMismatch() {
return (this.form.get('password').dirty || this.form.get('confirmPassword').dirty) && this.form.hasError('confirmedDoesNotMatch');
}
this.form = new FormGroup({
userName: new FormControl(null, [Validators.required]),
fullName: new FormControl(null, [Validators.required]),
email: new FormControl(null, [Validators.required, Validators.pattern(this.EMAIL_REGEX)]),
password: new FormControl(null),
confirmPassword: new FormControl(null, ),
}, (form: FormGroup) => passwordValidator.validate(form));
The desired effect is that the error shows when the user has entered text into pw input when confirm pw is empty and to show an error when both have text but confirm doesn't match pw.
I solved it like this:
Template:
<mat-form-field>
<input matInput type="password" placeholder="Password" formControlName="password" (input)="onPasswordInput()">
<mat-error *ngIf="password.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="password.hasError('minlength')">Password must have at least {{minPw}} characters</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" placeholder="Confirm password" formControlName="password2" (input)="onPasswordInput()">
<mat-error *ngIf="password2.hasError('required')">Please confirm your password</mat-error>
<mat-error *ngIf="password2.invalid && !password2.hasError('required')">Passwords don't match</mat-error>
</mat-form-field>
Component:
export class SignUpFormComponent implements OnInit {
minPw = 8;
formGroup: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.formGroup = this.formBuilder.group({
password: ['', [Validators.required, Validators.minLength(this.minPw)]],
password2: ['', [Validators.required]]
}, {validator: passwordMatchValidator});
}
/* Shorthands for form controls (used from within template) */
get password() { return this.formGroup.get('password'); }
get password2() { return this.formGroup.get('password2'); }
/* Called on each input in either password field */
onPasswordInput() {
if (this.formGroup.hasError('passwordMismatch'))
this.password2.setErrors([{'passwordMismatch': true}]);
else
this.password2.setErrors(null);
}
}
Validator:
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
if (formGroup.get('password').value === formGroup.get('password2').value)
return null;
else
return {passwordMismatch: true};
};
Notes:
Thanks to onPasswordInput() being called from either password field, editing the first password field (and thus invalidating the password confirmation) also causes the mismatch error to be displayed in the password confirmation field.
The *ngIf="password2.invalid && !password2.hasError('required')" test for the password confirmation field ensures that never both error messages ("mismatch" and "required") are displayed at the same time.
Based on your code, it doesn't look like you added any validation for the confirmPassword field: confirmPassword: new FormControl(null, ) so the only validation happening is via the required attribute. Also, the mat-error will only be displayed by the form field if the form control is invalid. That means you can't force an error to be displayed just by using ngIf. Since you only have required validation on that field, it makes sense that you only have an error when the field is empty. To solve this problem, you need to create a validator for mismatch checking and add it to the confirmPassword form control. As an alternative, you can manually add errors to the form control using setErrors() when the field changes by adding an input listener - for example (this is just from memory - may need fixing):
<mat-form-field>
<mat-label>Confirm</mat-label>
<input matInput required type="password" #confirm formControlName="confirmPassword"
(input)="onInput($event.target.value)">
<mat-error *ngIf="form.get('confirmPassword').invalid>
Password does not match
</mat-error>
</mat-form-field>
onInput(value) {
if (this.form.hasError('confirmedDoesNotMatch')) { // or some other test of the value
this.form.get('confirmPassword').setErrors([{'confirmedDoesNotMatch': true}]);
} else {
this.form.get('confirmPassword').setErrors(null);
}
}
you are essentially validating how 2 fields in a form interact with each other ("password" and "confirm password" fields). This is known as "cross-field validation"
that means, your custom validator cannot be assigned to just 1 field. The custom validator needs to be assigned to the common parent, the form.
Here is the official documented best practice, for validating how 2 fields in a form interact with each other
https://angular.io/guide/form-validation#cross-field-validation
this code snippet worked for me
template:
<form method="post" [formGroup]="newPasswordForm">
<input type="password" formControlName="newPassword" />
<input type="password" formControlName="newPasswordConfirm" />
<div class="err-msg" *ngIf="newPasswordForm.errors?.passwordMismatch && (newPasswordForm.touched || newPasswordForm.dirty)">
confirm-password does not match password
</div>
</form>
component.ts:
export class Component implements OnInit {
this.newPasswordForm = new FormGroup({
'newPassword': new FormControl('', [
Validators.required,
]),
'newPasswordConfirm': new FormControl('', [
Validators.required
])
}, { validators: passwordMatchValidator });
}
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
return formGroup.get('newPassword').value === formGroup.get('newPasswordConfirm').value ?
null : { 'passwordMismatch': true };
}
Note that for passwordMatchValidator, it is outside the component class. It is NOT inside the class
Henry's answer was very helpful (and I'm surprised it doesn't have more votes), but I've made a tweak to it so that it plays well with Angular Material controls.
The tweak relies on the fact that the FormGroup class has a parent property. Using this property, you can tie the validation message to the password confirmation field, and then refer up the chain in the validator.
public signUpFormGroup: FormGroup = this.formBuilder.group({
email: ['', [Validators.required, Validators.pattern(validation.patterns.email)]],
newPassword: this.formBuilder.group({
password: ['', [
Validators.required,
Validators.minLength(validation.passwordLength.min),
Validators.maxLength(validation.passwordLength.max)]],
confirmPassword: ['', [Validators.required, passwordMatchValidator]]
})
});
The validator looks like this:
export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
const parent = formGroup.parent as FormGroup;
if (!parent) return null;
return parent.get('password').value === parent.get('confirmPassword').value ?
null : { 'mismatch': true };
}
and the form then looks like this:
<div formGroupName="newPassword" class="full-width new-password">
<mat-form-field class="full-width sign-up-password">
<mat-label>{{ 'sign-up.password' | translate }}</mat-label>
<input matInput [type]="toggleSignUpPass.type" [maxlength]="max" [minlength]="min" formControlName="password" required/>
<mat-pass-toggle-visibility #toggleSignUpPass matSuffix></mat-pass-toggle-visibility>
<mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
<mat-hint aria-live="polite">{{ signUpFormGroup.get('newPassword').value.password.length }}
/ {{ max }} </mat-hint>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('required')">
{{ 'sign-up.password-error.required' | translate }}
</mat-error>
<mat-error class="password-too-short" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('minlength')">
{{ 'sign-up.password-error.min-length' | translate:passwordRestriction.minLength }}
</mat-error>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('maxlength')">
{{ 'sign-up.password-error.max-length' | translate:passwordRestriction.maxLength }}
</mat-error>
</mat-form-field>
<mat-form-field class="full-width sign-up-confirm-password">
<mat-label>{{ 'sign-up.confirm-password' | translate }}</mat-label>
<input matInput [type]="toggleSignUpPassConf.type" formControlName="confirmPassword" required />
<mat-pass-toggle-visibility #toggleSignUpPassConf matSuffix></mat-pass-toggle-visibility>
<mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
<mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('required')">
{{ 'sign-up.confirm-password-error.required' | translate }}
</mat-error>
<mat-error class="password-mismatch" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('mismatch')">
{{ 'sign-up.password-error.mismatch' | translate }}
</mat-error>
</mat-form-field>
</div>

Disable Angular 5 Input fields correct way

I have a FormGroup that was created like that:
form: FormGroup;
constructor(private _formBuilder: FormBuilder) { }
this.form = this._formBuilder.group({
name: ['', Validators.required],
email: ['', Validators.required, Validators.email]
});
When an event occurs I want to disable those inputs, so, in the HTML I added:
<input class="form-control" placeholder="Name" name="name" formControlName="name" [(ngModel)]="name" autocomplete="off" [disabled]="isDisabled" required>
<input class="form-control" placeholder="Email" name="email" formControlName="email" [(ngModel)]="email" email="true" autocomplete="off" [disabled]="isDisabled" required>
Where isDisabled is a variable I toggle to true when the said event happens.
As you can imagine, I get the message:
It looks like you're using the disabled attribute with a reactive form
directive. If you set disabled to true
when you set up this control in your component class, the disabled attribute will actually be set in the DOM for
you. We recommend using this approach to avoid 'changed after checked' errors.
Example:
form = new FormGroup({
first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
last: new FormControl('Drew', Validators.required)
});
So, with the example this warning shows and with a little search I found that I should declare my controls like:
name: [{ value: '', disabled: this.isDisabled }, Validators.required]
The problem is: It is not toggling between disabled/enabled when the variable changes between true/false
How is the correct way of having a variable controlling if an input is enabled or disabled?
I don't want to do it manually (ex: this.form.controls['name'].disable()) because it doesn't seems a very reactive way, I would have to call it inside a good amount of methods. Probably not a good practice.
Thx
You can change the assignment of the variable to a setter method so that you'd have:
set isDisabled(value: boolean) {
this._isDisabled = value;
if(value) {
this.form.controls['name'].disable();
} else {
this.form.controls['name'].enable();
}
}
One solution is creating a directive and using binding for that as described in here
import { NgControl } from '#angular/forms';
#Directive({
selector: '[disableControl]'
})
export class DisableControlDirective {
#Input() set disableControl( condition : boolean ) {
const action = condition ? 'disable' : 'enable';
this.ngControl.control[action]();
}
constructor( private ngControl : NgControl ) {
}
}
then
<input class="form-control" placeholder="Name" name="name" formControlName="name" autocomplete="off" [disableControl]="isDisabled" required>
NOTE:
Doesn't work with Ivy
For input use [readonly] rather than [disabled] and it'll work
The proper way to disable an form control. With reactive forms you should never disable an input from the template. So in whatever method in your component you are calling you should disable the input like this:
this.form.get('name').disable();
Disable TextBox in Angular 7
<div class="center-content tp-spce-hdr">
<div class="container">
<div class="row mx-0 mt-4">
<div class="col-12" style="padding-right: 700px;" >
<div class="form-group">
<label>Email</label>
<input [disabled]="true" type="text" id="email" name="email"
[(ngModel)]="email" class="form-control">
</div>
</div>
</div>
</div>
You can use this code on your ts file.
All controls:
this.form.disable()
this.form.enable()
Some controls
this.form.get('first').disable()
this.form.get('first').enable()
Or Initial set method.
first: new FormControl({value: '', disabled: true}, Validators.required)
In Reactive Form you can disable all form fields by this.form.disable().
In Template Driven Form you can disable all form fields by this.myform.form.disable() where myForm is #ViewChild('form') myForm;
Not the clean or dry'st I imagine. Bu I tried the "set method" and didn't work out of the box...
Needed some refactoring () => {simpleVersion} (hope it helps someone)
component.ts
...
// standard stuff...
form: FormGroup;
isEditing = false;
...
// build the form...
buildForm() {
this.form = this.FormBuilder.group({
key: [{value:'locked', disabled: !this.isEditing}],
name: [],
item: [],
active: [false]
})
}
// map the controls to "this" object
// => i.e. now you can refer to the controls directly (ex. this.yourControlName)
get key() { return <FormControl>this.form.get('key') }
get name() { return <FormControl>this.form.get('name') }
...
// ----------------------------------------
// THE GRAND FINALÉ - disable entire form or individual controls
// ----------------------------------------
toggleEdit() {
if(!this.isEditing) {
this.key.enable(); // controls
this.name.enable();
// this.form.enable(); // the form
this.isEditing = !this.isEditing;
} else {
this.key.disable(); // the controls
this.name.disable(); // the controls
// this.form.disable(); // or the entire form
this.isEditing = !this.isEditing;
}
}
& perhaps overkill on the HTML logic, so hope you find the bonus integrated ngClass toggle just as helpful.
component.html (toggle button)
<div class="btn-group" (click)="toggleEdit()">
<label
class="btn"
role="button"
[ngClass]="{'btn-success': isEditing,
'btn-warning': !isEditing}">toggle edit
</label>
</div>
The solution by creating a directive and using binding for that worked for me in Angular 10 is described in here
Template:
<mat-form-field>
<input matInput class="form-control" formControlName="NameText" [disableControl]="condition" type="text">
</mat-form-field>
TypeScript:
import { Directive, Input } from '#angular/core';
import { NgControl } from '#angular/forms';
#Directive({
selector: '[opDisabled]'
})
export class DisabledDirective {
#Input()
set opDisabled(condition: boolean) {
const action = condition ? 'disable' : 'enable';
setTimeout(() => this.ngControl.control[action]());
}
constructor(private ngControl: NgControl) {}
}
I have a function that enables a control on click.
controlClick(control: any) {
this.form.controls[control.ngControl.name].enable();
}
Originally i was using
control.disabled = false;
But this did not work for controls with <input> for example in my mat-chip-list.
I use FormGroup and disable each control in the constructor
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<EditDialogComponent>,
#Inject(MAT_DIALOG_DATA) data
) {
this.data = data;
this.multiEdit = data.multiSelect;
this.form = new FormGroup({
autoArchive: new FormControl({
value:
this.getPreFill(data.selectedPolicy.autoArchive, this.multiEdit),
disabled: true
/*, Validators.required*/
}),
...
<mat-form-field (click)="controlClick(retrieveChipList)">
<mat-chip-list #retrieveChipList formControlName="retrieveChipList">
<mat-chip
*ngFor="let email of data.selectedPolicy.retrieveEmailsToBeNotified"
(removed)="remove(email)" [selectable]="selectable"
[removable]="removable"
>
{{ email }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Retrieve Emails to be Notified"
formControlName="retrieveChipList"
[matChipInputFor]="retrieveChipList"
[matChipInputAddOnBlur]="true"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRetrieveEmails($event)"
/>
</mat-chip-list>
</mat-form-field>
As control can't be accessed in reactive forms. This is due to migration to Ivy. You can use can access the html attribute directly and specify your condition. See this issue #35330 for more details and alternative methods.
[attr.disabled]="true || false"
You can create set and get method to achieve conditionally enable/disable functionality for Angular material Reactive Forms:
*// 1st Step:
set isDisabled(value:boolean) {
if(value){
this.form.controls['Form controller name'].disable(); //you can keep empty if you don't add controller name
}
else{
this.form.controls['Form controller name'].enable();
}
}
// 2nd Step: Add conditions in getter
get isDisabled(){
return condition ? true : false;
}
// 3rd Step
this.form = this._formBuilder.group({
name: [{value: '', disabled: this.isDisabled }, [Validators.required]],
});
Remove [disabled]="isDisabled" from input fields and add ng-disabled="all" and in the event field add ng-model="all"
<body ng-app="">
Click here to disable all the form fields:<input type="checkbox" ng-model="all">
<input class="form-control" placeholder="Name" name="name" formControlName="name" [(ngModel)]="name" autocomplete="off" ng-disabled="all" required>
<input class="form-control" placeholder="Email" name="email" formControlName="email" [(ngModel)]="email" email="true" autocomplete="off" ng-disabled="all" required>
</body>

Add inputs dynamically when click on button in angular 4

I would create a form with the possibility to add inputs dynamically
I found a question about the same problem in angular 2 but I can't make it working in my exemple
Here's my component ts file :
export class AjoutProjetComponent implements OnInit {
isLinear = false;
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;
constructor(private _formBuilder: FormBuilder) {}
ngOnInit() {
this.secondFormGroup = this._formBuilder.group({
pers: [this._formBuilder.array([this.createItem()])]
});
}
createItem(): FormGroup {
return this._formBuilder.group({
name: ['', Validators.required]
poste: ['', Validators.required],
});
}
addItem(): void {
const control = < FormArray > this.secondFormGroup.controls['pers'];
control.push(this.createItem());
}
}
then HTML file
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>Constituez votre équipe</ng-template>
<div formArrayName="pers">
<mat-form-field *ngFor="let control of secondFormGroup.controls.pers.controls; let i= index">
<input matInput placeholder="Nom collaborateur" formControlName="name" required>
</mat-form-field>
</div>
</form>
</mat-step>
<div>{{secondFormGroup.value | json}}</div>
When I click in my favorite icon I get this error :
ERROR TypeError: control.push is not a function at AjoutProjetComponent.addItem
How can I make adding dynamically inputs working ?
UPDATE
I have updated my html code so that I could print two inputs but when I run my code I get this error now
ERROR Error: Cannot find control with path: 'pers -> name'
You did not declare your FormArray properly. You use arrays only to initialize simple FormControls, not FormGroups or FormControls, change to :
this.secondFormGroup = this._formBuilder.group({
pers: this._formBuilder.array([this.createItem()]) // remove opening and closing brackets
});
To see the inputs added dynamically to the html, you need to use an ngFor loop. I think you somewhat misunderstood the usage of formArrayName, which only adds context to the template to use with FormArrays. Try this:
<ng-container formArrayName="pers">
<input placeholder="Address"
*ngFor="let control of secondFormGroup.controls.pers.controls"
[formControl]="control.controls.name" required />
</ng-container>
And read more about FormArrayName directive here

Angular material mat-error cannot show message

I have 2 datetime picker, endDate cannot be less than startDate
endDateAfterOrEqualValidator(formGroup): any {
var startDateTimestamp: number;
var endDateTimestamp: number;
startDateTimestamp = Date.parse(formGroup.controls['startDateForm'].value);
endDateTimestamp = Date.parse(formGroup.controls['endDateForm'].value);
return (endDateTimestamp < startDateTimestamp) ? { endDateLessThanStartDate: true } : null;
}
in html:
<mat-form-field>
<input matInput name="endDate" id="endDate" formControlName="endDateForm" [(ngModel)]="endDate" [matDatepicker]="toDatePicker"
placeholder="To Date">
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
<mat-datepicker disabled="false" #toDatePicker></mat-datepicker>
<mat-error *ngIf="trainingDetail.hasError('endDateLessThanStartDate')">Not valid<mat-error>
</mat-form-field>
with "mat-error", the message does not display. I try to change by "small"
<small *ngIf="trainingDetail.hasError('endDateLessThanStartDate')">Not valid</small>
the message well. Please advice me how to using mat-error
a mat-error only shows when a FormControl is invalid, but you have the validation on your formgroup. So in that case you need to use a ErrorStateMatcher
In your case it would look like this:
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const invalidCtrl = !!(control && control.invalid);
const invalidParent = !!(control && control.parent && control.parent.invalid);
return (invalidCtrl || invalidParent);
}
}
Also worth mentioning, it's not recommended to have two bindings, i.e formControl and ngModel. Remove the ngModel and utilize the form control instead. If you receive your start date and end date at a later point, you can use patchValue (just set some values to form) or setValue (set all values to form)
mark in component the errorstatematcher:
matcher = new MyErrorStateMatcher();
As for your custom validator, you don't need to parse the dates, just check if end date is smaller than start date:
checkDates(group: FormGroup) {
if (group.controls.endDate.value < group.controls.startDate.value) {
return { endDateLessThanStartDate: true }
}
return null;
}
and then mark the error state matcher in your template:
<mat-form-field>
<input matInput [matDatepicker]="picker2" type="text" formControlName="endDate" [errorStateMatcher]="matcher">
<mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
<mat-error *ngIf="myForm.hasError('endDateLessThanStartDate')">End date cannot be earlier than start date</mat-error>
</mat-form-field>
Here's a StackBlitz
If you want to set a control as invalid from the .ts file manually...
HTML:
<mat-form-field class="full-width">
<input matInput [formControl]="exampleFormControl" (change)="changeDetected()">
<mat-hint>(Optional)</mat-hint>
<mat-error *ngIf="exampleFormControl.hasError('invalid')">
Must be a <strong>valid input</strong>!
</mat-error>
</mat-form-field>
TS:
import { FormControl } from '#angular/forms';
#Component({
selector: 'derp',
templateUrl: './derp.html',
styleUrls: ['./derp.scss'],
})
export class ExampleClass {
// Date Error Form Control
exampleFormControl = new FormControl('');
// Variable Check
inputValid: boolean;
changeDetected() {
// Check if input valid
if (this.inputValid) {
console.log('Valid Input');
} else {
console.log('Invalid Input');
// IMPORTANT BIT - This makes the input invalid and resets after a form change is made
this.exampleFormControl.setErrors({
invalid: true,
});
}
}
// CODE THAT CHANGES VALUE OF 'inputValid' //
}

Categories

Resources