fix(angular): min/max validator for ion-input type number (#27993)

Issue number: Resolves #23480

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

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?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- 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

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
This commit is contained in:
Sean Perkins
2023-08-18 14:34:02 -04:00
committed by GitHub
parent 444acc1f1b
commit 77707b8c1e
8 changed files with 116 additions and 17 deletions

View File

@ -0,0 +1,2 @@
export * from './max-validator';
export * from './min-validator';

View File

@ -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 {}

View File

@ -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 {}

View File

@ -17,6 +17,7 @@ export { NavParams } from './directives/navigation/nav-params';
export { IonModal } from './directives/overlays/modal'; export { IonModal } from './directives/overlays/modal';
export { IonPopover } from './directives/overlays/popover'; export { IonPopover } from './directives/overlays/popover';
export * from './directives/proxies'; export * from './directives/proxies';
export * from './directives/validators';
// PROVIDERS // PROVIDERS
export { AngularDelegate } from './providers/angular-delegate'; export { AngularDelegate } from './providers/angular-delegate';

View File

@ -22,6 +22,7 @@ import {
import { IonModal } from './directives/overlays/modal'; import { IonModal } from './directives/overlays/modal';
import { IonPopover } from './directives/overlays/popover'; import { IonPopover } from './directives/overlays/popover';
import { DIRECTIVES } from './directives/proxies-list'; import { DIRECTIVES } from './directives/proxies-list';
import { IonMaxValidator, IonMinValidator } from './directives/validators';
import { AngularDelegate } from './providers/angular-delegate'; import { AngularDelegate } from './providers/angular-delegate';
import { ConfigToken } from './providers/config'; import { ConfigToken } from './providers/config';
import { ModalController } from './providers/modal-controller'; import { ModalController } from './providers/modal-controller';
@ -49,6 +50,10 @@ const DECLARATIONS = [
NavDelegate, NavDelegate,
RouterLinkDelegateDirective, RouterLinkDelegateDirective,
RouterLinkWithHrefDelegateDirective, RouterLinkWithHrefDelegateDirective,
// validators
IonMinValidator,
IonMaxValidator,
]; ];
@NgModule({ @NgModule({

View File

@ -30,6 +30,8 @@ describe('Form', () => {
toggle: false, toggle: false,
input: '', input: '',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: false checkbox: false
}); });
}); });
@ -55,6 +57,8 @@ describe('Form', () => {
toggle: false, toggle: false,
input: 'Some value', input: 'Some value',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: false checkbox: false
}); });
}); });
@ -67,6 +71,8 @@ describe('Form', () => {
toggle: true, toggle: true,
input: '', input: '',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: false checkbox: false
}); });
}); });
@ -79,6 +85,8 @@ describe('Form', () => {
toggle: false, toggle: false,
input: '', input: '',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: true checkbox: true
}); });
}); });
@ -99,6 +107,8 @@ describe('Form', () => {
toggle: true, toggle: true,
input: '', input: '',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: false checkbox: false
}); });
cy.get('ion-checkbox').click(); cy.get('ion-checkbox').click();
@ -108,10 +118,39 @@ describe('Form', () => {
toggle: true, toggle: true,
input: '', input: '',
input2: 'Default Value', input2: 'Default Value',
inputMin: 1,
inputMax: 1,
checkbox: true 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) { function testStatus(status) {

View File

@ -1,15 +1,12 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title> <ion-title> Forms test </ion-title>
Forms test
</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"> <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<ion-list> <ion-list>
<ion-item> <ion-item>
<ion-label>DateTime</ion-label> <ion-label>DateTime</ion-label>
<ion-datetime formControlName="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY"> <ion-datetime formControlName="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY">
@ -29,13 +26,16 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-toggle formControlName="toggle"> <ion-toggle formControlName="toggle"> Toggle </ion-toggle>
Toggle
</ion-toggle>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input label="Input (required)" formControlName="input" class="required" id="touched-input-test"></ion-input> <ion-input
label="Input (required)"
formControlName="input"
class="required"
id="touched-input-test"
></ion-input>
</ion-item> </ion-item>
<ion-button id="input-touched" (click)="setTouched()">Set Input Touched</ion-button> <ion-button id="input-touched" (click)="setTouched()">Set Input Touched</ion-button>
@ -45,11 +45,20 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-checkbox formControlName="checkbox"> <ion-checkbox formControlName="checkbox"> Checkbox </ion-checkbox>
Checkbox
</ion-checkbox>
</ion-item> </ion-item>
<ion-item>
<ion-label>Min</ion-label>
<ion-input formControlName="inputMin" type="number"></ion-input>
<pre>errors: {{ profileForm.controls['inputMin'].errors | json }}</pre>
</ion-item>
<ion-item>
<ion-label>Max</ion-label>
<ion-input formControlName="inputMax" type="number"></ion-input>
<pre>errors: {{ profileForm.controls['inputMax'].errors | json }}</pre>
</ion-item>
</ion-list> </ion-list>
<p> <p>
Form Status: <span id="status">{{ profileForm.status }}</span> Form Status: <span id="status">{{ profileForm.status }}</span>
@ -58,18 +67,15 @@
Form value: <span id="data">{{ profileForm.value | json }}</span> Form value: <span id="data">{{ profileForm.value | json }}</span>
</p> </p>
<p> <p>
Form Submit: <span id="submit">{{submitted}}</span> Form Submit: <span id="submit">{{ submitted }}</span>
</p> </p>
<ion-button id="mark-all-touched-button" (click)="markAllAsTouched()">Mark all as touched</ion-button> <ion-button id="mark-all-touched-button" (click)="markAllAsTouched()">Mark all as touched</ion-button>
<ion-button id="submit-button" type="submit" [disabled]="!profileForm.valid">Submit</ion-button> <ion-button id="submit-button" type="submit" [disabled]="!profileForm.valid">Submit</ion-button>
</form> </form>
<ion-list> <ion-list>
<ion-item> <ion-item>
<ion-toggle [formControl]="outsideToggle"> <ion-toggle [formControl]="outsideToggle"> Outside form </ion-toggle>
Outside form <ion-note slot="end">{{ outsideToggle.value }}</ion-note>
</ion-toggle>
<ion-note slot="end">{{outsideToggle.value}}</ion-note>
</ion-item> </ion-item>
</ion-list> </ion-list>
<p> <p>

View File

@ -18,6 +18,8 @@ export class FormComponent {
toggle: [false], toggle: [false],
input: ['', Validators.required], input: ['', Validators.required],
input2: ['Default Value'], input2: ['Default Value'],
inputMin: [1, Validators.min(1)],
inputMax: [1, Validators.max(1)],
checkbox: [false] checkbox: [false]
}, { }, {
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change' updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'