Skip to content

Commit 77707b8

Browse files
authored
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. -->
1 parent 444acc1 commit 77707b8

File tree

8 files changed

+116
-17
lines changed

8 files changed

+116
-17
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './max-validator';
2+
export * from './min-validator';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, forwardRef, Provider } from '@angular/core';
2+
import { MaxValidator, NG_VALIDATORS } from '@angular/forms';
3+
4+
/**
5+
* @description
6+
* Provider which adds `MaxValidator` to the `NG_VALIDATORS` multi-provider list.
7+
*/
8+
export const ION_MAX_VALIDATOR: Provider = {
9+
provide: NG_VALIDATORS,
10+
useExisting: forwardRef(() => IonMaxValidator),
11+
multi: true,
12+
};
13+
14+
@Directive({
15+
selector:
16+
'ion-input[type=number][max][formControlName],ion-input[type=number][max][formControl],ion-input[type=number][max][ngModel]',
17+
providers: [ION_MAX_VALIDATOR],
18+
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
19+
host: { '[attr.max]': '_enabled ? max : null' },
20+
})
21+
// eslint-disable-next-line @angular-eslint/directive-class-suffix
22+
export class IonMaxValidator extends MaxValidator {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, forwardRef, Provider } from '@angular/core';
2+
import { MinValidator, NG_VALIDATORS } from '@angular/forms';
3+
4+
/**
5+
* @description
6+
* Provider which adds `MinValidator` to the `NG_VALIDATORS` multi-provider list.
7+
*/
8+
export const ION_MIN_VALIDATOR: Provider = {
9+
provide: NG_VALIDATORS,
10+
useExisting: forwardRef(() => IonMinValidator),
11+
multi: true,
12+
};
13+
14+
@Directive({
15+
selector:
16+
'ion-input[type=number][min][formControlName],ion-input[type=number][min][formControl],ion-input[type=number][min][ngModel]',
17+
providers: [ION_MIN_VALIDATOR],
18+
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
19+
host: { '[attr.min]': '_enabled ? min : null' },
20+
})
21+
// eslint-disable-next-line @angular-eslint/directive-class-suffix
22+
export class IonMinValidator extends MinValidator {}

packages/angular/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export { NavParams } from './directives/navigation/nav-params';
1717
export { IonModal } from './directives/overlays/modal';
1818
export { IonPopover } from './directives/overlays/popover';
1919
export * from './directives/proxies';
20+
export * from './directives/validators';
2021

2122
// PROVIDERS
2223
export { AngularDelegate } from './providers/angular-delegate';

packages/angular/src/ionic-module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { IonModal } from './directives/overlays/modal';
2323
import { IonPopover } from './directives/overlays/popover';
2424
import { DIRECTIVES } from './directives/proxies-list';
25+
import { IonMaxValidator, IonMinValidator } from './directives/validators';
2526
import { AngularDelegate } from './providers/angular-delegate';
2627
import { ConfigToken } from './providers/config';
2728
import { ModalController } from './providers/modal-controller';
@@ -49,6 +50,10 @@ const DECLARATIONS = [
4950
NavDelegate,
5051
RouterLinkDelegateDirective,
5152
RouterLinkWithHrefDelegateDirective,
53+
54+
// validators
55+
IonMinValidator,
56+
IonMaxValidator,
5257
];
5358

5459
@NgModule({

packages/angular/test/base/e2e/src/form.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe('Form', () => {
3030
toggle: false,
3131
input: '',
3232
input2: 'Default Value',
33+
inputMin: 1,
34+
inputMax: 1,
3335
checkbox: false
3436
});
3537
});
@@ -55,6 +57,8 @@ describe('Form', () => {
5557
toggle: false,
5658
input: 'Some value',
5759
input2: 'Default Value',
60+
inputMin: 1,
61+
inputMax: 1,
5862
checkbox: false
5963
});
6064
});
@@ -67,6 +71,8 @@ describe('Form', () => {
6771
toggle: true,
6872
input: '',
6973
input2: 'Default Value',
74+
inputMin: 1,
75+
inputMax: 1,
7076
checkbox: false
7177
});
7278
});
@@ -79,6 +85,8 @@ describe('Form', () => {
7985
toggle: false,
8086
input: '',
8187
input2: 'Default Value',
88+
inputMin: 1,
89+
inputMax: 1,
8290
checkbox: true
8391
});
8492
});
@@ -99,6 +107,8 @@ describe('Form', () => {
99107
toggle: true,
100108
input: '',
101109
input2: 'Default Value',
110+
inputMin: 1,
111+
inputMax: 1,
102112
checkbox: false
103113
});
104114
cy.get('ion-checkbox').click();
@@ -108,10 +118,39 @@ describe('Form', () => {
108118
toggle: true,
109119
input: '',
110120
input2: 'Default Value',
121+
inputMin: 1,
122+
inputMax: 1,
111123
checkbox: true
112124
});
113125
});
114126
});
127+
128+
describe('validators', () => {
129+
130+
it('ion-input should error with min set', () => {
131+
const control = cy.get('form ion-input[formControlName="inputMin"]');
132+
133+
control.should('have.class', 'ng-valid');
134+
135+
control.type('{backspace}0');
136+
137+
control.within(() => cy.get('input').blur());
138+
139+
control.should('have.class', 'ng-invalid');
140+
});
141+
142+
it('ion-input should error with max set', () => {
143+
const control = cy.get('form ion-input[formControlName="inputMax"]');
144+
145+
control.should('have.class', 'ng-valid');
146+
147+
control.type('2');
148+
control.within(() => cy.get('input').blur());
149+
150+
control.should('have.class', 'ng-invalid');
151+
});
152+
153+
});
115154
});
116155

117156
function testStatus(status) {

packages/angular/test/base/src/app/form/form.component.html

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
<ion-header>
22
<ion-toolbar>
3-
<ion-title>
4-
Forms test
5-
</ion-title>
3+
<ion-title> Forms test </ion-title>
64
</ion-toolbar>
75
</ion-header>
86

97
<ion-content>
108
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
119
<ion-list>
12-
1310
<ion-item>
1411
<ion-label>DateTime</ion-label>
1512
<ion-datetime formControlName="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY">
@@ -29,13 +26,16 @@
2926
</ion-item>
3027

3128
<ion-item>
32-
<ion-toggle formControlName="toggle">
33-
Toggle
34-
</ion-toggle>
29+
<ion-toggle formControlName="toggle"> Toggle </ion-toggle>
3530
</ion-item>
3631

3732
<ion-item>
38-
<ion-input label="Input (required)" formControlName="input" class="required" id="touched-input-test"></ion-input>
33+
<ion-input
34+
label="Input (required)"
35+
formControlName="input"
36+
class="required"
37+
id="touched-input-test"
38+
></ion-input>
3939
</ion-item>
4040

4141
<ion-button id="input-touched" (click)="setTouched()">Set Input Touched</ion-button>
@@ -45,11 +45,20 @@
4545
</ion-item>
4646

4747
<ion-item>
48-
<ion-checkbox formControlName="checkbox">
49-
Checkbox
50-
</ion-checkbox>
48+
<ion-checkbox formControlName="checkbox"> Checkbox </ion-checkbox>
49+
</ion-item>
50+
51+
<ion-item>
52+
<ion-label>Min</ion-label>
53+
<ion-input formControlName="inputMin" type="number"></ion-input>
54+
<pre>errors: {{ profileForm.controls['inputMin'].errors | json }}</pre>
5155
</ion-item>
5256

57+
<ion-item>
58+
<ion-label>Max</ion-label>
59+
<ion-input formControlName="inputMax" type="number"></ion-input>
60+
<pre>errors: {{ profileForm.controls['inputMax'].errors | json }}</pre>
61+
</ion-item>
5362
</ion-list>
5463
<p>
5564
Form Status: <span id="status">{{ profileForm.status }}</span>
@@ -58,18 +67,15 @@
5867
Form value: <span id="data">{{ profileForm.value | json }}</span>
5968
</p>
6069
<p>
61-
Form Submit: <span id="submit">{{submitted}}</span>
70+
Form Submit: <span id="submit">{{ submitted }}</span>
6271
</p>
6372
<ion-button id="mark-all-touched-button" (click)="markAllAsTouched()">Mark all as touched</ion-button>
6473
<ion-button id="submit-button" type="submit" [disabled]="!profileForm.valid">Submit</ion-button>
65-
6674
</form>
6775
<ion-list>
6876
<ion-item>
69-
<ion-toggle [formControl]="outsideToggle">
70-
Outside form
71-
</ion-toggle>
72-
<ion-note slot="end">{{outsideToggle.value}}</ion-note>
77+
<ion-toggle [formControl]="outsideToggle"> Outside form </ion-toggle>
78+
<ion-note slot="end">{{ outsideToggle.value }}</ion-note>
7379
</ion-item>
7480
</ion-list>
7581
<p>

packages/angular/test/base/src/app/form/form.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class FormComponent {
1818
toggle: [false],
1919
input: ['', Validators.required],
2020
input2: ['Default Value'],
21+
inputMin: [1, Validators.min(1)],
22+
inputMax: [1, Validators.max(1)],
2123
checkbox: [false]
2224
}, {
2325
updateOn: typeof (window as any) !== 'undefined' && window.location.hash === '#blur' ? 'blur' : 'change'

0 commit comments

Comments
 (0)