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);
}
Related
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;
}
}
I'm trying to render a component only if the value accessed from my Pinia store is true -
// MessageArea.vue
import { useValidationStore } from '../../stores/ValidationStore.js'
data() {
return {
errors: {
english: useValidationStore().english.error,
},
}
},
<template>
<p v-if="errors.english">
<EnglishErrorMessage />
</p>
</template>
By default, it is false -
import { defineStore } from "pinia";
export const useValidationStore = defineStore("validation", {
state: () => {
return {
english: {
input: '',
error: false,
},
}
}
})
I'm also accessing that same value from another file to check for the error -
<script>
import { useValidationStore } from '../../../stores/ValidationStore';
export default {
data() {
return {
input: useValidationStore().english.message,
error: useValidationStore().english.error,
}
},
methods: {
validate(input) {
this.input = input.target.value
const legalChars = /^[A-Za-z\s]*$/.test(this.input);
if (!legalChars && this.input !== "") {
this.error = true;
} else if (this.input === "" || (legalChars && !legalChars)) {
this.error = false;
}
console.log(this.error)
console.log(this.input)
}
}
}
</script>
<template>
<input
#input="validate"
:value="input"
/>
</template>
The console log values are updating reactively. Also, this.error will be logged as true when an illegal character is entered into the input. The reactivity is behaving as expected. So, I'm not sure why '' will not render in this case?
Am I not accessing the pinia value correctly in the template?
I've tried to understand what 'mapState()' and 'mapStores()' from the docs and if they can help me but I'm still confused.
You need to mutate the store state in validate method.
If you want to use pinia in options api you can use mapState and mapWritableState in computed property to remain rective.
Take a look here codesandbox please.
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;
}
I've reduced this to its simplest form as below:
<select (change)="switch()" [hidden]="visible" [(ngModel)]="model">
<option *ngFor="let amount of [1,2,3]" [ngValue]="amount"> {{amount}} </option>
</select>
<div [hidden]="!visible">... we swap places</div>
export class SomeComponent {
model = 1;
visible = false;
switch() {
if (this.visible === 3) {
this.visible = true;
}
}
This seems to work fine, however it also throws: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
How do I change it before it gets checked here?
You can handle this by explicitly triggering the change,
import { ChangeDetectorRef } from '#angular/core';
constructor(private cdr: ChangeDetectorRef) {}
switch() {
if (this.visible === 3) {
this.visible = true;
this.cdr.detectionChanges();
...
}
So I am working on this app in which I have used ViewContainerRef along with dynamicComponentLoader like below:
generic.component.ts
export class GenericComponent implements OnInit, OnChanges{
#ViewChild('target', { read: ViewContainerRef }) target;
#Input('input-model') inputModel: any = {};
constructor(private dcl: DynamicComponentLoader) { }
ngAfterViewInit() {
this.dcl.loadNextToLocation(DemoComponent,this.target)
.then(ref => {
if (this.inputModel[this.objAttr] === undefined) {
ref.instance.inputModel = this.inputModel;
} else {
ref.instance.inputModel[this.objAttr] = this.inputModel[this.objAttr];
}
});
console.log('Generic Component :===== DemoComponent===== Loaded');
}
ngOnChanges(changes) {
console.log('ngOnChanges - propertyName = ' + JSON.stringify(changes['inputModel'].currentValue));
this.inputModel=changes['inputModel'].currentValue;
}
}
generic.html
<div #target></div>
So It renders the DemoComponentin target element correctly.
but when I am changing the inputModel then I want to reset the view of target element.
I tried onOnChanges to reset the inputModel , its getting changed correctly but the view is not getting updated for respective change.
So I want to know if is it possible to reset the view inside ngOnChanges after the inputModel is updated?
any inputs?
There is no connection between this.inputModel and ref.instance.inputModel. If one changes you need to copy it again.
For example like:
export class GenericComponent implements OnInit, OnChanges{
componentRef:ComponentRef;
#ViewChild('target', { read: ViewContainerRef }) target;
#Input('input-model') inputModel: any = {};
constructor(private dcl: DynamicComponentLoader) { }
ngAfterViewInit() {
this.dcl.loadNextToLocation(DemoComponent,this.target)
.then(ref => {
this.componentRef = ref;
this.updateModel();
});
console.log('Generic Component :===== DemoComponent===== Loaded');
}
updateModel() {
if(!this.componentRef) return;
if (this.inputModel[this.objAttr] === undefined) {
this.componentRef.instance.inputModel = this.inputModel;
} else {
this.componentRef.instance.inputModel[this.objAttr] = this.inputModel[this.objAttr];
}
}
ngOnChanges(changes) {
console.log('ngOnChanges - propertyName = ' + JSON.stringify(changes['inputModel'].currentValue));
this.inputModel=changes['inputModel'].currentValue;
this.updateModel();
}
}