Pass a DOM event to custom form validator in Angular - javascript

I am trying to validate a form using the reactive approach. I am using the file input to take a file from the user. I have defined a custom validator that allows the user to upload a file on certain conditions. While trying to do so, I am getting an error. The validator does not receive the event as a whole but rather only the path of the file something like C:\fakepath\abc.xlsx. I want to pass the DOM event so that I can handle all the properties of files like type, size etc.
Here's my code:
file.validator.ts
import { AbstractControl } from '#angular/forms';
export function ValidateFile(control: AbstractControl) :
{ [key: string]: boolean } | null {
const value = control.value;
if (!value) {
return null;
}
return value.length < 0 && value.files[0].type !== '.xlsx' && value.files[0].size > 5000000
? { invalidFile: true } : null;
}
sheet.component.ts
constructor(
private formBuilder: FormBuilder,
private alertService: AlertService
) {
this.sheetForm = this.formBuilder.group({
sheetType: ['Select Sheet Type', [Validators.required]],
sheetUpload: [null, [Validators.required, ValidateFile]],
sheetDescription: [
null,
[
Validators.required,
Validators.minLength(10),
Validators.maxLength(100),
],
],
});
}
sheet.component.html
<div class="input-group">
<label for="sheet-upload">Upload Sheet: </label>
<input
id="sheet-upload"
type="file"
(change)="handleFileInput($event)"
formControlName="sheetUpload"
accept=".xlsx"
/>
<small
id="custom-error-message"
*ngIf="
(sheetForm.get('sheetUpload').dirty ||
sheetForm.get('sheetUpload').touched) &&
sheetForm.get('sheetUpload').invalid
"
>
The file size exceeds 5 MB or isn't a valid excel type. Please
upload again.
</small>
</div>
Any help would be appreciated. Thanks!

Not sure if this is the best way but it works
Create a directive to attach the native element to form control
On validation get the file from the native element in the validator
And also to use formControlName you need to assign a formGroup in the parent element (ignore if included in some other parent element)
#Directive({
selector: '[formControlName]',
})
export class NativeElementInjectorDirective implements OnInit {
constructor(private el: ElementRef, private control: NgControl) {}
ngOnInit() {
(this.control.control as any).nativeElement = this.el.nativeElement;
}
}
file.validator.ts
export function ValidateFile(control: any): { [key: string]: boolean } | null {
const value = control.value;
const file = control?.nativeElement?.files[0];
if (!value) {
return null;
}
return value.length < 0 || file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.size > 5000000
? { invalidFile: true }
: null;
}
sheet.component.html
<div class="input-group" [formGroup]="sheetForm">
<label for="sheet-upload">Upload Sheet: </label>
<input
id="sheet-upload"
type="file"
formControlName="sheetUpload"
accept=".xlsx"
/>
<small
id="custom-error-message"
*ngIf="
(sheetForm.get('sheetUpload').dirty ||
sheetForm.get('sheetUpload').touched) &&
sheetForm.get('sheetUpload').invalid
"
>
The file size exceeds 5 MB or isn't a valid excel type. Please upload again.
</small>
</div>

You can get reference to input element and use it in validator.
<input #sheetUpload ...>
#ViewChild('sheetUpload') fileInput: HTMLInputElement;
private ValidateFile(): ValidatorFn {
return (control) => {
const value = control.value;
if (!value || !this.fileInput) {
return null;
}
const file = this.fileInput.files[0];
return value.length < 0 && file.type !== '.xlsx' && file.size > 5000000
? { invalidFile: file.name }
: null;
}
}

Related

Invalid input focus is not working with FormArray

So everything works fine with normal FormGroup but when it comes to FormArray it doesn't focus the invalid input.
My form initialization is below
initForm() {
this.parentForm= this.fb.group({
childFormArray: this.fb.array([this.createchildForm()])
});
}
after this, I initialize formarray like below
createChildForm(data?: any): FormGroup {
var childForm = this.fb.group({
name: [data?.name? data?.name: '']
});
childForm .valueChanges.subscribe(value => {
var fieldWithValue = Object.keys(value).filter(key => value[key] == '');
fieldWithValue.forEach(conName => {
childForm .get(conName)?.addValidators([Validators.required]);
});
});
return childForm ;
}
My method to set errors after clicking submit (requirement);
assignError(){
this.parentForm.controls.childFormArray.value.forEach((v: any, index: number) => {
var array = this.parentForm.controls.childFormArrayas FormArray;
var item = array.at(index);
var emptyItems = Object.keys(v).filter(key => v[key] == '');
emptyItems.forEach(ele => {
if (ele != "section") {
item.get(ele)?.updateValueAndValidity({ emitEvent: false });
}
});
});
}
and after this I have made my validator which will check for invalid input and focus it.
import { Directive, HostListener, ElementRef } from '#angular/core';
#Directive({
selector: '[focusInvalidInput]'
})
export class FormDirective {
constructor(private el: ElementRef) { }
#HostListener('submit')
onFormSubmit() {
const invalidControl = this.el.nativeElement.querySelector('.ng-invalid');
if (invalidControl) {
invalidControl.focus();
}
}
}
after this I have used its selector in my corresponding form
focusInvalidInput (ngSubmit)="saveDetails()"
and inside submit method I call my error adding method which is
saveDetails(){
assignError();
}
After doing all this I am able to focus invalid input but somehow its not working for formarray.
and when I console invalidControl its prints all the invalid input which should not happen maybe bcz there are many invalid input and whome should it focus so I tried using .first() method but it gives error saying first is not a method
The actual reason was focus doesn't work on div and my input which were using formArray's controls were wrapped inside a div which is
<div id="resp-table-body" *ngFor="let item of getParentFormControls(); let i = index"
[formGroupName]="i">
<div class="table-body-cell">
<input type="text" class="form-control no_shadow_input" id="name"
placeholder="Enter Here" formControlName="name" autocomplete="off">
<span *ngIf="item.get('name')?.hasError('required')"
class="text-danger">
Name is required
</span>
</div>
</div>
So all I had to change is add input.ng-invalid in my directive
const invalidControl = this.el.nativeElement.querySelector('input.ng-invalid');
Now everything is working fine

Mat-Error Required validation error message shows when form is first open

I have a custom control component that is used to show a required validation error for mat-chip-lists. On the form that I'm using this component, when I first open this form the required validation message shows. I only want it to show when the field is not populated with any data. Can someone please provide a reason why and a solution to what I need to do to get the validation to work properly.
I could be overcomplicating this and it's much simpler than I thought.
html where mat-error element for required show:
<mat-form-field [floatLabel]="floatLabel">
<mat-label>{{ label }}</mat-label>
<mat-chip-list #optionList aria-label="label" required>
<mat-chip *ngFor="let item of selectedOptions" (removed)="removed(item)"
[removable]="!item.disabled" [disabled]="item.disabled">
{{ item.text }}
<mat-icon *ngIf="!item.disabled" matChipRemove>cancel</mat-icon>
</mat-chip>
<input
#optionInput
type="text"
[placeholder]="placeholder"
[formControl]="formControl"
[matAutocomplete]="optionAutoComplete"
[matChipInputFor]="optionList"
[required]="required"
/>
</mat-chip-list>
<mat-autocomplete #optionAutoComplete="matAutocomplete"
(optionSelected)="selected($event.option.value)">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option.text }}
</mat-option>
</mat-autocomplete>
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
</mat-form-field>
<mat-error *ngIf="required === true && hasValue === false &&
isInitialized === true"> {{ label }} is
<strong>required</strong> </mat-error>
Typescript for the custom control
The removed function fires when you remove selected items from the list. It turns the check back on and sends the update to the parent component.
Component({
selector: 'app-chips',
templateUrl: './chips.component.html',
})
export class ChipsComponent implements OnInit, DoCheck {
#Input() label = '';
#Input() placeholder = '';
#Input() options: Options[] = [];
#Input() selectedOptions: Options[] = [];
#Input() floatLabel: FloatLabelType = 'auto';
#Input() hint!: string | undefined;
#Input() required = true;
hasValue: boolean = this.selectedOptions.length > 0;
isInitialized: boolean | undefined;
#ViewChild('optionInput') optionInput: ElementRef | undefined;
#Output() onRemoved = new EventEmitter<Options>();
#Output() selectedOptionsChanged = new EventEmitter<Options[]>();
formControl = new FormControl('');
filteredOptions: Observable<Options[]> | undefined;
iterableDiffer: IterableDiffer<Options>;
constructor(private readonly iterableDiffers: IterableDiffers) {
this.iterableDiffer = this.iterableDiffers.find([]).create();
}
ngDoCheck(): void {
const optionChanges = this.iterableDiffer.diff(this.options);
if (optionChanges) {
this.filteredOptions = of(this.options);
}
if (this.required === undefined) {
this.required = false;
}
}
ngOnInit(): void {
this.subscribeFilterOptions();
this.isInitialized = true;
}
ngAfterViewInit(): void {
this.isInitialized = true;
}
selected(value: Options): void {
if (this.optionInput) {
this.optionInput.nativeElement.value = '';
}
if (!this.selectedOptions.find((x) => x.text === value.text)) {
this.selectedOptions.push(value);
this.selectedOptionsChanged.emit(this.selectedOptions);
}
this.hasValue = this.selectedOptions.length > 0;
}
private subscribeFilterOptions() {
this.filteredOptions = this.formControl.valueChanges.pipe(
startWith(''),
map((value: string | Options) =>
value && typeof value === 'string' ? this.options.filter((o) =>
o.text.toLowerCase().includes(value.toLowerCase())) :
this.options.slice()
)
);
}
removed(value: Options): void {
this.onRemoved.emit(value);
this.hasValue = this.selectedOptions.length > 0;
}
}
Mat-chip-list component on the form
<div class="col-md-12">
<app-linq-chips
label="Entities"
placeholder="Add Entity..."
[options]="entityOptions"
[selectedOptions]="selectedEntities"
[hint]="
entityListHasDisabledOptions === true
? 'Please remove this contact from any roles for an entity prior to removing their
association to that entity.'
: undefined
"
(onRemoved)="removeEntity($event)"
>
</app-linq-chips>
</div>
Your variable hasValue is only set when a chip is selected or removed. When the component is initialized it's value is false as the number of selected options is zero.
hasValue: boolean = this.selectedOptions.length > 0;
The required variable is initialized to true, so the mat error condition below:
<mat-error *ngIf="required === true && hasValue === false"> {{ label }} is
<strong>required</strong> </mat-error>
will be true, so the error will show initially.
To fix this issue, add an extra boolean variable, isInitialized (or whatever you want to name it), set it to false in the ngOnInit(), then set it to true in the ngAfterViewinit().
Update your mat-error condition like follows:
<mat-error *ngIf="required === true && hasValue === false && isInitialized === true"> {{ label }} is
<strong>required</strong> </mat-error>
Try the above and it should only error check after the component selections are made.

FormControl validator always invalid

Following my previous question, I'm trying to create a custom validator that allow the users to type only specific values in an input of text.
app.component.ts:
export class AppComponent implements OnInit {
myForm: FormGroup;
allowedValuesArray = ['Foo', 'Boo'];
ngOnInit() {
this.myForm = new FormGroup({
'foo': new FormControl(null, [this.allowedValues.bind(this)])
});
}
allowedValues(control: FormControl): {[s: string]: boolean} {
if (this.allowedValuesArray.indexOf(control.value)) {
return {'notValidFoo': true};
}
return {'notValidFoo': false};
}
}
app.component.html:
<form [formGroup]="myForm">
Foo: <input type="text" formControlName="foo">
<span *ngIf="!myForm.get('foo').valid">Not valid foo</span>
</form>
The problem is that the foo FormControl is always false, (the myForm.get('foo').valid is always false).
What wrong with my implementation?
you just need to return null when validation is ok. and change that method like below
private allowedValues: ValidatorFn (control: FormControl) => {
if (this.allowedValuesArray.indexOf(control.value) !== -1) {
return {'notValidFoo': true};
}
return null;
}

Get sibling AbstractControl from Angular reactive FormArray - Custom Validation

I'm having a Contact form in Angular using a Reactive form. The form contains firstName, lastName and an Array of address. Each address formgroup contains a checkbox, if the checkbox is checked, validation of the State text box mandatory is needed, along with min and max char length.
I have written a custom validator namely "stateValidator" and I tried to get the sibling element "isMandatory" but I am not able to get the control.
I tried the following approach
control.root.get('isMandatory'); - Its returning null
control.parent.get('isMandatory'); - Its throwing exception
I found some links in stackoverflow, but there is no answer available and some answers are not giving solutions, for example the code above: control.root.get('isMandatory'); I got this from one of the video tutorials but nothing worked.
The complete working source code is available in StackBlitz: https://stackblitz.com/edit/angular-custom-validators-in-dynamic-formarray
app.component.ts
import { Component } from '#angular/core';
import { FormBuilder, FormGroup, FormArray, Validators, AbstractControl } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
public userForm: FormGroup;
constructor(private _fb: FormBuilder) {
this.userForm = this._fb.group({
firstName: [],
lastName: [],
address: this._fb.array([this.addAddressGroup()])
});
}
private addAddressGroup(): FormGroup {
return this._fb.group({
street: [],
city: [],
state: ['', this.stateValidator],
isMandatory: [false, [Validators.required]]
});
}
get addressArray(): FormArray {
return <FormArray>this.userForm.get('address');
}
addAddress(): void {
this.addressArray.push(this.addAddressGroup());
}
removeAddress(index: number): void {
this.addressArray.removeAt(index);
}
stateValidator(control: AbstractControl): any {
if(typeof control === 'undefined' || control === null
|| typeof control.value === 'undefined' || control.value === null) {
return {
required: true
}
}
const stateName: string = control.value.trim();
const isPrimaryControl: AbstractControl = control.root.get('isMandatory');
console.log(isPrimaryControl)
if(typeof isPrimaryControl === 'undefined' || isPrimaryControl === null||
typeof isPrimaryControl.value === 'undefined' || isPrimaryControl.value === null) {
return {
invalidFlag: true
}
}
const isPrimary: boolean = isPrimaryControl.value;
if(isPrimary === true) {
if(stateName.length < 3) {
return {
minLength: true
};
} else if(stateName.length > 50) {
return {
maxLength: true
};
}
} else {
control.setValue('');
}
return null;
}
}
app.component.html
<form class="example-form" [formGroup]="userForm">
<div>
<mat-card class="example-card">
<mat-card-header>
<mat-card-title>Users Creation</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="primary-container">
<mat-form-field>
<input matInput placeholder="First Name" value="" formControlName="firstName">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Last Name" value="" formControlName="lastName">
</mat-form-field>
</div>
<div formArrayName="address">
<div class="address-container" *ngFor="let group of addressArray.controls; let i = index;"
[formGroupName]="i">
<fieldset>
<legend>
<h3>Address: {{i + 1}}</h3>
</legend>
<mat-checkbox formControlName="isMandatory">Mandatory</mat-checkbox>
<div>
<mat-form-field>
<input matInput placeholder="Street" value="" formControlName="street">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="City" value="" formControlName="city">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="State" value="" formControlName="state">
</mat-form-field>
</div>
</fieldset>
</div>
</div>
<div class="form-row org-desc-parent-margin">
<button mat-raised-button (click)="addAddress()">Add more address</button>
</div>
</mat-card-content>
</mat-card>
</div>
</form>
<mat-card class="pre-code">
<mat-card-header>
<mat-card-title>Users Information</mat-card-title>
</mat-card-header>
<mat-card-content>
<pre>{{userForm.value | json}}</pre>
</mat-card-content>
</mat-card>
Kindly assist me how to get the sibling abstract control in the custom validator method.
I tried the code which was specified in the following couple of answers
isPrimaryControl = (<FormGroup>control.parent).get('isMandatory')
It's throwing an error ERROR Error: too much recursion. Kindly assist me how to fix this.
You may need to cast the parent to FormGroup, try using :
if(control.parent) {
(<FormGroup>control.parent).get('isMandatory')
}
You can get the value of isMandatory from Form control like this, i have changed your stateValidator method to basically typecast the control to concrete sub class then from the controls array you can get the formControls
stateValidator(control: AbstractControl): any {
let mandatory:boolean=false;
if(control.parent){
console.log('control',<FormGroup>control.parent.controls.isMandatory.value);
mandatory=<FormGroup>control.parent.controls.isMandatory.value;
}
if((typeof control === 'undefined' || control === null
|| typeof control.value === 'undefined' || control.value === null)&& mandatory) {
debugger;
return {
required: true
}
}
const stateName: string = control.value.trim();
let isPrimaryControl: AbstractControl=null;
if(control.parent){
isPrimaryControl=<FormGroup>control.parent.controls.isMandatory;
console.log(isPrimaryControl)
}
if(typeof isPrimaryControl === 'undefined' || isPrimaryControl === null||typeof isPrimaryControl.value === 'undefined' || isPrimaryControl.value === null) {
return {
invalidFlag: true
}
}
const isPrimary: boolean = isPrimaryControl.value;
if(isPrimary === true) {
if(stateName.length < 3) {
return {
minLength: true
};
} else if(stateName.length > 50) {
return {
maxLength: true
};
}
} else {
control.setValue('');
}
return null;
}

Angular - Directive's Input not updating when pressing button

I've created a directive in Angular that checks if two controls have value. If one of them has been filled, the other must be filled too, so they have to be both empty or both filled. This is working fine so far.
This directive must be disabled by default. It only must be enabled after pressing a button. To control this, I have an #Input in the directive where I bind the variable that the button sets to 'true':
import { Validator, FormControl, NG_VALIDATORS, FormGroup, NgForm } from '#angular/forms';
import { Directive, forwardRef, Input, ElementRef } from '#angular/core';
#Directive({
selector: '[correquired][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => CorrequiredDirective), multi: true }
]
})
export class CorrequiredDirective implements Validator {
#Input() other: FormControl;
#Input() enabled: boolean;
validate(c: FormControl): { [index: string]: any; } {
if (!this.enabled) { return null; }
if (this.other != null && this.other.value != null && this.other.value.trim && this.other.value.trim() === '') {
this.other.setValue(null);
}
if (c.value != null && c.value.trim && c.value.trim() === '') {
c.setValue(null);
}
if (c.value != null && c.value != undefined) {
this.other.markAsTouched();
}
if (this.other != null && c.value == null && this.other.value != null) {
return { 'correquired': { valid: false } };
}
}
}
And, in the component, I set the control this way:
<input type="text" correquired [other]="form3.controls['delivered_quantity']" [enabled]="publishing" ...
The button that sets the variable "publishing" to true also submits the form. The problem is that when pressing this button, the directive is being executed before changing the value of "publishing" and not after that, so the "enabled" variable in the directive is always false. How can I update it when pressing the button?
Thanks in advance.
Ok, I could solve it by adding a setTimeOut in the method called by the button, when setting the variable to true:
publish() {
this.publishing = true;
setTimeout(() => {
if (this.form3.control.controls['delivered_quantity'] != null) {
this.form3.control.controls['delivered_quantity'].updateValueAndValidity();
}
if (this.form3.control.controls['delivered_no'] != null)
this.form3.control.controls['delivered_no'].updateValueAndValidity();
if (this.formsValid && this.radioForm.valid) {
if (this.formsDirty) {
this.doSave()
.add(() => {
this.doPublish();
});
} else {
this.doPublish();
}
}
}, 0);
}

Categories

Resources