Angular2 forms : validator with interrelated fields - javascript

Given a form where one can enter either a city name or its latitude and longitude.
The form would validate if city name is filled OR if both latitude AND longitude are filled. Latitude and longitude, if filled, must be numbers.
I could create a FormGroup with those three fields and do one custom validators...
function fatValidator(group: FormGroup) {
// if cityName is present : is valid
// else if lat and lng are numbers : is valid
// else : is not valid
}
builder.group({
cityName: [''],
lat: [''],
lng: ['']
},
{
validators: fatValidator
});
...but I would like to take advantage of validators composition (e.g testing latitude and longitude to be valid numbers at the fields level in one validator and test the interrelation at the group level in another validator).
I have tested several options but I am stuck with the fact that a group is valid if all its fields are valid. The following construction seems not to be the proper way to approach the problem :
function isNumber(control: FormControl) { ... }
function areAllFilled(group: FormGroup) { ... }
function oneIsFilledAtLeast(group: FormGroup) { ... }
builder.group({
cityName: [''],
localisation: builder.group({
lat: ['', Validators.compose([Validators.minLength(1), isNumber])],
lng: ['', Validators.compose([Validators.minLength(1), isNumber])]
},
{
validators: areAllFilled
})
},
{
validators: oneIsFilledAtLeast
});
How would you do that with Angular2 Is it even possible ?
EDIT
Here is an example of how the fatValidator could be implemented. As you can see it is not reusable and harder to test than composed validators :
function fatValidator (group: FormGroup) {
const coordinatesValidatorFunc = Validators.compose([
Validators.required,
CustomValidators.isNumber
]);
const cityNameControl = group.controls.cityName;
const latControl = group.controls.lat;
const lngControl = group.controls.lng;
const cityNameValidationResult = Validators.required(cityNameControl);
const latValidationResult = coordinatesValidatorFunc(latControl);
const lngValidationResult = coordinatesValidatorFunc(lngControl);
const isCityNameValid = !cityNameValidationResult;
const isLatValid = !latValidationResult;
const isLngValid = !lngValidationResult;
if (isCityNameValid) {
return null;
}
if (isLatValid && isLngValid) {
return null;
}
if (!isCityNameValid && !isLatValid && !isLngValid) {
return { cityNameOrCoordinatesRequired: true, latAndLngMustBeNumbers: true };
}
return Object.assign({},
{ cityName: cityNameValidationResult },
{ lat: latValidationResult },
{ lng: lngValidationResult }
);
}

Using the final release or new of Angular, I have written a reusable method to add a Conditional Required- or other Validation -to a given set of Controls.
export class CustomValidators {
static controlsHaveValueCheck(controlKeys: Array<string>, formGroup: FormGroup): Array<boolean> {
return controlKeys.map((item) => {
// reset any errors already set (ON ALL GIVEN KEYS).
formGroup.controls[item].setErrors(null);
// Checks for empty string and empty array.
let hasValue = (formGroup.controls[item].value instanceof Array) ? formGroup.controls[item].value.length > 0 :
!(formGroup.controls[item].value === "");
return (hasValue) ? false : true;
});
}
static conditionalAnyRequired(controlKeys: Array<string>): ValidatorFn {
return (control: FormControl): {[key: string]: any} => {
let formGroup = control.root;
if (formGroup instanceof FormGroup) {
// Only check if all FormControls are siblings(& present on the nearest FormGroup)
if (controlKeys.every((item) => {
return formGroup.contains(item);
})) {
let result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);
// If any item is valid return null, if all are invalid return required error.
return (result.some((item) => {
return item === false;
})) ? null : {required: true};
}
}
return null;
}
}
}
This can be used in your code like this:
this.form = new FormGroup({
'cityName': new FormControl('',
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])),
'lat': new FormControl('',
Validators.compose([Validators.minLength(1),
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng']))),
'lng': new FormControl('',
Validators.compose([Validators.minLength(1),
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])))
})
This would make any of 'city', 'lat' or 'lng' required.
Additionally, if you wanted either 'city' or 'lat' and 'lng' to be required you can include an additional validator such as this:
static conditionalOnRequired(conditionalControlKey: string, controlKeys: Array<string>): ValidatorFn {
return (control: FormControl): {[key: string]: any} => {
let formGroup = control.root;
if (formGroup instanceof FormGroup) {
if (controlKeys.every((item) => {
return formGroup.contains(item);
}) && formGroup.contains(conditionalControlKey)) {
let firstControlHasValue = (formGroup.controls[conditionalControlKey].value instanceof Array) ? formGroup.controls[conditionalControlKey].value.length > 0 :
!(formGroup.controls[conditionalControlKey].value === ""),
result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);
formGroup.controls[conditionalControlKey].setErrors(null); // Also reset the conditional Control...
if (firstControlHasValue && formGroup.controls[conditionalControlKey].value !== false) {// also checks for false (for unchecked checkbox value)...
return (result.every((invalid) => {
return invalid === false;
})) ? null : {required: true};
}
}
}
return null;
}
}
This method will make a set of form controls 'required' based on the value of the conditionalControlKey, i.e. if conditionalControlKey has a value all other controls in controlKeys Array are not required, otherwise the all are required.
I hope this isn't too convoluted for anyone to follow- I am sure these code snippets can be improved, but I feel they aptly demonstrate one way of going about this.

Related

Using custom Angular Material Sort?

I want to create a dropdown (or mat-select) to use as a sorting mechanism instead of the Angular Material Sort Header. So, if I for example click on the 'username' inside the dropdown, I want the table to sort by the username (instead of clicking on the header).
How can I do it? Any documentation online on how to achieve this?
Thank you for any help.
As required, I attach some code:
ngOnInit(): void {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(""),
map((value) => this._filter(value))
);
}
ngAfterViewInit() {
this.providersAdmin.sort = this.sort;
}
getAllAdmins() {
this.isLoading = true;
this.homeService.getAllAdmins().subscribe(
(response) => {
this.admins = response;
this.providersAdmin = new MatTableDataSource(this.admins);
this.isLoading = false;
},
(error) => {}
);
}
sortTableBy(event: any) {
const sortState: Sort = {
active: "username",
direction: "desc",
};
this.sort.active = sortState.active;
this.sort.direction = sortState.direction;
this.sort.sortChange.emit(sortState);
console.log(event);
}
The sortTableBy method is the one I found on here but nothing happens.
I added matSort on the mat-table and I added mat-sort-header on the header cell.
EDIT:
Hi, I managed to fix the problem by writing the following:
sortTableBy(event: any) {
const sortState: Sort = {
active: "username",
direction: "desc",
};
this.sort.active = sortState.active;
this.sort.direction = sortState.direction;
this.sort.sortChange.emit(sortState);
this.providersAdmin.sort = this.sort;
}
There is an example for you:
Exmaple
Your sort function has a wrong implementation, this is work for me:
sortData(fieldName: string) {
if (!fieldName) {
return;
}
const sortState: MatSortable = {
id: fieldName,
start: 'desc',
disableClear: true
};
this.sort.sort(sortState);
}
I am going to set up an example which you can adapt easily:
compare(a: number | string, b: number | string, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
sortData() {
let isAsc = this.sort.direction != "" ?
event.direction == SortDirection.asc :
true;
let data = this.dataSource.data.slice();
data.sort((a, b) => {
switch (this.myChosenSort) {
case 'healthCareCenterName':
return this.compare(a.healthCareCenterName, b.healthCareCenterName, isAsc);
case 'address':
return this.compare(a.address, b.address, isAsc);
case 'contact':
return this.compare(a.contact, b.contact, isAsc);
default:
return 0;
}
});
this.dataSource = new MatTableDataSource<ServiceProviderTable>(data);
}
To change the sort.direction you need to play around a little bit with the code, maybe directly from the dropdown and hardcoding the isAsc when calling the compare method, depending on the value of the this.myChosenSort.

Why does my form input only register one character after re-render?

On initial render, the input components acts as normal. However after it is unfocused and refocused again, it will only accept one character. This also happens if I reset the form.
I created a FormValidationStateManager class to reuse Joi validation logic that is needed.
Form.jsx
class Form extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.validation = new FormValidationStateManager();
}
handleSubmit(e) {
e.preventDefault();
var result = this.validation.validateAll();
this.setState({
// need to do this, trigger render after validations
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
}, function() {
if (result.valid) {
this.props.onSubmit(result.data);
}
}.bind(this));
}
resetForm(e) {
e.preventDefault();
this.validation.clearForm();
this.setState({
// need to do this, trigger render after validations,
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
});
}
render() {
// TODO implement logic for rendering name from API call
let name = "John Doe";
return (
<form class='info-form' onSubmit={this.handleSubmit} autocomplete='off'>
<div class='info-block-section-title'>
<h2>Welcome {name}</h2>
<h4>Some nicely worded few sentences that let's the user know they just need to fill out a few bits of information to start the process of getting their student loans paid down!</h4>
</div>
<br />
<div class='info-block-section-body'>
<InfoFieldset validation={this.validation} />
</div>
<div class='info-form-button'>
<FormButton label="Continue" />
<FormButton type="button" onClick={this.resetForm.bind(this)} label="Reset" />
</div>
</form>
)
}
}
export default Form;
Fieldset
class Fieldset extends Component {
constructor(props) {
super(props);
this.state = {
secondaryEmailVisible: false
};
this.handleToggle = this.handleToggle.bind(this);
this.validation = this.props.validation || new FormValidationStateManager();
this.validation.addRules(this.validatorRules.bind(this));
}
validatorRules() {
var _rules = {
// TODO validate format of date, address and zip
date_of_birth: Joi.date().label("Date of Birth").required(),
address_street: Joi.string().label("Street Address").required(),
address_zip: Joi.string().label("Zip Code").required(),
password_confirmation: Joi.any()
.valid(Joi.ref('password'))
.required()
.label('Password Confirmation')
.messages({
"any.only": "{{#label}} must match password"
}).strip(),
password: Joi.string()
.pattern(/\d/, 'digit')
.pattern(/^\S*$/, 'spaces')
.pattern(/^(?!.*?(.)\1{2})/, 'duplicates')
.pattern(/[a-zA-z]/, 'alpha')
.required()
.min(CONSTANTS.PASSWORD_MIN)
.label('Password')
.messages({
"string.min": PASSWORD_HINT,
"string.pattern.name": PASSWORD_HINT
})
}
if (this.state.secondaryEmailVisible) {
_rules['secondary_email'] = Joi.string()
.label("Secondary Email")
.email({ tlds: false })
.required();
}
return _rules;
}
handleToggle() {
this.setState(state => ({
secondaryEmailVisible: !state.secondaryEmailVisible
}));
}
render() {
return(
<div class='info-fieldset'>
{/* for skipping chrome browser autocomplete feature
<input type='text' style={{ display: 'none '}} />
<input type='password' style={{ display: 'none '}} />
*/ }
<strong>Primary email for login: work#example.com</strong>
<FormElementToggle
field="secondary"
label={"Add another email for notifications?"}
onChange={this.handleToggle} />
<div class='info-form-element'>
<DisplayToggle remove={true} show={this.state.secondaryEmailVisible}>
<FormElementInput
field='secondary_email'
label="Secondary Email"
validation={this.validation}
placeholder='john#example.com' />
</DisplayToggle>
<FormElementInput
field='password'
type='password'
label={(
<div>
Password
<Tooltip
placement='top'
trigger={['hover']}
overlay={PASSWORD_HINT}>
<span><i className='fa fa-info-circle'></i></span>
</Tooltip>
</div>
)}
placeholder='********'
validation={this.validation} />
<FormElementInput
key={1}
field='password_confirmation'
type='password'
label="Password Confirmation"
placeholder='********'
validation={this.validation} />
<FormElementInput
field='date_of_birth'
label="Date of Birth"
validation={this.validation}
placeholder='MM/DD/YYYY' />
<FormElementInput
field='address_street'
label="Street Address"
validation={this.validation}
placeholder='123 Example St' />
<FormElementInput
field="address_zip"
label="Zip Code"
validation={this.validation}
placeholder='01234' />
</div>
</div>
)
}
}
export default Fieldset;
FormElementInput.jsx
class FormElementInput extends Component {
constructor(props) {
super(props);
this.input = React.createRef();
this.cachedErrors = {};
this.cachedValue = null;
this.state = { focused: false };
this.validation = this.props.validation || new FormValidationStateManager();
}
componentDidUpdate(prevProps, prevState) {
}
shouldComponentUpdate(nextProps, nextState) {
var filter = function(val, key, obj) {
if (_.isFunction(val)) {
return true;
}
// Certain props like "label" can take a React element, but those
// are created dynamically on the fly and they have an random internal UUID,
// which trips the deep-equality comparision with a false-positive.
// This is wasted cycles for rendering.
if (React.isValidElement(val)) {
return true;
}
// validation is a complex obj that we shouldn't be caring about
if (_.contains(['validation'], key)) {
return true;
}
};
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (!_.isEqual(this.cachedErrors, this.getErrors())) {
return true;
}
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (this.cachedValue !== this.getDisplayValue()) {
return true;
}
return !_.isEqual(_.omit(nextProps, filter), _.omit(this.props, filter)) || !_.isEqual(this.state, nextState);
}
setValue(value) {
console.error('deprecated - can not set value directly on FormElementInput anymore');
throw 'deprecated - can not set value directly on FormElementInput anymore';
}
isDollarType() {
return this.props.type === 'dollar';
}
isRateType() {
return this.props.type === 'rate';
}
getType() {
if (this.isDollarType() || this.isRateType()) {
return 'text';
} else {
return this.props.type;
}
}
getPrefix() {
if (this.isDollarType()) {
return _.compact([this.props.prefix, '$']).join(' ');
} else {
return this.props.prefix;
}
}
getPostfix() {
if (this.isRateType()) {
return _.compact([this.props.postfix, '%']).join(' ');
} else {
return this.props.postfix;
}
}
getDisplayValue() {
var value = this.props.value || this.validation.getFormValue(this.props.field);
// while DOM input is focused, just return the same value being typed
if (this.state.focused) {
return value;
}
if (this.isDollarType()) {
if (_.isUndefined(value)) {
return value;
}
// keep in sync with the validation check in getOnChange()
// even if this is different - it wont break - cause accounting.js handles gracefully
else if (_.isNumber(value) || !value.match(/[a-z]/i)) {
return accounting.formatMoney(value, '', 2);
} else {
return value;
}
} else if (this.isRateType()){
if (_.isUndefined(value)) {
return '';
}
return accounting.formatNumber(value, 3)
} else {
return value;
}
}
getErrors() {
return this.props.errors || this.validation.getValidationMessages(this.props.field);
}
onBlur(event) {
this.validation.validate(this.props.field);
this.setState({ focused: false });
}
onFocus(event) {
this.setState({ focused: true });
}
onChange(event) {
if (this.isDollarType()) {
// only accept if the input at least resembles a currency
// accounting.parse already strips alot of characters and does this silently
// we want to be a little less strict than accounting.parse since we need to allow more
// different types of inputs +-,. and also let accounting.parse do its job
// very basic check here for a-z characters
if (!event.target.value.match(/[a-z]/i)) {
var parsedValue = accounting.parse(event.target.value);
event._parsedValue = parsedValue;
}
}
let _value = event._parsedValue || event.target.value;
console.log(this.input)
this.validation.setFormValue(this.props.field, _value);
}
_eventHandlers(field) {
return {
onChange: this.onChange.bind(this),
onBlur: this.onBlur.bind(this),
onFocus: this.onFocus.bind(this)
};
}
_mergeEventHandlers(p1, p2) {
var events = [
'onChange',
'onBlur',
'onFocus'
];
var r1 = {};
events.map(function(e) {
r1[e] = FuncUtils.mergeFunctions(p1[e], p2[e]);
}.bind(this));
return r1;
}
render() {
let _errors = this.cachedErrors = this.getErrors();
let _value = this.cachedValue = this.getDisplayValue();
let attrs = {
id: this.props.htmlId || ('field_' + this.props.field),
className: classnames({
'error': _errors && _errors.length
}),
type: this.getType(),
placeholder: this.props.placeholder,
autoComplete: "false",
disabled: this.props.disabled,
readOnly: this.props.disabled,
value: _value,
ref: this.input,
};
attrs = _.extend(attrs, this._mergeEventHandlers(this.props, this._eventHandlers()));
const prefix = this.getPrefix();
const postfix = this.getPostfix();
return (
<FormElementWrapper
type="input"
field={this.props.field}
errors={_errors}
label={this.props.label}>
<div className="form-element-input">
<DisplayToggle remove={true} hide={!prefix}>
<span className="prefix">{prefix}</span>
</DisplayToggle>
<div className={classnames({
"has-prefix": !!prefix,
"has-postfix": !!postfix
})}>
<input {...attrs} />
</div>
<DisplayToggle remove={true} hide={!postfix}>
<span className="postfix">{postfix}</span>
</DisplayToggle>
</div>
<div className="form-error">
{_errors.map((msg, idx) => {
return (<FormError key={idx}>{msg}</FormError>)
})}
</div>
</FormElementWrapper>
)
}
};
FormElementInput.PropTypes = {
field: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
value: PropTypes.any,
label: PropTypes.any,
errors: PropTypes.array,
type: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onKeyUp: PropTypes.func,
tooltip: PropTypes.string,
prefix: PropTypes.string,
postfix: PropTypes.string,
disabled: PropTypes.bool,
disableTrimming: PropTypes.bool
}
FormElementInput.defaultProps = {
type: 'text',
placeholder: 'Enter Value',
tooltip: undefined,
prefix: undefined,
postfix: undefined,
disabled: false,
disableTrimming: false
}
export default FormElementInput;
FormValidationStateManager.js
class FormValidationStateManager {
// default state, validation ref obj
constructor(defaultState) {
this.rules = [];
this.state = defaultState || {};
this.errors = {};
this.dirty = false;
}
getFormValue(key) {
return this.state[key];
}
setFormValue(key, value) {
this.dirty = true;
this.state[key] = value;
}
setFormState(newState) {
this.state = _.extend({}, newState);
}
addRules(rules) {
this.rules.push(rules);
}
isFormDirty(){
return this.dirty;
}
/**
* Clear all previous validations
*
* #return {void}
*/
clearValidations() {
this.dirty = false;
this.errors = {};
}
clearForm() {
this.state = _.mapObject(this.state, function(v, k) {
return '';
});
this.clearValidations();
}
/**
* Check current validity for a specified key or entire form.
*
* #param {?String} key to check validity (entire form if undefined).
* #return {Boolean}.
*/
isValid(key) {
return _.isEmpty(this.getValidationMessages(key));
}
// call within submit
validateAll() {
return this.validate();
}
/**
* Method to validate single form key or entire form against the component data.
*
* #param {String|Function} key to validate, or error-first containing the validation errors if any.
* #param {?Function} error-first callback containing the validation errors if any.
*/
validate(key, callback) {
if (_.isFunction(key)) {
callback = key;
key = undefined;
}
var schema = this._buildRules();
var data = this.state;
var result = this._joiValidate(schema, data, key);
if (key) {
this.errors[key] = result.errors[key];
} else {
this.errors = result.errors;
}
return {
valid: this.isValid(key),
errors: this.errors,
data: result.data
};
}
_buildRules() {
var allRules = this.rules.map(function(r) {
// Rules can be functions and be dynamically evalulated on-the-fly,
// to allow for more complex validation rules.
if (_.isFunction(r)) {
return r();
} else {
return r;
}
});
// collapse all the rules into one single object that will
// be evaluated against `this.state`
return Joi.object(_.extend({}, ...allRules));
}
/**
* Get current validation messages for a specified key or entire form.
*
* #param {?String} key to get messages, or entire form if key is undefined.
* #return {Array}
*/
getValidationMessages(key) {
let errors = this.errors || {};
if (_.isEmpty(errors)) {
return [];
} else {
if (key === undefined) {
return _.flatten(_.keys(errors).map(error => {
return errors[error] || []
}));
} else {
return errors[key] ? errors[key].map(he.decode) : [];
}
}
}
_joiValidate(joiSchema, data, key) {
joiSchema = joiSchema || Joi.object();
data = data || {};
const joiOptions = {
// when true, stops validation on the first error, otherwise returns all the errors found. Defaults to true.
abortEarly: false,
// when true, allows object to contain unknown keys which are ignored. Defaults to false.
allowUnknown: true,
// remove unknown elements from objects and arrays. Defaults to false
stripUnknown: true,
errors: {
escapeHtml: true,
// overrides the way values are wrapped (e.g. [] around arrays, "" around labels).
// Each key can be set to a string with one (same character before and after the value)
// or two characters (first character before and second character after), or false to disable wrapping:
label: false,
wrap: {
label: false,
array: false
}
},
messages: {
"string.empty": "{{#label}} is required",
"any.empty": "{{#label}} is required",
"any.required": "{{#label}} is required"
}
};
const result = joiSchema.validate(data, joiOptions);
let errors = this._formatErrors(result);
if (key) {
errors = _.pick(errors, key);
}
return {
errors: errors,
data: result.value
};
}
_formatErrors(joiResult) {
if (joiResult.error) {
return _.reduce(joiResult.error.details, (memo, detail) => {
// According to docs:
// - "detail.path": ordered array where each element is the accessor to the value where the error happened.
// - "detail.context.key": key of the value that erred, equivalent to the last element of details.path.
// Which is why we get the last element in "path"
var key = _.last(detail.path);
if (!Array.isArray(memo[key])) {
memo[key] = [];
}
if (!_.contains(memo[key], detail.message)) {
memo[key].push(detail.message);
}
return memo;
}, {});
} else {
return {};
}
}
}
export default FormValidationStateManager;
I think I got it. I changed value: _value to defaultValue: _value for the attributes in FormElementInput.jsx
saw it here
Anyone know why this fixed it under the hood?

Having problem in accessing values from state when traversing an array

i'm trying to traverse an Array and perform some operation. But cann't access the values as i want to.
inputObj.map(el => {
const msg = this.state.validation[el.id].message;
const msg2 = this.state.validation['name'].message;
})
Here, el.id can be name, dob or address. When i use this.state.validation[el.id].message;, it shows TypeError: Cannot read property 'message' of undefined. But if i hardcode it like this.state.validation['name'].message;, it works fine. when comparing both el.id and 'name', they have same datatype and same value. So, why having problem when using el.id instead of hardcoding it.
NB: i'm using reactjs.
Edit:
this.state:
this.state = {
super(props);
this.validator = new FormValidator([
{
field: 'name',
method: 'isEmpty',
validWhen: false,
message: 'Name is required'
},
...
]);
orderForm: {
name: {
elementType: 'input',
elementConfig: ''
},
...
}
validation: this.validator.setValid() // it will be the updated upon submitting form on submitHandler by calling validate() from FormValidator
}
inputObj:
const inputObj= [];
for(let key in this.state.orderForm){
inputObj.push({
id : key,
config: this.state.orderForm[key]
});
}
FormValidator
import validator from 'validator';
class FormValidator {
constructor(rules){
this.rules = rules;
}
setValid(){
const validation = {};
this.rules.map(rule => (
validation[rule.field] = {isValid: true, message: ''}
));
return {isValid: true, ...validation};
}
validate(form){
let validation = this.setValid();
this.rules.forEach( rule => {
if (validation[rule.field].isValid){
const field = form[rule.field].toString();
const args = rule.args || [];
const validationMethod = typeof rule.method === 'string' ?
validator[rule.method] : rule.method;
if (validationMethod(field, ...args, form) !== rule.validWhen){
validation[rule.field] = {isValid: false, message: rule.message};
validation.isValid = false;
}
}
});
return validation;
}
}
export default FormValidator;
You can check if this.state.validation[el.id] is defined before getting message key.
Like that you can't get fatal error.
inputObj.map(el => {
this.state.validation[el.id] && (
const msg = this.state.validation[el.id].message
);
})

Unit test own validation function in Angular

I'm new to testing and starting out writing a own validation method for my Angular app. But I cant figure it out how to test it with Karma. What is the right way to set a value and test it against my validation method?
I get following error
Property 'valid' does not exist on type 'void'.
Please be patient with me but I don't grasp why I get this error.
The form
this.carForm = this.fb.group({
car: this.fb.array([
this.fb.group({
id: car.id,
properties: this.fb.group({
color: 'blue',
vinNumber: ['', ValidationService.isValidVinNumber]
})
})
]),
registrationDate: '2011-11-12',
howManyOwners: car.previousOwners,
})
});
this.carForm.valueChanges.subscribe(value => {
});
The validation method
static isValidVinNumber(formControl: FormControl) {
if (formControl && formControl.value) {
if (formControl.value.length !== 14) {
return false;
}
} else {
return null;
}
}
My test case not working
beforeEach(() => {
fixture = TestBed.createComponent(CarComponent);
component = fixture.componentInstance;
}
........
it('should be possible to post empty vin number', () => {
const iccNumber = component.carForm.get(['car', 0, 'properties', 'vinNumber']);
expect(iccNumber.valid).toBeTruthy();
});

Angular 4 - Custom validator with dynamic parameter value

I have written a custom validator that checks if a date is above a certain minimum date.
the code looks like this:
export function validateMinDate(min: Date): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min;
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
}
I initate my form like this
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required, validateMinDate(this.details.From)]]
});
I can see that the validator is being applied, but when I console.log() my min value in the validator I can see that it equal null.
this.details.From starts at null when I initiate the form, so I assume the parameter is not dynamic and just takes the value when the form is being set?
How can I make sure the min date is being updated when a users picks a from date, and thus changes the value of this.details.From?
#Nicolas Validator takes value only once it does not look for it changes. So we can change parameters value dynamically by assigning new validator on value changes. In your case you can do in this way:
onChanges(){
var self=this;
this.definitionForm.get('from').valueChanges.subscribe(val => {
this.from=val;
this.definitionForm.controls['to'].
setValidators(Validators.compose([Validators.required,
TimeValidators.isTimeAfter(this.from)]));
})}
Here i created a separate custom validator for comparing the time. You can either use this or modify yours
import { FormControl, Validators,ValidatorFn, AbstractControl} from '#angular/forms';
export class TimeValidators extends Validators{
static isTimeBefore(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) || c.value > timeStr || c.value== timeStr){
return {
'isTimeBefore':true
}
}
return null;
}
}
static isTimeAfter(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) && (c.value < timeStr || c.value == timeStr)){
return {
'isTimeAfter':true
}
}
return null;
}
}
}
Call onChanges() function after you initialize your definitionForm FormGroup.
You can modify your custom validator to take function as parameter like
export function validateMinDate(min: DateFunc): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min();
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
and initiate the form like this
this.definitionForm = this.fb.group({
...
"to": [this.details.To, [Validators.required, validateMinDate(() => this.details.From)]]
});
the DateFunc is just a type that you can create like
export interface DateFunc{
(): Date
}
and expect this.details.From to return value of type Date
How I see it, would be to apply the validator on the form group, or if you have a large form, I suggest you create a nested group for from and to and apply the validator on that, since otherwise this custom validator would be fired whenever any changes happen to form. So it would mean to update the validator and formgroup to such:
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required]]
}, {validator: validateMinDate()});
export function validateMinDate(): ValidatorFn {
return (c: AbstractControl) => {
if(c) {
let isValid = c.get('to').value >= c.get('from').value;
if (isValid) {
return null;
} else {
return {validateMinDate: true};
}
}
};
}
Of course there are other options as well, such as listening for change event and then do the check of the dates, if not valid, use setErrors on form.
As an alternative to the given answers, at least in Angular 6 you can pass the component as ValidatorFn argument, so you can use its properties at runtime to validate your form control.
Here is a working example:
Component declarations:
#Component({
templateUrl: './create-application.component.html'
})
export class CreateApplicationComponent implements OnInit, OnDestroy {
viewMode: ViewMode;
application: Application;
form: FormGroup;
enterprisesData: any[] = [];
Form:
this.form = this.formBuilder.group({
name: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.name : '',
disabled: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT)
},
{
validators: [ Validators.required, Validators.minLength(3), Validators.maxLength(80) ],
updateOn: 'blur'
}
),
area: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.area : '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ Validators.required ]
}
),
country: new FormControl(
{
value: '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ applicationCountryEnterprisesValidator(this) ]
}
)
});
ValidatorFn:
export function applicationCountryEnterprisesValidator(component: CreateApplicationComponent): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
return (component.enterprisesData && component.enterprisesData.length > 0) ? null : { noEnterprisesSelected: true };
};
}

Categories