From 77707b8c1eb939e57207c775ba92a2050632edd0 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 18 Aug 2023 14:34:02 -0400 Subject: [PATCH] fix(angular): min/max validator for ion-input type number (#27993) Issue number: Resolves #23480 --------- ## What is the current behavior? Angular's min/max validators do not work with `ion-input[type=number]`. Using the built-in validators with `ion-input` will not update the control status to invalid, reflect the `ng-invalid` class or report the correct errors. ## What is the new behavior? - The `IonicModule` now includes two additional directive declarations that extend Angular's built-in min/max validators and target the `ion-input` component when using `type="number"`. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --- .../src/directives/validators/index.ts | 2 + .../directives/validators/max-validator.ts | 22 ++++++++++ .../directives/validators/min-validator.ts | 22 ++++++++++ packages/angular/src/index.ts | 1 + packages/angular/src/ionic-module.ts | 5 +++ .../angular/test/base/e2e/src/form.spec.ts | 39 ++++++++++++++++++ .../base/src/app/form/form.component.html | 40 +++++++++++-------- .../test/base/src/app/form/form.component.ts | 2 + 8 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 packages/angular/src/directives/validators/index.ts create mode 100644 packages/angular/src/directives/validators/max-validator.ts create mode 100644 packages/angular/src/directives/validators/min-validator.ts diff --git a/packages/angular/src/directives/validators/index.ts b/packages/angular/src/directives/validators/index.ts new file mode 100644 index 0000000000..50edda4ec5 --- /dev/null +++ b/packages/angular/src/directives/validators/index.ts @@ -0,0 +1,2 @@ +export * from './max-validator'; +export * from './min-validator'; diff --git a/packages/angular/src/directives/validators/max-validator.ts b/packages/angular/src/directives/validators/max-validator.ts new file mode 100644 index 0000000000..be316f3dca --- /dev/null +++ b/packages/angular/src/directives/validators/max-validator.ts @@ -0,0 +1,22 @@ +import { Directive, forwardRef, Provider } from '@angular/core'; +import { MaxValidator, NG_VALIDATORS } from '@angular/forms'; + +/** + * @description + * Provider which adds `MaxValidator` to the `NG_VALIDATORS` multi-provider list. + */ +export const ION_MAX_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => IonMaxValidator), + multi: true, +}; + +@Directive({ + selector: + 'ion-input[type=number][max][formControlName],ion-input[type=number][max][formControl],ion-input[type=number][max][ngModel]', + providers: [ION_MAX_VALIDATOR], + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { '[attr.max]': '_enabled ? max : null' }, +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonMaxValidator extends MaxValidator {} diff --git a/packages/angular/src/directives/validators/min-validator.ts b/packages/angular/src/directives/validators/min-validator.ts new file mode 100644 index 0000000000..bce1f0b066 --- /dev/null +++ b/packages/angular/src/directives/validators/min-validator.ts @@ -0,0 +1,22 @@ +import { Directive, forwardRef, Provider } from '@angular/core'; +import { MinValidator, NG_VALIDATORS } from '@angular/forms'; + +/** + * @description + * Provider which adds `MinValidator` to the `NG_VALIDATORS` multi-provider list. + */ +export const ION_MIN_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => IonMinValidator), + multi: true, +}; + +@Directive({ + selector: + 'ion-input[type=number][min][formControlName],ion-input[type=number][min][formControl],ion-input[type=number][min][ngModel]', + providers: [ION_MIN_VALIDATOR], + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { '[attr.min]': '_enabled ? min : null' }, +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class IonMinValidator extends MinValidator {} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index b92167e481..fb0daa2a4d 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -17,6 +17,7 @@ export { NavParams } from './directives/navigation/nav-params'; export { IonModal } from './directives/overlays/modal'; export { IonPopover } from './directives/overlays/popover'; export * from './directives/proxies'; +export * from './directives/validators'; // PROVIDERS export { AngularDelegate } from './providers/angular-delegate'; diff --git a/packages/angular/src/ionic-module.ts b/packages/angular/src/ionic-module.ts index 32ad4eaa4d..b8c7f5c227 100644 --- a/packages/angular/src/ionic-module.ts +++ b/packages/angular/src/ionic-module.ts @@ -22,6 +22,7 @@ import { import { IonModal } from './directives/overlays/modal'; import { IonPopover } from './directives/overlays/popover'; import { DIRECTIVES } from './directives/proxies-list'; +import { IonMaxValidator, IonMinValidator } from './directives/validators'; import { AngularDelegate } from './providers/angular-delegate'; import { ConfigToken } from './providers/config'; import { ModalController } from './providers/modal-controller'; @@ -49,6 +50,10 @@ const DECLARATIONS = [ NavDelegate, RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective, + + // validators + IonMinValidator, + IonMaxValidator, ]; @NgModule({ diff --git a/packages/angular/test/base/e2e/src/form.spec.ts b/packages/angular/test/base/e2e/src/form.spec.ts index 39995f2f5a..10d5be7132 100644 --- a/packages/angular/test/base/e2e/src/form.spec.ts +++ b/packages/angular/test/base/e2e/src/form.spec.ts @@ -30,6 +30,8 @@ describe('Form', () => { toggle: false, input: '', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: false }); }); @@ -55,6 +57,8 @@ describe('Form', () => { toggle: false, input: 'Some value', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: false }); }); @@ -67,6 +71,8 @@ describe('Form', () => { toggle: true, input: '', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: false }); }); @@ -79,6 +85,8 @@ describe('Form', () => { toggle: false, input: '', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: true }); }); @@ -99,6 +107,8 @@ describe('Form', () => { toggle: true, input: '', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: false }); cy.get('ion-checkbox').click(); @@ -108,10 +118,39 @@ describe('Form', () => { toggle: true, input: '', input2: 'Default Value', + inputMin: 1, + inputMax: 1, checkbox: true }); }); }); + + describe('validators', () => { + + it('ion-input should error with min set', () => { + const control = cy.get('form ion-input[formControlName="inputMin"]'); + + control.should('have.class', 'ng-valid'); + + control.type('{backspace}0'); + + control.within(() => cy.get('input').blur()); + + control.should('have.class', 'ng-invalid'); + }); + + it('ion-input should error with max set', () => { + const control = cy.get('form ion-input[formControlName="inputMax"]'); + + control.should('have.class', 'ng-valid'); + + control.type('2'); + control.within(() => cy.get('input').blur()); + + control.should('have.class', 'ng-invalid'); + }); + + }); }); function testStatus(status) { diff --git a/packages/angular/test/base/src/app/form/form.component.html b/packages/angular/test/base/src/app/form/form.component.html index 4ba9b9621f..f5891cb19f 100644 --- a/packages/angular/test/base/src/app/form/form.component.html +++ b/packages/angular/test/base/src/app/form/form.component.html @@ -1,15 +1,12 @@ - - Forms test - + Forms test
- DateTime @@ -29,13 +26,16 @@ - - Toggle - + Toggle - + Set Input Touched @@ -45,11 +45,20 @@ - - Checkbox - + Checkbox + + Min + +
errors: {{ profileForm.controls['inputMin'].errors | json }}
+
+ + + Max + +
errors: {{ profileForm.controls['inputMax'].errors | json }}
+

Form Status: {{ profileForm.status }} @@ -58,18 +67,15 @@ Form value: {{ profileForm.value | json }}

- Form Submit: {{submitted}} + Form Submit: {{ submitted }}

Mark all as touched Submit -
- - Outside form - - {{outsideToggle.value}} + Outside form + {{ outsideToggle.value }}

diff --git a/packages/angular/test/base/src/app/form/form.component.ts b/packages/angular/test/base/src/app/form/form.component.ts index ec10f31cbb..131237632c 100644 --- a/packages/angular/test/base/src/app/form/form.component.ts +++ b/packages/angular/test/base/src/app/form/form.component.ts @@ -18,6 +18,8 @@ export class FormComponent { toggle: [false], input: ['', Validators.required], input2: ['Default Value'], + inputMin: [1, Validators.min(1)], + inputMax: [1, Validators.max(1)], checkbox: [false] }, { updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'