mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
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:
2
packages/angular/src/directives/validators/index.ts
Normal file
2
packages/angular/src/directives/validators/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './max-validator';
|
||||||
|
export * from './min-validator';
|
22
packages/angular/src/directives/validators/max-validator.ts
Normal file
22
packages/angular/src/directives/validators/max-validator.ts
Normal 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 {}
|
22
packages/angular/src/directives/validators/min-validator.ts
Normal file
22
packages/angular/src/directives/validators/min-validator.ts
Normal 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 {}
|
@ -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';
|
||||||
|
@ -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({
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
Reference in New Issue
Block a user