mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
feat(input): ionChange will only emit from user committed changes (#25858)
Resolves #20106, #20061
This commit is contained in:
@ -14,6 +14,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
|||||||
|
|
||||||
- [Browser and Platform Support](#version-7x-browser-platform-support)
|
- [Browser and Platform Support](#version-7x-browser-platform-support)
|
||||||
- [Components](#version-7x-components)
|
- [Components](#version-7x-components)
|
||||||
|
- [Input](#version-7x-input)
|
||||||
- [Overlays](#version-7x-overlays)
|
- [Overlays](#version-7x-overlays)
|
||||||
- [Range](#version-7x-range)
|
- [Range](#version-7x-range)
|
||||||
- [Slides](#version-7x-slides)
|
- [Slides](#version-7x-slides)
|
||||||
@ -50,6 +51,12 @@ This section details the desktop browser, JavaScript framework, and mobile platf
|
|||||||
|
|
||||||
<h2 id="version-7x-components">Components</h2>
|
<h2 id="version-7x-components">Components</h2>
|
||||||
|
|
||||||
|
<h4 id="version-7x-input">Input</h4>
|
||||||
|
|
||||||
|
`ionChange` is no longer emitted when the `value` of `ion-input` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the input and the input losing focus or from clicking the clear action within the input.
|
||||||
|
|
||||||
|
If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead.
|
||||||
|
|
||||||
<h4 id="version-7x-overlays">Overlays</h4>
|
<h4 id="version-7x-overlays">Overlays</h4>
|
||||||
|
|
||||||
Ionic now listens on the `keydown` event instead of the `keyup` event when determining when to dismiss overlays via the "Escape" key. Any applications that were listening on `keyup` to suppress this behavior should listen on `keydown` instead.
|
Ionic now listens on the `keydown` event instead of the `keyup` event when determining when to dismiss overlays via the "Escape" key. Any applications that were listening on `keyup` to suppress this behavior should listen on `keydown` instead.
|
||||||
|
@ -25,6 +25,6 @@ export class BooleanValueAccessorDirective extends ValueAccessor {
|
|||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
_handleIonChange(el: any): void {
|
_handleIonChange(el: any): void {
|
||||||
this.handleChangeEvent(el, el.checked);
|
this.handleValueChange(el, el.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
angular/src/directives/control-value-accessors/index.ts
Normal file
5
angular/src/directives/control-value-accessors/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './boolean-value-accessor';
|
||||||
|
export * from './numeric-value-accessor';
|
||||||
|
export * from './radio-value-accessor';
|
||||||
|
export * from './select-value-accessor';
|
||||||
|
export * from './text-value-accessor';
|
@ -18,13 +18,13 @@ export class NumericValueAccessorDirective extends ValueAccessor {
|
|||||||
super(injector, el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionInput', ['$event.target'])
|
||||||
_handleIonChange(el: any): void {
|
handleInputEvent(el: HTMLIonInputElement): void {
|
||||||
this.handleChangeEvent(el, el.value);
|
this.handleValueChange(el, el.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerOnChange(fn: (_: number | null) => void): void {
|
registerOnChange(fn: (_: number | null) => void): void {
|
||||||
super.registerOnChange((value) => {
|
super.registerOnChange((value: string) => {
|
||||||
fn(value === '' ? null : parseFloat(value));
|
fn(value === '' ? null : parseFloat(value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,6 @@ export class RadioValueAccessorDirective extends ValueAccessor {
|
|||||||
|
|
||||||
@HostListener('ionSelect', ['$event.target'])
|
@HostListener('ionSelect', ['$event.target'])
|
||||||
_handleIonSelect(el: any): void {
|
_handleIonSelect(el: any): void {
|
||||||
this.handleChangeEvent(el, el.checked);
|
this.handleValueChange(el, el.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,6 @@ export class SelectValueAccessorDirective extends ValueAccessor {
|
|||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
_handleChangeEvent(el: any): void {
|
_handleChangeEvent(el: any): void {
|
||||||
this.handleChangeEvent(el, el.value);
|
this.handleValueChange(el, el.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
/* tslint:disable-next-line:directive-selector */
|
/* tslint:disable-next-line:directive-selector */
|
||||||
selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar',
|
selector: 'ion-textarea,ion-searchbar',
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
@ -21,6 +21,27 @@ export class TextValueAccessorDirective extends ValueAccessor {
|
|||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
_handleInputEvent(el: any): void {
|
_handleInputEvent(el: any): void {
|
||||||
this.handleChangeEvent(el, el.value);
|
this.handleValueChange(el, el.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'ion-input:not([type=number])',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: InputValueAccessorDirective,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class InputValueAccessorDirective extends ValueAccessor {
|
||||||
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
|
super(injector, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('ionInput', ['$event.target'])
|
||||||
|
_handleInputEvent(el: any): void {
|
||||||
|
this.handleValueChange(el, el.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,20 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
|||||||
setIonicClasses(this.el);
|
setIonicClasses(this.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeEvent(el: HTMLElement, value: any): void {
|
/**
|
||||||
|
* Notifies the ControlValueAccessor of a change in the value of the control.
|
||||||
|
*
|
||||||
|
* This is called by each of the ValueAccessor directives when we want to update
|
||||||
|
* the status and validity of the form control. For example with text components this
|
||||||
|
* is called when the ionInput event is fired. For select components this is called
|
||||||
|
* when the ionChange event is fired.
|
||||||
|
*
|
||||||
|
* This also updates the Ionic form status classes on the element.
|
||||||
|
*
|
||||||
|
* @param el The component element.
|
||||||
|
* @param value The new value of the control.
|
||||||
|
*/
|
||||||
|
handleValueChange(el: HTMLElement, value: any): void {
|
||||||
if (el === this.el.nativeElement) {
|
if (el === this.el.nativeElement) {
|
||||||
if (value !== this.lastValue) {
|
if (value !== this.lastValue) {
|
||||||
this.lastValue = value;
|
this.lastValue = value;
|
||||||
|
@ -3,7 +3,10 @@ export { BooleanValueAccessorDirective as BooleanValueAccessor } from './directi
|
|||||||
export { NumericValueAccessorDirective as NumericValueAccessor } from './directives/control-value-accessors/numeric-value-accessor';
|
export { NumericValueAccessorDirective as NumericValueAccessor } from './directives/control-value-accessors/numeric-value-accessor';
|
||||||
export { RadioValueAccessorDirective as RadioValueAccessor } from './directives/control-value-accessors/radio-value-accessor';
|
export { RadioValueAccessorDirective as RadioValueAccessor } from './directives/control-value-accessors/radio-value-accessor';
|
||||||
export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-value-accessor';
|
export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-value-accessor';
|
||||||
export { TextValueAccessorDirective as TextValueAccessor } from './directives/control-value-accessors/text-value-accessor';
|
export {
|
||||||
|
TextValueAccessorDirective as TextValueAccessor,
|
||||||
|
InputValueAccessorDirective as InputValueAccessor,
|
||||||
|
} from './directives/control-value-accessors/text-value-accessor';
|
||||||
export { IonTabs } from './directives/navigation/ion-tabs';
|
export { IonTabs } from './directives/navigation/ion-tabs';
|
||||||
export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button';
|
export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button';
|
||||||
export { NavDelegate } from './directives/navigation/nav-delegate';
|
export { NavDelegate } from './directives/navigation/nav-delegate';
|
||||||
|
@ -3,11 +3,14 @@ import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular
|
|||||||
import { IonicConfig } from '@ionic/core';
|
import { IonicConfig } from '@ionic/core';
|
||||||
|
|
||||||
import { appInitialize } from './app-initialize';
|
import { appInitialize } from './app-initialize';
|
||||||
import { BooleanValueAccessorDirective } from './directives/control-value-accessors/boolean-value-accessor';
|
import {
|
||||||
import { NumericValueAccessorDirective } from './directives/control-value-accessors/numeric-value-accessor';
|
BooleanValueAccessorDirective,
|
||||||
import { RadioValueAccessorDirective } from './directives/control-value-accessors/radio-value-accessor';
|
NumericValueAccessorDirective,
|
||||||
import { SelectValueAccessorDirective } from './directives/control-value-accessors/select-value-accessor';
|
RadioValueAccessorDirective,
|
||||||
import { TextValueAccessorDirective } from './directives/control-value-accessors/text-value-accessor';
|
SelectValueAccessorDirective,
|
||||||
|
TextValueAccessorDirective,
|
||||||
|
InputValueAccessorDirective,
|
||||||
|
} from './directives/control-value-accessors';
|
||||||
import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button';
|
import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button';
|
||||||
import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
|
import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
|
||||||
import { IonTabs } from './directives/navigation/ion-tabs';
|
import { IonTabs } from './directives/navigation/ion-tabs';
|
||||||
@ -38,6 +41,7 @@ const DECLARATIONS = [
|
|||||||
RadioValueAccessorDirective,
|
RadioValueAccessorDirective,
|
||||||
SelectValueAccessorDirective,
|
SelectValueAccessorDirective,
|
||||||
TextValueAccessorDirective,
|
TextValueAccessorDirective,
|
||||||
|
InputValueAccessorDirective,
|
||||||
|
|
||||||
// navigation
|
// navigation
|
||||||
IonTabs,
|
IonTabs,
|
||||||
|
@ -36,7 +36,9 @@ describe('Form', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should become valid', () => {
|
it('should become valid', () => {
|
||||||
cy.get('ion-input.required').invoke('prop', 'value', 'Some value');
|
cy.get('ion-input.required').type('Some value');
|
||||||
|
cy.get('ion-input.required input').blur();
|
||||||
|
|
||||||
testStatus('INVALID');
|
testStatus('INVALID');
|
||||||
|
|
||||||
// TODO: FW-1160 - Remove when v7 is released
|
// TODO: FW-1160 - Remove when v7 is released
|
||||||
|
@ -40,7 +40,10 @@ describe('Inputs', () => {
|
|||||||
|
|
||||||
cy.get('ion-checkbox').invoke('prop', 'checked', true);
|
cy.get('ion-checkbox').invoke('prop', 'checked', true);
|
||||||
cy.get('ion-toggle').invoke('prop', 'checked', true);
|
cy.get('ion-toggle').invoke('prop', 'checked', true);
|
||||||
cy.get('ion-input').invoke('prop', 'value', 'hola');
|
|
||||||
|
cy.get('ion-input').eq(0).type('hola');
|
||||||
|
cy.get('ion-input input').eq(0).blur();
|
||||||
|
|
||||||
cy.get('ion-datetime').invoke('prop', 'value', '1996-03-15');
|
cy.get('ion-datetime').invoke('prop', 'value', '1996-03-15');
|
||||||
cy.get('ion-select').invoke('prop', 'value', 'playstation');
|
cy.get('ion-select').invoke('prop', 'value', 'playstation');
|
||||||
cy.get('ion-range').invoke('prop', 'value', 20);
|
cy.get('ion-range').invoke('prop', 'value', 20);
|
||||||
|
8
core/src/components.d.ts
vendored
8
core/src/components.d.ts
vendored
@ -5,7 +5,7 @@
|
|||||||
* It contains typing information for all components that exist in this project.
|
* It contains typing information for all components that exist in this project.
|
||||||
*/
|
*/
|
||||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||||
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
|
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, InputInputEventDetail, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
|
||||||
import { IonicSafeString } from "./utils/sanitization";
|
import { IonicSafeString } from "./utils/sanitization";
|
||||||
import { AlertAttributes } from "./components/alert/alert-interface";
|
import { AlertAttributes } from "./components/alert/alert-interface";
|
||||||
import { CounterFormatter } from "./components/item/item-interface";
|
import { CounterFormatter } from "./components/item/item-interface";
|
||||||
@ -4898,7 +4898,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void;
|
"onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when the value has changed.
|
* The `ionChange` event is fired for `<ion-input>` elements when the user modifies the element's value. Unlike the `ionInput` event, the `ionChange` event is not necessarily fired for each alteration to an element's value. Depending on the way the users interacts with the element, the `ionChange` event fires at a different moment: - When the user commits the change explicitly (e.g. by selecting a date from a date picker for `<ion-input type="date">`, etc.). - When the element loses focus after its value has changed: for elements where the user's interaction is typing.
|
||||||
*/
|
*/
|
||||||
"onIonChange"?: (event: IonInputCustomEvent<InputChangeEventDetail>) => void;
|
"onIonChange"?: (event: IonInputCustomEvent<InputChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
@ -4906,9 +4906,9 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonFocus"?: (event: IonInputCustomEvent<FocusEvent>) => void;
|
"onIonFocus"?: (event: IonInputCustomEvent<FocusEvent>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when a keyboard input occurred.
|
* The `ionInput` event fires when the `value` of an `<ion-input>` element has been changed. For elements that accept text input (`type=text`, `type=tel`, etc.), the interface is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others, the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event).
|
||||||
*/
|
*/
|
||||||
"onIonInput"?: (event: IonInputCustomEvent<InputEvent>) => void;
|
"onIonInput"?: (event: IonInputCustomEvent<InputInputEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when the styles change.
|
* Emitted when the styles change.
|
||||||
*/
|
*/
|
||||||
|
@ -2,6 +2,10 @@ export interface InputChangeEventDetail {
|
|||||||
value: string | undefined | null;
|
value: string | undefined | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We recognize that InputInput is not an ideal naming pattern for this type.
|
||||||
|
// TODO (FW-2199): Explore renaming this type to something more appropriate.
|
||||||
|
export type InputInputEventDetail = InputEvent | Event;
|
||||||
|
|
||||||
export interface InputCustomEvent extends CustomEvent {
|
export interface InputCustomEvent extends CustomEvent {
|
||||||
detail: InputChangeEventDetail;
|
detail: InputChangeEventDetail;
|
||||||
target: HTMLIonInputElement;
|
target: HTMLIonInputElement;
|
||||||
|
@ -6,6 +6,7 @@ import type {
|
|||||||
AutocompleteTypes,
|
AutocompleteTypes,
|
||||||
Color,
|
Color,
|
||||||
InputChangeEventDetail,
|
InputChangeEventDetail,
|
||||||
|
InputInputEventDetail,
|
||||||
StyleEventDetail,
|
StyleEventDetail,
|
||||||
TextFieldTypes,
|
TextFieldTypes,
|
||||||
} from '../../interface';
|
} from '../../interface';
|
||||||
@ -30,6 +31,17 @@ export class Input implements ComponentInterface {
|
|||||||
private didBlurAfterEdit = false;
|
private didBlurAfterEdit = false;
|
||||||
private inheritedAttributes: Attributes = {};
|
private inheritedAttributes: Attributes = {};
|
||||||
private isComposing = false;
|
private isComposing = false;
|
||||||
|
/**
|
||||||
|
* If `true`, the user cleared the input by pressing the clear icon,
|
||||||
|
* within the session of the input being focused.
|
||||||
|
*
|
||||||
|
* This property is reset to `false` when the input is blurred.
|
||||||
|
*/
|
||||||
|
private inputCleared = false;
|
||||||
|
/**
|
||||||
|
* The value of the input when the input is focused.
|
||||||
|
*/
|
||||||
|
private focusedValue: string | number | null | undefined;
|
||||||
|
|
||||||
@State() hasFocus = false;
|
@State() hasFocus = false;
|
||||||
|
|
||||||
@ -192,12 +204,27 @@ export class Input implements ComponentInterface {
|
|||||||
@Prop({ mutable: true }) value?: string | number | null = '';
|
@Prop({ mutable: true }) value?: string | number | null = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a keyboard input occurred.
|
* The `ionInput` event fires when the `value` of an `<ion-input>` element
|
||||||
|
* has been changed.
|
||||||
|
*
|
||||||
|
* For elements that accept text input (`type=text`, `type=tel`, etc.), the interface
|
||||||
|
* is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others,
|
||||||
|
* the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event).
|
||||||
*/
|
*/
|
||||||
@Event() ionInput!: EventEmitter<InputEvent>;
|
@Event() ionInput!: EventEmitter<InputInputEventDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the value has changed.
|
* The `ionChange` event is fired for `<ion-input>` elements when the user
|
||||||
|
* modifies the element's value. Unlike the `ionInput` event, the `ionChange`
|
||||||
|
* event is not necessarily fired for each alteration to an element's value.
|
||||||
|
*
|
||||||
|
* Depending on the way the users interacts with the element, the `ionChange`
|
||||||
|
* event fires at a different moment:
|
||||||
|
* - When the user commits the change explicitly (e.g. by selecting a date
|
||||||
|
* from a date picker for `<ion-input type="date">`, etc.).
|
||||||
|
* - When the element loses focus after its value has changed: for elements
|
||||||
|
* where the user's interaction is typing.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
@Event() ionChange!: EventEmitter<InputChangeEventDetail>;
|
@Event() ionChange!: EventEmitter<InputChangeEventDetail>;
|
||||||
|
|
||||||
@ -244,7 +271,6 @@ export class Input implements ComponentInterface {
|
|||||||
nativeInput.value = value;
|
nativeInput.value = value;
|
||||||
}
|
}
|
||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
this.ionChange.emit({ value: this.value == null ? this.value : this.value.toString() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
@ -310,6 +336,18 @@ export class Input implements ComponentInterface {
|
|||||||
return Promise.resolve(this.nativeInput!);
|
return Promise.resolve(this.nativeInput!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an `ionChange` event.
|
||||||
|
*
|
||||||
|
* This API should be called for user committed changes.
|
||||||
|
* This API should not be used for external value changes.
|
||||||
|
*/
|
||||||
|
private emitValueChange() {
|
||||||
|
const { value } = this;
|
||||||
|
const newValue = value == null ? value : value.toString();
|
||||||
|
this.ionChange.emit({ value: newValue });
|
||||||
|
}
|
||||||
|
|
||||||
private shouldClearOnEdit() {
|
private shouldClearOnEdit() {
|
||||||
const { type, clearOnEdit } = this;
|
const { type, clearOnEdit } = this;
|
||||||
return clearOnEdit === undefined ? type === 'password' : clearOnEdit;
|
return clearOnEdit === undefined ? type === 'password' : clearOnEdit;
|
||||||
@ -338,16 +376,33 @@ export class Input implements ComponentInterface {
|
|||||||
this.ionInput.emit(ev as InputEvent);
|
this.ionInput.emit(ev as InputEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onChange = () => {
|
||||||
|
this.emitValueChange();
|
||||||
|
};
|
||||||
|
|
||||||
private onBlur = (ev: FocusEvent) => {
|
private onBlur = (ev: FocusEvent) => {
|
||||||
this.hasFocus = false;
|
this.hasFocus = false;
|
||||||
this.focusChanged();
|
this.focusChanged();
|
||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
|
|
||||||
|
if (this.inputCleared) {
|
||||||
|
if (this.focusedValue !== this.value) {
|
||||||
|
/**
|
||||||
|
* Emits the `ionChange` event when the input value
|
||||||
|
* is different than the value when the input was focused.
|
||||||
|
*/
|
||||||
|
this.emitValueChange();
|
||||||
|
}
|
||||||
|
this.focusedValue = undefined;
|
||||||
|
this.inputCleared = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.ionBlur.emit(ev);
|
this.ionBlur.emit(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFocus = (ev: FocusEvent) => {
|
private onFocus = (ev: FocusEvent) => {
|
||||||
this.hasFocus = true;
|
this.hasFocus = true;
|
||||||
|
this.focusedValue = this.value;
|
||||||
this.focusChanged();
|
this.focusChanged();
|
||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
|
|
||||||
@ -386,6 +441,9 @@ export class Input implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.value = '';
|
this.value = '';
|
||||||
|
this.inputCleared = true;
|
||||||
|
|
||||||
|
this.ionInput.emit(ev);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is needed for clearOnEdit
|
* This is needed for clearOnEdit
|
||||||
@ -454,6 +512,7 @@ export class Input implements ComponentInterface {
|
|||||||
type={this.type}
|
type={this.type}
|
||||||
value={value}
|
value={value}
|
||||||
onInput={this.onInput}
|
onInput={this.onInput}
|
||||||
|
onChange={this.onChange}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onKeyDown={this.onKeydown}
|
onKeyDown={this.onKeydown}
|
||||||
|
74
core/src/components/input/test/input-events.e2e.ts
Normal file
74
core/src/components/input/test/input-events.e2e.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('input: events: ionChange', () => {
|
||||||
|
test.describe('when the input is blurred', () => {
|
||||||
|
test.describe('should emit', () => {
|
||||||
|
test('if the value has changed', async ({ page }) => {
|
||||||
|
await page.setContent(`<ion-input></ion-input>`);
|
||||||
|
|
||||||
|
const nativeInput = page.locator('ion-input input');
|
||||||
|
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||||
|
|
||||||
|
await nativeInput.type('new value', { delay: 100 });
|
||||||
|
// Value change is not emitted until the control is blurred.
|
||||||
|
await nativeInput.evaluate((e) => e.blur());
|
||||||
|
|
||||||
|
await ionChangeSpy.next();
|
||||||
|
|
||||||
|
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('should not emit', () => {
|
||||||
|
test('if the value has not changed', async ({ page }) => {
|
||||||
|
await page.setContent(`<ion-input value="" clear-input="true"></ion-input>`);
|
||||||
|
|
||||||
|
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||||
|
const nativeInput = page.locator('ion-input input');
|
||||||
|
|
||||||
|
await nativeInput.type('new value', { delay: 100 });
|
||||||
|
|
||||||
|
await page.click('ion-input .input-clear-icon');
|
||||||
|
|
||||||
|
await nativeInput.evaluate((e) => e.blur());
|
||||||
|
|
||||||
|
expect(ionChangeSpy.events.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if the value is set programmatically', async ({ page }) => {
|
||||||
|
await page.setContent(`<ion-input></ion-input>`);
|
||||||
|
|
||||||
|
const input = page.locator('ion-input');
|
||||||
|
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||||
|
|
||||||
|
await input.evaluate((el: HTMLIonInputElement) => {
|
||||||
|
el.value = 'new value';
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ionChangeSpy.events.length).toBe(0);
|
||||||
|
|
||||||
|
// Update the value again to make sure it doesn't emit a second time
|
||||||
|
await input.evaluate((el: HTMLIonInputElement) => {
|
||||||
|
el.value = 'new value 2';
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ionChangeSpy.events.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('input: events: ionInput', () => {
|
||||||
|
test.describe('should emit', () => {
|
||||||
|
test('when the input is cleared', async ({ page }) => {
|
||||||
|
await page.setContent(`<ion-input value="some value" clear-input="true"></ion-input>`);
|
||||||
|
|
||||||
|
const ionInputSpy = await page.spyOnEvent('ionInput');
|
||||||
|
|
||||||
|
await page.click('ion-input .input-clear-icon');
|
||||||
|
|
||||||
|
expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -8,7 +8,7 @@ import type { AnchorInterface, ButtonInterface } from '../../utils/element-inter
|
|||||||
import { raf } from '../../utils/helpers';
|
import { raf } from '../../utils/helpers';
|
||||||
import { printIonError } from '../../utils/logging';
|
import { printIonError } from '../../utils/logging';
|
||||||
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
|
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
|
||||||
import type { InputChangeEventDetail } from '../input/input-interface';
|
import type { InputInputEventDetail } from '../input/input-interface';
|
||||||
|
|
||||||
import type { CounterFormatter } from './item-interface';
|
import type { CounterFormatter } from './item-interface';
|
||||||
|
|
||||||
@ -149,8 +149,8 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
|||||||
this.updateCounterOutput(this.getFirstInput());
|
this.updateCounterOutput(this.getFirstInput());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Listen('ionChange')
|
@Listen('ionInput')
|
||||||
handleIonChange(ev: CustomEvent<InputChangeEventDetail>) {
|
handleIonInput(ev: CustomEvent<InputInputEventDetail>) {
|
||||||
if (this.counter && ev.target === this.getFirstInput()) {
|
if (this.counter && ev.target === this.getFirstInput()) {
|
||||||
this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement);
|
this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ test.describe('item: counter', () => {
|
|||||||
test('should format on input', async ({ page }) => {
|
test('should format on input', async ({ page }) => {
|
||||||
const input = page.locator('#customFormatter ion-input');
|
const input = page.locator('#customFormatter ion-input');
|
||||||
|
|
||||||
await input.click();
|
await page.click('#customFormatter ion-input input');
|
||||||
await input.type('abcde');
|
await input.type('abcde');
|
||||||
|
|
||||||
await page.waitForChanges();
|
await page.waitForChanges();
|
||||||
|
Reference in New Issue
Block a user