Skip to content

Commit 8fb83ea

Browse files
sonukapooralxhub
authored andcommitted
feat(forms): introduce min and max validators (#39063)
This commit adds the missing `min` and `max` validators. BREAKING CHANGE: Previously `min` and `max` attributes defined on the `<input type="number">` were ignored by Forms module. Now presence of these attributes would trigger min/max validation logic (in case `formControl`, `formControlName` or `ngModel` directives are also present on a given input) and corresponding form control status would reflect that. Fixes #16352 PR Close #39063
1 parent d067dc0 commit 8fb83ea

File tree

6 files changed

+936
-5
lines changed

6 files changed

+936
-5
lines changed

goldens/public-api/forms/forms.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,23 @@ export declare class MaxLengthValidator implements Validator, OnChanges {
337337
validate(control: AbstractControl): ValidationErrors | null;
338338
}
339339

340+
export declare class MaxValidator extends AbstractValidatorDirective implements OnChanges {
341+
max: string | number;
342+
ngOnChanges(changes: SimpleChanges): void;
343+
}
344+
340345
export declare class MinLengthValidator implements Validator, OnChanges {
341346
minlength: string | number;
342347
ngOnChanges(changes: SimpleChanges): void;
343348
registerOnValidatorChange(fn: () => void): void;
344349
validate(control: AbstractControl): ValidationErrors | null;
345350
}
346351

352+
export declare class MinValidator extends AbstractValidatorDirective implements OnChanges {
353+
min: string | number;
354+
ngOnChanges(changes: SimpleChanges): void;
355+
}
356+
347357
export declare const NG_ASYNC_VALIDATORS: InjectionToken<(Function | Validator)[]>;
348358

349359
export declare const NG_VALIDATORS: InjectionToken<(Function | Validator)[]>;

packages/forms/src/directives.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di
2424
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
2525
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
2626
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
27-
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
27+
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator} from './directives/validators';
2828

2929
export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
3030
export {ControlValueAccessor} from './directives/control_value_accessor';
@@ -63,6 +63,8 @@ export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
6363
PatternValidator,
6464
CheckboxRequiredValidator,
6565
EmailValidator,
66+
MinValidator,
67+
MaxValidator,
6668
];
6769

6870
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];

packages/forms/src/directives/validators.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,189 @@ export interface Validator {
6969
registerOnValidatorChange?(fn: () => void): void;
7070
}
7171

72+
/**
73+
* A base class for Validator-based Directives. The class contains common logic shared across such
74+
* Directives.
75+
*
76+
* For internal use only, this class is not intended for use outside of the Forms package.
77+
*/
78+
@Directive()
79+
abstract class AbstractValidatorDirective implements Validator {
80+
private _validator: ValidatorFn = Validators.nullValidator;
81+
private _onChange!: () => void;
82+
83+
/**
84+
* Name of an input that matches directive selector attribute (e.g. `minlength` for
85+
* `MinLengthDirective`). An input with a given name might contain configuration information (like
86+
* `minlength='10'`) or a flag that indicates whether validator should be enabled (like
87+
* `[required]='false'`).
88+
*
89+
* @internal
90+
*/
91+
abstract inputName: string;
92+
93+
/**
94+
* Creates an instance of a validator (specific to a directive that extends this base class).
95+
*
96+
* @internal
97+
*/
98+
abstract createValidator(input: unknown): ValidatorFn;
99+
100+
/**
101+
* Performs the necessary input normalization based on a specific logic of a Directive.
102+
* For example, the function might be used to convert string-based representation of the
103+
* `minlength` input to an integer value that can later be used in the `Validators.minLength`
104+
* validator.
105+
*
106+
* @internal
107+
*/
108+
abstract normalizeInput(input: unknown): unknown;
109+
110+
/**
111+
* Helper function invoked from child classes to process changes (from `ngOnChanges` hook).
112+
* @nodoc
113+
*/
114+
handleChanges(changes: SimpleChanges): void {
115+
if (this.inputName in changes) {
116+
const input = this.normalizeInput(changes[this.inputName].currentValue);
117+
this._validator = this.createValidator(input);
118+
if (this._onChange) {
119+
this._onChange();
120+
}
121+
}
122+
}
123+
124+
/** @nodoc */
125+
validate(control: AbstractControl): ValidationErrors|null {
126+
return this._validator(control);
127+
}
128+
129+
/** @nodoc */
130+
registerOnValidatorChange(fn: () => void): void {
131+
this._onChange = fn;
132+
}
133+
}
134+
135+
/**
136+
* @description
137+
* Provider which adds `MaxValidator` to the `NG_VALIDATORS` multi-provider list.
138+
*/
139+
export const MAX_VALIDATOR: StaticProvider = {
140+
provide: NG_VALIDATORS,
141+
useExisting: forwardRef(() => MaxValidator),
142+
multi: true
143+
};
144+
145+
/**
146+
* A directive which installs the {@link MaxValidator} for any `formControlName`,
147+
* `formControl`, or control with `ngModel` that also has a `max` attribute.
148+
*
149+
* @see [Form Validation](guide/form-validation)
150+
*
151+
* @usageNotes
152+
*
153+
* ### Adding a max validator
154+
*
155+
* The following example shows how to add a max validator to an input attached to an
156+
* ngModel binding.
157+
*
158+
* ```html
159+
* <input type="number" ngModel max="4">
160+
* ```
161+
*
162+
* @ngModule ReactiveFormsModule
163+
* @ngModule FormsModule
164+
* @publicApi
165+
*/
166+
@Directive({
167+
selector:
168+
'input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]',
169+
providers: [MAX_VALIDATOR],
170+
host: {'[attr.max]': 'max ? max : null'}
171+
})
172+
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
173+
/**
174+
* @description
175+
* Tracks changes to the max bound to this directive.
176+
*/
177+
@Input() max!: string|number;
178+
/** @internal */
179+
inputName = 'max';
180+
/** @internal */
181+
normalizeInput = (input: string): number => parseInt(input, 10);
182+
/** @internal */
183+
createValidator = (max: number): ValidatorFn => Validators.max(max);
184+
/**
185+
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
186+
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
187+
* AOT mode. This could be refactored once ViewEngine is removed.
188+
* @nodoc
189+
*/
190+
ngOnChanges(changes: SimpleChanges): void {
191+
this.handleChanges(changes);
192+
}
193+
}
194+
195+
/**
196+
* @description
197+
* Provider which adds `MinValidator` to the `NG_VALIDATORS` multi-provider list.
198+
*/
199+
export const MIN_VALIDATOR: StaticProvider = {
200+
provide: NG_VALIDATORS,
201+
useExisting: forwardRef(() => MinValidator),
202+
multi: true
203+
};
204+
205+
/**
206+
* A directive which installs the {@link MinValidator} for any `formControlName`,
207+
* `formControl`, or control with `ngModel` that also has a `min` attribute.
208+
*
209+
* @see [Form Validation](guide/form-validation)
210+
*
211+
* @usageNotes
212+
*
213+
* ### Adding a min validator
214+
*
215+
* The following example shows how to add a min validator to an input attached to an
216+
* ngModel binding.
217+
*
218+
* ```html
219+
* <input type="number" ngModel min="4">
220+
* ```
221+
*
222+
* @ngModule ReactiveFormsModule
223+
* @ngModule FormsModule
224+
* @publicApi
225+
*/
226+
@Directive({
227+
selector:
228+
'input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]',
229+
providers: [MIN_VALIDATOR],
230+
host: {'[attr.min]': 'min ? min : null'}
231+
})
232+
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
233+
/**
234+
* @description
235+
* Tracks changes to the min bound to this directive.
236+
*/
237+
@Input() min!: string|number;
238+
/** @internal */
239+
inputName = 'min';
240+
/** @internal */
241+
normalizeInput = (input: string): number => parseInt(input, 10);
242+
/** @internal */
243+
createValidator = (min: number): ValidatorFn => Validators.min(min);
244+
/**
245+
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
246+
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
247+
* AOT mode. This could be refactored once ViewEngine is removed.
248+
* @nodoc
249+
*/
250+
ngOnChanges(changes: SimpleChanges): void {
251+
this.handleChanges(changes);
252+
}
253+
}
254+
72255
/**
73256
* @description
74257
* An interface implemented by classes that perform asynchronous validation.

packages/forms/src/forms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export {FormGroupName} from './directives/reactive_directives/form_group_name';
4343
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
4444
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
4545
export {ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
46-
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
46+
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
4747
export {FormBuilder} from './form_builder';
4848
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup} from './model';
4949
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';

0 commit comments

Comments
 (0)