Skip to content

Commit 251fec5

Browse files
Support declaring your own validator functions
1 parent 5878f04 commit 251fec5

File tree

4 files changed

+177
-62
lines changed

4 files changed

+177
-62
lines changed

README.md

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
I believe Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy.
44

5+
## Table of contents
6+
7+
* [Installation](#installation)
8+
* [Basic usage](#basic-usage)
9+
* [Advanced validation declaration](#advanced-validation-declaration)
10+
* [Changing when validation messages are displayed](#changing-when-validation-messages-are-displayed)
11+
* [Declaring your own validator functions](#declaring-your-own-validator-functions)
12+
* [Edge use cases](#edge-use-cases)
13+
* [Handling custom HTML validation messages](#handling-custom-html-validation-messages)
14+
* [Using arv-validation-messages when not using `[formGroup]` or `formGroupName` attributes](#using-arv-validation-messages-when-not-using-[formGroup]-or-formGroupName-attributes)
15+
516
## Installation
617

718
To install this library, run:
@@ -131,6 +142,46 @@ export class AppModule { }
131142

132143
Note that `formSubmitted` can be undefined when it's not known if the form is submitted, due to the form tag missing a formGroup attribute.
133144

145+
## Declaring your own validator functions
146+
147+
Angular provides a limited set of validator functions. To declare your own validator functions _and_ combine it with this library use the `ValidatorDeclaration` class. It supports declaring validators with zero, one or two arguments.
148+
149+
**Note** that if your validator doesn't return an object as the inner error result, but e.g. a `boolean` such as in the examples below, then this will be replaced by an object that can hold the validation message. Thus in the first example below `{ 'hasvalue': true }` becomes `{ 'hasvalue': { 'message': 'validation message' } }`.
150+
151+
```ts
152+
const hasValueValidator = ValidatorDeclaration.wrapNoArgumentValidator(control => {
153+
return !!control.value ? null : { 'hasvalue': true };
154+
}, 'hasvalue');
155+
156+
const formControl = new FormControl('', hasValueValidator('error message to show'));
157+
```
158+
159+
```ts
160+
const minimumValueValidator = ValidatorDeclaration.wrapSingleArgumentValidator((min: number) => {
161+
return function(control: AbstractControl): ValidationErrors {
162+
return control.value >= min ? null : { 'min': true };
163+
};
164+
}, 'min');
165+
166+
const formControl = new FormControl('', minimumValueValidator(5, 'error message to show'));
167+
```
168+
169+
```ts
170+
const betweenValueValidator = ValidatorDeclaration.wrapTwoArgumentValidator((min: number, max: number) => {
171+
return function(control: AbstractControl): ValidationErrors {
172+
return control.value >= min && control.value <= max ? null : { 'between': true };
173+
};
174+
}, 'between');
175+
176+
const formControl = new FormControl('', betweenValueValidator(5, 10, 'error message to show'));
177+
```
178+
179+
Wrapping validator functions provided by other packages is also very simple:
180+
181+
```ts
182+
const minValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.min, 'min')
183+
```
184+
134185
## Edge use cases
135186

136187
### Handling custom HTML validation messages
@@ -183,10 +234,3 @@ Supplying FormControl instances instead of names is also supported:
183234
</arv-validation-message>
184235
</arv-validation-messages>
185236
```
186-
187-
## Future development
188-
189-
The following features are to be added or are under investigation:
190-
191-
* Creating your own validators and using them together with this library.
192-
* Providing interfaces for using other popular validation libraries within `angular-reactive-validation`.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ReactiveValidationModule } from './reactive-validation.module';
22
export { ReactiveValidationModuleConfiguration } from './reactive-validation-module-configuration';
33
export { Validators } from './validators';
4+
export { ValidatorDeclaration } from './validator-declaration';
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms';
2+
3+
/**
4+
* @dynamic
5+
*/
6+
export class ValidatorDeclaration {
7+
/**
8+
* Wraps your own validator functions for use with the angular-reactive-validation library.
9+
* @param validatorFn A function you want to wrap which can validate a control.
10+
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
11+
* @param message The message to display when a validation error occurs. A function can also be passed to determine
12+
* the message at a later time.
13+
*/
14+
static wrapNoArgumentValidator(validatorFn: ValidatorFn, resultKey: string):
15+
(message?: string | (() => string)) => ValidatorFn {
16+
return function(message?: string | (() => string)): ValidatorFn {
17+
return function(control: AbstractControl): ValidationErrors | null {
18+
return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, () => validatorFn, resultKey, message);
19+
};
20+
};
21+
}
22+
23+
/**
24+
* Wraps your own validator functions for use with the angular-reactive-validation library.
25+
* @param validatorFactoryFn A function which accepts a single argument and returns a ValidatorFn.
26+
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
27+
*/
28+
static wrapSingleArgumentValidator<TInput>(validatorFactoryFn: ((arg1: TInput) => ValidatorFn), resultKey: string):
29+
(arg1: TInput | (() => TInput), message?: string | ((arg1: TInput) => string)) => ValidatorFn {
30+
31+
return function (arg1: TInput | (() => TInput), message?: string | ((arg1: TInput) => string)): ValidatorFn {
32+
return function(control: AbstractControl): ValidationErrors | null {
33+
arg1 = ValidatorDeclaration.unwrapArgument(arg1);
34+
35+
return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, arg1);
36+
};
37+
};
38+
}
39+
40+
/**
41+
* Wraps your own validator functions for use with the angular-reactive-validation library.
42+
* @param validatorFactoryFn A function which accepts two arguments and returns a ValidatorFn.
43+
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
44+
*/
45+
static wrapTwoArgumentValidator<TInput1, TInput2>(validatorFactoryFn: ((arg1: TInput1, arg2: TInput2) => ValidatorFn), resultKey: string):
46+
(arg1: TInput1 | (() => TInput1), arg2: TInput2 | (() => TInput2), message?: string | ((arg1: TInput1, arg2: TInput2) => string)) =>
47+
ValidatorFn {
48+
49+
return function (arg1: TInput1 | (() => TInput1), arg2: TInput2 | (() => TInput2),
50+
message?: string | ((arg1: TInput1, arg2: TInput2) => string)): ValidatorFn {
51+
52+
return function(control: AbstractControl): ValidationErrors | null {
53+
arg1 = ValidatorDeclaration.unwrapArgument(arg1);
54+
arg2 = ValidatorDeclaration.unwrapArgument(arg2);
55+
56+
return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, arg1, arg2);
57+
};
58+
};
59+
}
60+
61+
private static unwrapArgument<T>(arg: T | (() => T)): T {
62+
if (typeof arg === 'function') {
63+
arg = arg();
64+
}
65+
66+
return arg;
67+
}
68+
69+
private static validateAndSetMessageIfInvalid(control: AbstractControl,
70+
validatorFactoryFn: (...args: any[]) => ValidatorFn, resultKey: string,
71+
message?: string | ((...args: any[]) => string), ...args: any[]): ValidationErrors {
72+
73+
const validationResult = ValidatorDeclaration.validate(control, validatorFactoryFn, ...args);
74+
ValidatorDeclaration.setMessageIfInvalid(control, resultKey, validationResult, message, ...args);
75+
76+
return validationResult;
77+
}
78+
79+
private static validate(control: AbstractControl, validatorFactoryFn: (...args: any[]) => ValidatorFn, ...args: any[]): ValidationErrors {
80+
const wrappedValidatorFn = validatorFactoryFn(...args);
81+
return wrappedValidatorFn(control);
82+
}
83+
84+
private static setMessageIfInvalid(control: AbstractControl, resultKey: string,
85+
validationResult: ValidationErrors, message?: string | ((...args: any[]) => string), ...args: any[]) {
86+
if (message) {
87+
if (validationResult && validationResult[resultKey]) {
88+
if (typeof message === 'function') {
89+
message = message(...args);
90+
}
91+
92+
// Not all validators set an object. Often they'll simply set a property to true.
93+
// Here, we replace any non-object (or array) to be an object on which we can set a message.
94+
if (!ValidatorDeclaration.isObject(validationResult[resultKey])) {
95+
validationResult[resultKey] = {};
96+
}
97+
98+
validationResult[resultKey]['message'] = message;
99+
}
100+
}
101+
}
102+
103+
private static isObject(arg: any) {
104+
return arg !== null && typeof arg === 'object' && !Array.isArray(arg);
105+
}
106+
}

angular-reactive-validation/src/validators.ts

Lines changed: 19 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { Validators as AngularValidators, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
1+
import { Validators as AngularValidators, ValidatorFn } from '@angular/forms';
2+
import { ValidatorDeclaration } from './validator-declaration';
23

34
/**
45
* Provides a set of validators used by form controls.
56
*
67
* Code comments have been copied from the Angular source code.
7-
*
8-
* @dynamic
98
*/
109
export class Validators {
10+
private static minValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.min, 'min');
11+
private static maxValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.max, 'max');
12+
private static minLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.minLength, 'minlength');
13+
private static maxLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.maxLength, 'maxlength');
14+
private static patternValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.pattern, 'pattern');
15+
private static requiredValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.required, 'required');
16+
private static requiredTrueValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.requiredTrue, 'required');
17+
private static emailValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.email, 'email');
18+
1119
/**
1220
* No-op validator.
1321
*/
@@ -57,7 +65,7 @@ export class Validators {
5765
*/
5866
static min(min: () => number, messageFunc: ((min: number) => string)): ValidatorFn;
5967
static min(min: number | (() => number), message?: string | ((min: number) => string)): ValidatorFn {
60-
return Validators.singleArgumentValidator(AngularValidators.min, 'min', min, message);
68+
return Validators.minValidator(min, message);
6169
}
6270

6371
/**
@@ -89,7 +97,7 @@ export class Validators {
8997
*/
9098
static max(max: () => number, messageFunc: ((max: number) => string)): ValidatorFn;
9199
static max(max: number | (() => number), message?: string | ((max: number) => string)): ValidatorFn {
92-
return Validators.singleArgumentValidator(AngularValidators.max, 'max', max, message);
100+
return Validators.maxValidator(max, message);
93101
}
94102

95103
/**
@@ -121,7 +129,7 @@ export class Validators {
121129
*/
122130
static minLength(minLength: () => number, messageFunc: ((minLength: number) => string)): ValidatorFn;
123131
static minLength(minLength: number | (() => number), message?: string | ((minLength: number) => string)): ValidatorFn {
124-
return Validators.singleArgumentValidator(AngularValidators.minLength, 'minlength', minLength, message);
132+
return Validators.minLengthValidator(minLength, message);
125133
}
126134

127135
/**
@@ -153,7 +161,7 @@ export class Validators {
153161
*/
154162
static maxLength(maxLength: () => number, messageFunc: ((maxLength: number) => string)): ValidatorFn;
155163
static maxLength(maxLength: number | (() => number), message?: string | ((maxLength: number) => string)): ValidatorFn {
156-
return Validators.singleArgumentValidator(AngularValidators.maxLength, 'maxlength', maxLength, message);
164+
return Validators.maxLengthValidator(maxLength, message);
157165
}
158166

159167
/**
@@ -177,7 +185,7 @@ export class Validators {
177185
*/
178186
static pattern(pattern: () => string|RegExp, message: string): ValidatorFn;
179187
static pattern(pattern: (string|RegExp) | (() => string|RegExp), message?: string): ValidatorFn {
180-
return Validators.singleArgumentValidator(AngularValidators.pattern, 'pattern', pattern, message);
188+
return Validators.patternValidator(pattern, message);
181189
}
182190

183191
/**
@@ -191,7 +199,7 @@ export class Validators {
191199
*/
192200
static required(message: string): ValidatorFn;
193201
static required(message?: string): ValidatorFn {
194-
return Validators.zeroArgumentValidator(AngularValidators.required, 'required', message);
202+
return Validators.requiredValidator(message);
195203
}
196204

197205
/**
@@ -205,7 +213,7 @@ export class Validators {
205213
*/
206214
static requiredTrue(message: string): ValidatorFn;
207215
static requiredTrue(message?: string): ValidatorFn {
208-
return Validators.zeroArgumentValidator(AngularValidators.requiredTrue, 'required', message);
216+
return Validators.requiredTrueValidator(message);
209217
}
210218

211219
/**
@@ -219,50 +227,6 @@ export class Validators {
219227
*/
220228
static email(message: string): ValidatorFn;
221229
static email(message?: string): ValidatorFn {
222-
return Validators.zeroArgumentValidator(AngularValidators.email, 'email', message);
223-
}
224-
225-
private static singleArgumentValidator<TInput>(validatorFunc: ((TInput) => ValidatorFn), resultKey: string,
226-
input: TInput | (() => TInput), message?: string | ((TInput) => string)): ValidatorFn {
227-
return function(c: AbstractControl): ValidationErrors | null {
228-
if (typeof input === 'function') {
229-
input = input();
230-
}
231-
232-
const nativeValidator = validatorFunc(input);
233-
const result = nativeValidator(c);
234-
235-
if (message) {
236-
if (result && result[resultKey]) {
237-
if (typeof message === 'function') {
238-
message = message(input);
239-
}
240-
241-
result[resultKey]['message'] = message;
242-
}
243-
}
244-
245-
return result;
246-
};
247-
}
248-
249-
private static zeroArgumentValidator(validatorFunc: ValidatorFn, resultKey: string, message?: string) {
250-
return function(c: AbstractControl): ValidationErrors | null {
251-
const result = validatorFunc(c);
252-
253-
if (message) {
254-
if (result && result[resultKey]) {
255-
// required, requiredTrue and email validators don't set an object, but set a boolean property.
256-
// When this happens, we replace the boolean with an object containing the message.
257-
if (typeof result[resultKey] === 'boolean') {
258-
result[resultKey] = { message: message };
259-
} else {
260-
result[resultKey]['message'] = message;
261-
}
262-
}
263-
}
264-
265-
return result;
266-
};
230+
return Validators.emailValidator(message);
267231
}
268232
}

0 commit comments

Comments
 (0)