Angular2: how to create custom validator for FormGroup? - validation

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;
}
};

Related

Array validation not working for PATCH in mongoose

This is my mongoose schema. but when i send PATCH req from client. the options (array) validation not work. but others field's validation work.
I search in online but dont get the problem. How can I slove it. Thank you.
const optionsValidator = function (options) {
console.log("Validating options...");
const MinLength = 2;
const MaxLength = 6;
const MCQCode = 0;
// checking options are required or not for the question;
if (this.questionType !== MCQCode) {
throw new Error("Options are required only for MCQ");
}
if (options.length < MinLength) {
throw new Error(`At least ${MinLength} options are required`);
}
if (options.length > MaxLength) {
throw new Error(`Maximum ${MaxLength} options can be created`);
}
// make options lower case before checking uniqueness of the options
const lowerOptions = options.map((option) => option.toLowerCase());
if (lowerOptions.length !== new Set(lowerOptions).size) {
throw new Error("Options are not unique");
}
// options are validated
return true;
};
const questionSchema = new Schema({
quesId: { type: String, required: [true, "Id_required"] },
title: { type: String, required: [true, "title_required"] },
questionType: {
type: Number,
default: 0,
},
options: {
type: [String],
default: undefined,
required: function () {
return this.questionType === 0 && !this.options;
},
validate: {
validator: optionsValidator,
message: (props) => props.reason.message,
},
},
});
const updatedData = { ...questionData };
let optionsData;
if (updatedData.options) {
data = await Question.findById(id);
optionsData = {
$push: { options: { $each: updatedData.options } },
};
delete updatedData.options;
}
exports.patchQuestion = async (id, questionData) => {
return (result = await Question.findOneAndUpdate(
{ _id: id },
{ ...optionsData, $set: updatedData },
{ new: true, runValidators: true }
));
}
The is the PATCH request send form client.
{ "options": ["A","A"] }

How to create a custom validator to check password and confirm password mismatch

I am working on Angular 11 project I want create a custom validator in reactive form. Here is the code
but it gives following errors
if I omit .value from control.get('password').value error is not shown but validator is not working.
How can I solve this problem.
change control.get('password').value to control.value['password']
change control.get('ConfirmPassword').value to control.value['confirmPassword']
You have problem because , object is possibly null. It's necessary create condition when is object password or confirm Password null return null.
So I am sharing my solution . This working for me.
Code is here
this.userForm = new FormGroup(
{
name: new FormControl('', [
Validators.required,
Validators.email,
Validators.minLength(4),
Validators.maxLength(8),
]),
email: new FormControl('', [
Validators.required,
Validators.email,
Validators.pattern("^[a-z0-9._%+-]+#[a-z0-9.-]+\\.[a-z]{2,4}$")
]),
phone: new FormControl('', [
Validators.required,
Validators.pattern("^((\\+91-?)|0)?[0-9]{10}$") // 10 characters
]),
password: new FormControl('', [
Validators.required,
Validators.minLength(8),
// this.passwordStrengthValidator() // another Validator
]),
password2: new FormControl('', Validators.required),
message: new FormControl('', Validators.required),
},
//this.passwordMatch('password', 'password2') // working
{
validators: this.passwordMatch('password', 'password2') // working
}
);
passwordMatch(password: string, confirmPassword: string): ValidatorFn {
return (formGroup: AbstractControl): { [key: string]: any } | null => {
const passwordControl = formGroup.get(password);
const confirmPasswordControl = formGroup.get(confirmPassword);
if (!passwordControl || !confirmPasswordControl) {
return null;
}
if (
confirmPasswordControl.errors &&
!confirmPasswordControl.errors.mustMatch
) {
return null;
}
if (passwordControl.value !== confirmPasswordControl.value) {
confirmPasswordControl.setErrors({ mustMatch: true });
return { mustMatch: true }
} else {
confirmPasswordControl.setErrors(null);
return null;
}
};
}

Vue3 composition API refactor computed favoritesRecipes

I am new to composition API with vue3. I have created that computed property and I would like to have that computed variable in a different file, I'm not sure if I should create a new component or I could achieve it from a js file.
Here is the component working (I did it with setup()):
export default {
name: "Recipes",
setup() {
const state = reactive({
recipes: [],
sortBy: "alphabetically",
ascending: true,
searchValue: "",
});
const favoritesRecipes = computed(() => {
let tempFavs = state.recipes;
// Show only favorites
if (state.heart) {
tempFavs = tempFavs.filter(item => {
return item.favorite;
});
}
return tempFavs;
...
});
...
}
return {
...toRefs(state),
favoriteRecipes
}
// end of setup
}
You can split it into two files
state.js
export const state = reactive({
recipes: [],
sortBy: "alphabetically",
ascending: true,
searchValue: "",
});
export const favoriteRecipes = computed(() => {
let tempFavs = state.recipes;
// Show only favorites
if (state.heart) {
tempFavs = tempFavs.filter(item => {
return item.favorite;
});
}
return tempFavs;
})
and recipes.vue
import { state, favoriteRecipes } from "state.js";
export default {
name: "Recipes",
setup() {
return {
...toRefs(state),
favoriteRecipes,
};
},
};
But this will make the state persistent, so if you have multiple components, they will all have the same favoriteRecipes and state values.
If you want them to be unique for each component...
state.js
export const withState = () => {
const state = reactive({
recipes: [],
sortBy: "alphabetically",
ascending: true,
searchValue: "",
});
const favoriteRecipes = computed(() => {
let tempFavs = state.recipes;
// Show only favorites
if (state.heart) {
tempFavs = tempFavs.filter((item) => {
return item.favorite;
});
}
return tempFavs;
});
return { state, favoriteRecipes };
};
and recipes.vue
import { withState } from "state.js";
export default {
name: "Recipes",
setup() {
const {state, favoriteRecipes} = withState()
return {
...toRefs(state),
favoriteRecipes,
};
},
};

How to load AJAX and update state in componentDidUpdate?

I have a component that renders a list of credit cards. It receives a contract as a prop. A contract id can have multiple related credit cards. This list is fetched from the API via AJAX. The component can be visible or hidden on screen.
I fetch the list of credit cards via AJAX in state cards which is a tuple (key/value): [number, ICreditCardRecord[]]. That way I can keep track of which contract id the list I have in state belongs to.
So in componentDidUpdate I check if the component is not hidden and if the selected contract matches the one on which I saved the list. If not, I fetch the list again for the new contract.
Problem is, it triggers an endless loop. I read that it's not possible to setState inside componentDidUpdate as it triggers a re-render which triggers the function again which ... (endless loop).
So how can I do this? BTW I also use state to manage whether to show loading overlay or not (isLoading: boolean). I update this state prop when ever I start/stop loading.
import React, { Component } from 'react'
import { IApiVehicleContract, ICreditCardRecord } from '#omnicar/sam-types'
import * as api from 'api/api'
import { WithStyles, withStyles } from '#material-ui/core'
import CreditCard from 'components/customer/Contract/Details/CreditCard'
import NewCreditCard from 'components/customer/Contract/Details/NewCreditCard'
import AddCardDialog from 'components/customer/Contract/Details/AddCardDialog'
import { AppContext } from 'store/appContext'
import LoadingOverlay from 'components/LoadingOverlay'
import styles from './styles'
import alertify from 'utils/alertify'
import { t } from '#omnicar/sam-translate'
import { loadScript } from 'utils/script'
interface IProps extends WithStyles<typeof styles> {
contract: IApiVehicleContract | undefined
hidden: boolean
}
interface IState {
cards: [number, ICreditCardRecord[]]
showAddCreditCardDialog: boolean
isStripeLoaded: boolean
isLoading: boolean
}
class CustomerContractDetailsTabsCreditCards extends Component<IProps, IState> {
public state = {
cards: [0, []] as [number, ICreditCardRecord[]],
showAddCreditCardDialog: false,
isStripeLoaded: false,
isLoading: false,
}
public componentDidUpdate(prevProps: IProps, prevState: IState) {
const { cards } = this.state
const { contract, hidden } = this.props
// selected contract changed
const changed = contract && contract.serviceContractId !== cards[0]
// visible and changed
if (!hidden && changed) {
this.getCreditCards()
console.log('load')
}
}
public async componentDidMount() {
// load stripe
const isStripeLoaded = await loadScript('https://js.stripe.com/v3/', 'stripe-payment')
this.setState({ isStripeLoaded: !!isStripeLoaded })
}
public render() {
const { classes, contract, hidden } = this.props
const { cards, showAddCreditCardDialog, isLoading } = this.state
return (
<div className={`CustomerContractDetailsTabsCreditCards ${classes.root} ${hidden && classes.hidden}`}>
<React.Fragment>
<ul className={classes.cards}>
{cards[1].map(card => (
<CreditCard
className={classes.card}
key={card.cardId}
card={card}
onDeleteCard={this.handleDeleteCard}
onMakeCardDefault={this.handleMakeDefaultCard}
/>
))}
<NewCreditCard className={classes.card} onToggleAddCreditCardDialog={this.toggleAddCreditCardDialog} />
</ul>
<AppContext.Consumer>
{({ stripePublicKey }) => {
return (
<AddCardDialog
open={showAddCreditCardDialog}
onClose={this.toggleAddCreditCardDialog}
contract={contract!}
onAddCard={this.handleAddCard}
stripePublicKey={stripePublicKey}
isLoading={isLoading}
/>
)
}}
</AppContext.Consumer>
</React.Fragment>
<LoadingOverlay open={isLoading} />
</div>
)
}
private getCreditCards = async () => {
const { contract } = this.props
if (contract) {
// this.setState({ isLoading: true })
const res = await api.getContractCreditCards(contract.prettyIdentifier)
debugger
if (res) {
if (res.errorData) {
// this.setState({ isLoading: false })
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
console.error(res.errorData.message)
return
}
// sort list: put active first
const creditCards: ICreditCardRecord[] = res.data!.sort((a, b) => +b.isDefault - +a.isDefault)
const cards: [number, ICreditCardRecord[]] = [contract.serviceContractId, creditCards]
debugger
this.setState({ cards, isLoading: false })
}
}
}
private handleDeleteCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.deleteCreditCard(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// remove card from list
const creditCards = this.state.cards[1].filter(card => card.cardId !== cardId)
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card has been deleted'))
}
private handleMakeDefaultCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.makeCreditCardDefault(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// show new card as default
const creditCards = this.state.cards[1].map(card => {
const res = { ...card }
if (card.cardId !== cardId) {
res.isDefault = false
} else {
res.isDefault = true
}
return res
})
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card is now default'))
}
private toggleAddCreditCardDialog = () => {
this.setState({ showAddCreditCardDialog: !this.state.showAddCreditCardDialog })
}
private appendNewCardToList = (data: any) => {
const {cards: cardsList} = this.state
let creditCards = cardsList[1].map(card => {
return { ...card, isDefault: false }
})
creditCards = [data, ...creditCards]
const cards: [number, ICreditCardRecord[]] = [cardsList[0], creditCards]
this.setState({ cards })
// notify user
alertify.success(t('Credit card has been added'))
}
private handleAddCard = (stripe: stripe.Stripe) => {
// show spinner
this.setState({ isLoading: true })
const stripeScript = stripe as any
stripeScript.createToken().then(async (result: any) => {
if (result.error) {
// remove spinner
this.setState({ isLoading: false })
// notify user
alertify.warning(t('An error occurred... Please contact OmniCar if the problem persists. '))
return console.error(result.error)
}
// add credit card
const prettyId = this.props.contract ? this.props.contract.prettyIdentifier : ''
const req = await api.addCreditCard({ cardToken: result.token.id, isDefault: true }, prettyId)
if (req.errorData) {
alertify.warning(t('An error occurred while trying to add credit card'))
return console.error(req.errorData)
}
// remove spinner and close dialog
this.setState({ isLoading: false }, () => {
this.toggleAddCreditCardDialog()
this.appendNewCardToList(req.data)
})
})
}
}
export default withStyles(styles)(CustomerContractDetailsTabsCreditCards)

Angular 2 Custom Validator with Observable Parameter

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.

Resources