Skip to content

Commit

Permalink
feat(forms): introduce min and max validators (#39063)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sonukapoor authored and alxhub committed Feb 8, 2021
1 parent d067dc0 commit 8fb83ea
Show file tree
Hide file tree
Showing 6 changed files with 936 additions and 5 deletions.
10 changes: 10 additions & 0 deletions goldens/public-api/forms/forms.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,23 @@ export declare class MaxLengthValidator implements Validator, OnChanges {
validate(control: AbstractControl): ValidationErrors | null;
}

export declare class MaxValidator extends AbstractValidatorDirective implements OnChanges {
max: string | number;
ngOnChanges(changes: SimpleChanges): void;
}

export declare class MinLengthValidator implements Validator, OnChanges {
minlength: string | number;
ngOnChanges(changes: SimpleChanges): void;
registerOnValidatorChange(fn: () => void): void;
validate(control: AbstractControl): ValidationErrors | null;
}

export declare class MinValidator extends AbstractValidatorDirective implements OnChanges {
min: string | number;
ngOnChanges(changes: SimpleChanges): void;
}

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

export declare const NG_VALIDATORS: InjectionToken<(Function | Validator)[]>;
Expand Down
4 changes: 3 additions & 1 deletion packages/forms/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di
import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name';
import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators';
import {CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator} from './directives/validators';

export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor';
export {ControlValueAccessor} from './directives/control_value_accessor';
Expand Down Expand Up @@ -63,6 +63,8 @@ export const SHARED_FORM_DIRECTIVES: Type<any>[] = [
PatternValidator,
CheckboxRequiredValidator,
EmailValidator,
MinValidator,
MaxValidator,
];

export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm];
Expand Down
183 changes: 183 additions & 0 deletions packages/forms/src/directives/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,189 @@ export interface Validator {
registerOnValidatorChange?(fn: () => void): void;
}

/**
* A base class for Validator-based Directives. The class contains common logic shared across such
* Directives.
*
* For internal use only, this class is not intended for use outside of the Forms package.
*/
@Directive()
abstract class AbstractValidatorDirective implements Validator {
private _validator: ValidatorFn = Validators.nullValidator;
private _onChange!: () => void;

/**
* Name of an input that matches directive selector attribute (e.g. `minlength` for
* `MinLengthDirective`). An input with a given name might contain configuration information (like
* `minlength='10'`) or a flag that indicates whether validator should be enabled (like
* `[required]='false'`).
*
* @internal
*/
abstract inputName: string;

/**
* Creates an instance of a validator (specific to a directive that extends this base class).
*
* @internal
*/
abstract createValidator(input: unknown): ValidatorFn;

/**
* Performs the necessary input normalization based on a specific logic of a Directive.
* For example, the function might be used to convert string-based representation of the
* `minlength` input to an integer value that can later be used in the `Validators.minLength`
* validator.
*
* @internal
*/
abstract normalizeInput(input: unknown): unknown;

/**
* Helper function invoked from child classes to process changes (from `ngOnChanges` hook).
* @nodoc
*/
handleChanges(changes: SimpleChanges): void {
if (this.inputName in changes) {
const input = this.normalizeInput(changes[this.inputName].currentValue);
this._validator = this.createValidator(input);
if (this._onChange) {
this._onChange();
}
}
}

/** @nodoc */
validate(control: AbstractControl): ValidationErrors|null {
return this._validator(control);
}

/** @nodoc */
registerOnValidatorChange(fn: () => void): void {
this._onChange = fn;
}
}

/**
* @description
* Provider which adds `MaxValidator` to the `NG_VALIDATORS` multi-provider list.
*/
export const MAX_VALIDATOR: StaticProvider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MaxValidator),
multi: true
};

/**
* A directive which installs the {@link MaxValidator} for any `formControlName`,
* `formControl`, or control with `ngModel` that also has a `max` attribute.
*
* @see [Form Validation](guide/form-validation)
*
* @usageNotes
*
* ### Adding a max validator
*
* The following example shows how to add a max validator to an input attached to an
* ngModel binding.
*
* ```html
* <input type="number" ngModel max="4">
* ```
*
* @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi
*/
@Directive({
selector:
'input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]',
providers: [MAX_VALIDATOR],
host: {'[attr.max]': 'max ? max : null'}
})
export class MaxValidator extends AbstractValidatorDirective implements OnChanges {
/**
* @description
* Tracks changes to the max bound to this directive.
*/
@Input() max!: string|number;
/** @internal */
inputName = 'max';
/** @internal */
normalizeInput = (input: string): number => parseInt(input, 10);
/** @internal */
createValidator = (max: number): ValidatorFn => Validators.max(max);
/**
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
* AOT mode. This could be refactored once ViewEngine is removed.
* @nodoc
*/
ngOnChanges(changes: SimpleChanges): void {
this.handleChanges(changes);
}
}

/**
* @description
* Provider which adds `MinValidator` to the `NG_VALIDATORS` multi-provider list.
*/
export const MIN_VALIDATOR: StaticProvider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MinValidator),
multi: true
};

/**
* A directive which installs the {@link MinValidator} for any `formControlName`,
* `formControl`, or control with `ngModel` that also has a `min` attribute.
*
* @see [Form Validation](guide/form-validation)
*
* @usageNotes
*
* ### Adding a min validator
*
* The following example shows how to add a min validator to an input attached to an
* ngModel binding.
*
* ```html
* <input type="number" ngModel min="4">
* ```
*
* @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi
*/
@Directive({
selector:
'input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]',
providers: [MIN_VALIDATOR],
host: {'[attr.min]': 'min ? min : null'}
})
export class MinValidator extends AbstractValidatorDirective implements OnChanges {
/**
* @description
* Tracks changes to the min bound to this directive.
*/
@Input() min!: string|number;
/** @internal */
inputName = 'min';
/** @internal */
normalizeInput = (input: string): number => parseInt(input, 10);
/** @internal */
createValidator = (min: number): ValidatorFn => Validators.min(min);
/**
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
* AOT mode. This could be refactored once ViewEngine is removed.
* @nodoc
*/
ngOnChanges(changes: SimpleChanges): void {
this.handleChanges(changes);
}
}

/**
* @description
* An interface implemented by classes that perform asynchronous validation.
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/src/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export {FormGroupName} from './directives/reactive_directives/form_group_name';
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor';
export {ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
export {FormBuilder} from './form_builder';
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup} from './model';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
Expand Down
Loading

0 comments on commit 8fb83ea

Please sign in to comment.