Angular 2 Custom Validator with Observable Parameter - validation

I have this custom validator:
export const mealTypesValidator = (mealSelected: boolean) => {
return (control: FormControl) => {
var mealTypes = control.value;
if (mealTypes) {
if (mealTypes.length < 1 && mealSelected) {
return {
mealTypesValid: { valid: false }
};
}
}
return null;
};
};
If I use it like this it works:
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], mealTypesValidator(true)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
The catch is, mealSelected is a property on my component - that changes when the user selects and deselects a meal.
How I call the validator above is using static true which can never change.
How can I get the validator to work when I use the component.mealSelected value as the parameter eg:
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], mealTypesValidator(this.mealSelected)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
Because if i do it as above, it evaluates this.mealSelected instantly which is false at the time - and then when the user selects a meal, it doesn't then go ahead and pass true into the custom validator.

Solution was to move the validator inside my component and use this.mealSelected to check against. Then I had an issue with the validator not being triggered when a meal was selected/deselected and I used this.findForm.controls['mealTypes'].updateValueAndValidity(); to trigger the validation.
Code (can probably be refactored to remove the parameter from the custom validator):
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], this.mealTypesValidator(true)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
mealTypesValidator = (mealSelected: boolean) => {
return (control: FormControl) => {
var mealTypes = control.value;
if (mealTypes) {
if (mealTypes.length < 1 && this.mealSelected) {
return {
mealTypesValid: { valid: false }
};
}
}
return null;
};
};
However It would still be nice to be able to have a seperate validation module to centralise validation, so if anyone knows how to have a changing parameter value such as a component field as a parameter to a custom validator - like I initially asked, then I'd appreciate an answer that goes with that technique.

Related

Angular11 asyncvalidator custom validator error not added to reactive form errors list

I have a reactive form and adding a custom async validator to check if the values entered is unique by checking the array of available values. The validator is being invoked, but the validation error 'duplicate' is not getting added to the form field errors and not displaying in the template. Not sure what i am doing wrong here.
component.ts
private registerFormGroup(): void {
this.titleField = new FormControl(
{ value: this.auditTemplate.title, disabled: true },
[Validators.required],
[TemplateNameValidator.createValidator(this.auditTemplateService)]
);
this.templateForm = this.formBuilder.group({
title: this.titleField,
tags: [this.auditTemplate.tags]
});
}
name validator:
export class TemplateNameValidator {
static createValidator(auditTemplateService: AuditTemplateService): AsyncValidatorFn {
console.log("Static factory call");
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if(isEmptyInputValue(control.value)) {
return of(null);
} else {
return control.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap((name: string) =>
auditTemplateService.isNameUnique(name)
.pipe
(
tap(response => console.log('inside switchmap', response)),
map(isUnique => !isUnique ? { 'duplicate' : true } : null),
catchError(() => of(null))
)
)
);
}
};
}
}
function isEmptyInputValue(value: any): boolean {
return value === null || value.length === 0;
}
in the template html, if i try to display the error, it is not displaying anything:
<span>Duplicate: </span>{{templateNameField.errors?.duplicate}}<br>
Thanks
AsyncValidators are used every time the AbstractControl state is changed, so you don't need to use the control.valueChanges observable.
Additionally, the observable you're returning from the validator function should send a complete signal after sending null or an error. If you return a hot observable, then the async validator will not work.
Try replacing control.valueChanges.pipe( with of(control.value).pipe(. Also, if your service method .isNameUnique() doesn't emit a complete signal, add take(1) before your the first tap() operator.
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (isEmptyInputValue(control.value)) {
return of(null);
} else {
return of(control.value).pipe(
distinctUntilChanged(),
debounceTime(500),
switchMap((name: string) =>
auditTemplateService.isNameUnique(name).pipe(
take(1),
tap((response) => console.log("inside switchmap", response)),
map((isUnique) => (!isUnique ? { duplicate: true } : null)),
catchError(() => of(null))
)
)
);
}
};

Validate each Map<string, number> value using class-validator

I'm trying to perform simple validation on a JSON input, modelled by one of my DTOs.
One of the object properties is of type Map<string, number>. an example input:
{
"type": "CUSTOM",
"is_active": true,
"current_plan_day": 1,
"custom_warmup_plan": {
"1": 123,
"2": 456
}
On my controller I'm using a DTO to specify the body type. the class, together with class-validator decorators is this:
export class CreateWarmupPlanRequestDto {
#IsEnum(WarmupPlanType)
type: string;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
hard_cap: number | null;
#IsBoolean()
is_active: boolean;
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
current_plan_day: number;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
previous_plan_day: number | null;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 }, { each: true })
#IsPositive({ each: true })
custom_warmup_plan: Map<string, number>; // PROBLEM HERE
}
I'm looking to validate each value of custom_warmup_plan to be an existing positive integer.
Validation of the other properties of the object works just fine and as expected, but for my example input I keep getting errors (2 error messages, joined):
{
"message": "each value in custom_warmup_plan must be a positive number. |#| each value in custom_warmup_plan must be a number conforming to the specified constraints",
"statusCode": 400,
"timestamp": "2021-07-29T13:18:29.331Z",
"path": "/api/warmup-plan/bc4c3f0e-8e77-46de-a46a-a908edbdded5"
}
Documentation for this seems to be pretty straight forward, but I just cant get it to work.
I've also played around with a simple Map<string, string> and the #IsString(each: true) validator, but that does not seem to work either.
any ideas?
versions:
"#nestjs/common": "^8.0.0",
"#nestjs/core": "^8.0.0",
"#nestjs/mapped-types": "^1.0.0",
"#nestjs/platform-express": "^8.0.0",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
It is necessary to convert plain object to map. Use Transform decorator from class-transformer
#IsOptional()
#IsNumber(undefined, { each: true })
#Transform(({ value }) => new Map(Object.entries(value)))
prop?: Map<string, number>;
From the docs
If your field is an array and you want to perform validation of each item in the array you must specify a special each: true decorator option
If you want to be able to validate maps you could write a custom decorator and pass in a list of class-validator functions to validate the keys and values. For example the below decorator takes as input a list of validation functions for both the keys and values (e.g. passing in isString, isObject, etc..., class-validator has a corresponding function you can call for all the validation decorators they provide)
export function IsMap(
key_validators: ((value: unknown) => boolean)[],
value_validators: ((value: unknown) => boolean)[],
validationOptions?: ValidationOptions
) {
return function (object: unknown, propertyName: string) {
registerDecorator({
name: 'isMap',
target: (object as any).constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: unknown, args: ValidationArguments) {
if (!isObject(value)) return false;
const keys = Object.keys(value);
const is_invalid = keys.some((key) => {
const is_key_invalid = key_validators.some((validator) => !validator(key));
if (is_key_invalid) return false;
const is_value_invalid = value_validators.some((validator) => !validator(value[key]));
return is_value_invalid;
});
return !is_invalid;
},
},
});
};
}
And you can use this decorator in your example like this
import { isInt } from 'class-validator'
export class CreateWarmupPlanRequestDto {
#IsOptional()
#IsMap([], [isInt])
custom_warmup_plan: Map<string, number>;
}
Using the same approach with #Daniel, I modified the code little bit so that the focus is on 'isValid' rather than 'IsInvalid'. So that we could avoid double negation.
Additionally, the coming object is transformed to map in the DTO.
#Transform(({ value }) => new Map(Object.entries(value)))
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
import * as $$ from 'lodash';
export function IsMap(
keyValidators: ((value: unknown) => boolean)[],
valueValidators: ((value: unknown) => boolean)[],
validationOptions?: ValidationOptions,
) {
return function (object: unknown, propertyName: string) {
/**
* ** value is expected to be a MAP already, we are just checking types of keys and values...
*/
registerDecorator({
name: 'isMap',
target: (object as any).constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: Map<any, any>, args: ValidationArguments) {
if (!$$.isMap(value)) {
return false;
}
const keys = Array.from(value.keys());
return $$.every(keys, (key) => {
// checking if keys are valid...
const isKeyInvalid = keyValidators.some(
(validator) => !validator(key),
);
if (isKeyInvalid) {
return false;
}
// checking if values are valid...
const isValueInvalid = valueValidators.some(
(validator) => !validator(value.get(key)),
);
if (isValueInvalid) {
return false;
} else {
return true;
}
});
},
},
});
};
}

Make an ajax call inside of a .map()

I am upgrading jquery and saw that the "async: false" option has been deprecated. This makes sense and in 99.9% of cases I agree with the rationale, but I have a case where I think I really need it and I cannot for the life of me figure out how to make this work with a purely async ajax call no matter how I use promises or async/await.
My use case is in a Vue component and I have an array of contacts. What I need to do is map over the contacts and validate them. One such validation requires a quick check of email validity via a "check_email" ajax endpoint.
Once I validate (or not) the list, I then submit the list (if valid) or show error messages (if invalid).
My code is something like this
sendContacts: function() {
valid = this.validateContacts()
if (valid) {
// send the contacts
} else {
return // will display error messages on contacts objects
}
},
validateContacts: function() {
this.contacts = this.contacts.map((contact) => {
if (!contact.name) {
contact.validDetails.name = false
contact.valid = false
return contact
}
if (!contact.email) {
contact.validDetails.emailExists = false
contact.valid = false
return contact
}
if (!check_email(email)) { // THIS IS ASYNC NOW WHAT DO I DO
contact.valid = false
contact.validDetails.emailFormat = false
}
return contact
}
var validData = this.contacts.map(c => {
return c.valid
})
return !validData.includes(false)
}
function check_email(email) {
const url = `/api/v1/users/check-email?email=${email}`
let valid = false
$.ajax({
url: url,
type: 'POST',
async: false, // I can't do this anymore
headers: {
'X-CSRFToken': csrfToken
},
success: resp => {
valid = true
},
error: err => {
}
})
return valid
}
my data function:
data: function() {
return {
contacts: [this.initContact()],
showThanks: false,
emailError: false,
blankEmail: false,
blankName: false
}
},
methods: {
initContact: function() {
return {
name: null,
email: null,
title: null,
validDetails: this.initValidDetails(),
valid: true,
}
},
initValidDetails: function() {
return {
emailDomain: true,
emailExists: true,
emailFormat: true,
name: true
}
}
}
Again, I have tried async/await in every place I could think of and I cannot get this to validate properly and then perform correct logic regarding whether the send contacts function part of the function should fire. Please help!
Once any part of your validation is asynchronous, you must treat the entire thing as asynchronous. This includes when calling validateContacts in sendContacts.
First, you should change check_email to return Promise<bool>. It's usually a bad idea to include jQuery in a Vue project so let's use fetch instead (Axios being another popular alternative).
async function check_email(email) {
const params = new URLSearchParams({ email })
const res = await fetch(`/api/v1/users/check-email?${params}`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
return res.ok
}
As for your async validation logic, it's best to map your contacts to an array of promises and wait for them all with Promise.all.
async validateContacts () {
const validationPromises = this.contacts.map(async contact => {
if (!contact.name) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
name: false
}
}
}
if (!contact.email) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailExists: false
}
}
}
if (await check_email(contact.email)) { // await here
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailFormat: false
}
}
}
return { ...contact, valid: true }
})
// now wait for all promises to resolve and check for any "false" values
this.contacts = await Promise.all(validationPromises)
return this.contacts.every(({ valid }) => valid)
}
As mentioned, now you need to treat this asynchronously in sendContacts
async sendContacts () {
if (await this.validateContacts()) {
// send the contacts
}
}

Angular2: how to create custom validator for FormGroup?

i'm creating a form with FormBuilder and i want to add a Validator to a formGroup.
Here is my code:
this.myForm = fb.group({
'name': ['', [Validators.maxLength(50), Validators.required]],
'surname': ['', [Validators.maxLength(50), Validators.required]],
'address': fb.group({
'street': ['', Validators.maxLength(300)],
'place': [''],
'postalcode': ['']
}),
'phone': ['', [Validators.maxLength(25), phoneValidator]],
'email': ['', emailValidator]
});
I would like to conditionally add validators to some of the address's formControls on certain conditions.
So I added a validator in the following way:
'address': fb.group({
'street': ['', Validators.maxLength(300)],
'place': [''],
'postalcode': ['']
}), { validator: fullAddressValidator })
Then i started to create a validator for the address FormGroup:
export const fullAddressValidator = (control:FormGroup) => {
var street:FormControl = control.controls.street;
var place:FormControl = control.controls.place;
var postalcode:FormControl = control.controls.postalcode;
if (my conditions are ok) {
return null;
} else {
return { valid: false };
}
};
I need to add the following conditions:
If all fields are empty the form is valid
If one of the field are filled in then all the fields must be required
If place is instance of country (instead of city) the postalcode
is optional
If the postalcode is filled in then the zipValidator must be
added to its formControl
So, it is possible to add Angular2 Validators to a FormGroup on certain conditions?
If it does, how to implement my conditions? Can i use setValidators() and updateValueAndValidity() in the source code of another validator?
Create a function that takes a parameter and returns a validator function
export const fullAddressValidator = (condition) => (control:FormGroup) => {
var street:FormControl = control.controls.street;
var place:FormControl = control.controls.place;
var postalcode:FormControl = control.controls.postalcode;
if (my conditions are ok) {
return null;
} else {
return { valid: false };
}
};
and use it like
'address': fb.group({
'street': ['', Validators.maxLength(300)],
'place': [''],
'postalcode': ['']
}), { validator: () => fullAddressValidator(condition) })
Yes, it's possible to set FormControl validators inside a FormGroup custom validator. Here is the solution to my needs:
export const fullAddressValidator = (control:FormGroup):any => {
var street:FormControl = control.controls.street;
var place:FormControl = control.controls.place;
var postalcode:FormControl = control.controls.postalcode;
if (!street.value && !place.value && !postalcode.value) {
street.setValidators(null);
street.updateValueAndValidity({onlySelf: true});
place.setValidators(null);
place.updateValueAndValidity({onlySelf: true});
postalcode.setValidators(null);
postalcode.updateValueAndValidity({onlySelf: true});
return null;
} else {
street.setValidators([Validators.required, Validators.maxLength(300)]);
street.updateValueAndValidity({onlySelf: true});
place.setValidators([Validators.required]);
place.updateValueAndValidity({onlySelf: true});
if (place.value instanceof Country) {
postalcode.setValidators(Validators.maxLength(5));
postalcode.updateValueAndValidity({onlySelf: true});
} else {
postalcode.setValidators([zipValidator()]);
postalcode.updateValueAndValidity({onlySelf: true});
}
}
if (street.invalid || place.invalid || postalcode.invalid) {
return {valid: false};
} else {
return null;
}
};

How to know which attribute called the waterline validation rule?

I'm doing my own custom validations on certain fields, so that only certain values are accepted (depending on the field) and the rest rejected. I would like to write a "filter" function that checks what attribute called the validation and from there decide what words the attribute is allowed to use. So the model would look something like this:
module.exports = {
types: {
filter: function(attribute) {
if (attribute === 'number') {
switch(attribute.value) {
case 'one':
return true;
case 'two':
return true;
default:
return false;
}
} else if (attribute === 'color') {
switch(attribute.value) {
case 'red':
return true;
case 'blue':
return true;
default:
return false;
}
}
},
},
attributes: {
number: {
type: 'string',
required: true,
filter: true
},
color: {
type: 'string',
required: true,
filter: true
}
}
};
Of course, in normal Sails.js behaviour, "attribute" would not be the attribute, but the value of the attribute. (And attribute.value was just an example, meaning, I want the attribute value in there).
So, I want attribute to be the actual attribute that called the validation rule. Is this possible with Sails? I mean, I could write a function for each field in the model, but it would be nice to have a function that fits them all (I have many of them).
Thanks.
Ok so I will answer your question, but this may not be exactly what you want. An attribute can have an "enum" which is how we'd achieve your end goal:
attributes: {
state: {
type: 'string',
enum: ['pending', 'approved', 'denied']
}
}
But I will assume that this code is just a contrived example. Here's a way that I think would work.
module.exports = {
types: {
filter: function(attribute) {
if (attribute === 'number') {
switch(attribute.value) {
case 'one':
return true;
case 'two':
return true;
default:
this.validationErrors.push(attribute);
return false;
}
} else if (attribute === 'color') {
switch(attribute.value) {
case 'red':
return true;
case 'blue':
return true;
default:
this.validationErrors.push(attribute);
return false;
}
}
},
},
attributes: {
validationErrors:(function(){
var errors = [];
return {
push:function(attr){
errors.push(attr);
},
get:function(){
return errors;
}
};
})(),
number: {
type: 'string',
required: true,
filter: true
},
color: {
type: 'string',
required: true,
filter: true
}
}
};
Edit:Used an attribute method instead of an attribute
There are potentially a couple problems with this answer. I'm not sure how waterline handles "this" within these custom type functions. Is "this" bound to the model? Or the instance of the model we're creating? There's a lot of questions to be asked here but maybe this can give you some ideas and create a discussion.

Resources