mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
feat(item): counter formatter to customize counter text display (#24336)
Resolves #24327
This commit is contained in:
@ -834,13 +834,13 @@ export declare interface IonItem extends Components.IonItem {}
|
|||||||
|
|
||||||
@ProxyCmp({
|
@ProxyCmp({
|
||||||
defineCustomElementFn: undefined,
|
defineCustomElementFn: undefined,
|
||||||
inputs: ['button', 'color', 'counter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type']
|
inputs: ['button', 'color', 'counter', 'counterFormatter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type']
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ion-item',
|
selector: 'ion-item',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: '<ng-content></ng-content>',
|
template: '<ng-content></ng-content>',
|
||||||
inputs: ['button', 'color', 'counter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type']
|
inputs: ['button', 'color', 'counter', 'counterFormatter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type']
|
||||||
})
|
})
|
||||||
export class IonItem {
|
export class IonItem {
|
||||||
protected el: HTMLElement;
|
protected el: HTMLElement;
|
||||||
|
@ -557,6 +557,7 @@ ion-item,shadow
|
|||||||
ion-item,prop,button,boolean,false,false,false
|
ion-item,prop,button,boolean,false,false,false
|
||||||
ion-item,prop,color,string | undefined,undefined,false,true
|
ion-item,prop,color,string | undefined,undefined,false,true
|
||||||
ion-item,prop,counter,boolean,false,false,false
|
ion-item,prop,counter,boolean,false,false,false
|
||||||
|
ion-item,prop,counterFormatter,((inputLength: number, maxLength: number) => string) | undefined,undefined,false,false
|
||||||
ion-item,prop,detail,boolean | undefined,undefined,false,false
|
ion-item,prop,detail,boolean | undefined,undefined,false,false
|
||||||
ion-item,prop,detailIcon,string,chevronForward,false,false
|
ion-item,prop,detailIcon,string,chevronForward,false,false
|
||||||
ion-item,prop,disabled,boolean,false,false,false
|
ion-item,prop,disabled,boolean,false,false,false
|
||||||
|
9
core/src/components.d.ts
vendored
9
core/src/components.d.ts
vendored
@ -8,6 +8,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
|||||||
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, 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, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, 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 { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces";
|
import { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces";
|
||||||
import { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces";
|
import { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces";
|
||||||
import { PinFormatter } from "./components/range/range-interface";
|
import { PinFormatter } from "./components/range/range-interface";
|
||||||
@ -1138,6 +1139,10 @@ export namespace Components {
|
|||||||
* If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`.
|
* If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`.
|
||||||
*/
|
*/
|
||||||
"counter": boolean;
|
"counter": boolean;
|
||||||
|
/**
|
||||||
|
* A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength".
|
||||||
|
*/
|
||||||
|
"counterFormatter"?: CounterFormatter;
|
||||||
/**
|
/**
|
||||||
* If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present.
|
* If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present.
|
||||||
*/
|
*/
|
||||||
@ -4872,6 +4877,10 @@ declare namespace LocalJSX {
|
|||||||
* If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`.
|
* If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`.
|
||||||
*/
|
*/
|
||||||
"counter"?: boolean;
|
"counter"?: boolean;
|
||||||
|
/**
|
||||||
|
* A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength".
|
||||||
|
*/
|
||||||
|
"counterFormatter"?: CounterFormatter;
|
||||||
/**
|
/**
|
||||||
* If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present.
|
* If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present.
|
||||||
*/
|
*/
|
||||||
|
2
core/src/components/item/item-interface.ts
Normal file
2
core/src/components/item/item-interface.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
export type CounterFormatter = (inputLength: number, maxLength: number) => string;
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, ComponentInterface, Element, Host, Listen, Prop, State, forceUpdate, h } from '@stencil/core';
|
import { Component, ComponentInterface, Element, Host, Listen, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||||
|
import { printIonError } from '@utils/logging';
|
||||||
import { chevronForward } from 'ionicons/icons';
|
import { chevronForward } from 'ionicons/icons';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
@ -8,6 +9,8 @@ import { raf } from '../../utils/helpers';
|
|||||||
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
|
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
|
||||||
import { InputChangeEventDetail } from '../input/input-interface';
|
import { InputChangeEventDetail } from '../input/input-interface';
|
||||||
|
|
||||||
|
import { CounterFormatter } from './item-interface';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||||
*
|
*
|
||||||
@ -134,8 +137,19 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
|||||||
*/
|
*/
|
||||||
@Prop() type: 'submit' | 'reset' | 'button' = 'button';
|
@Prop() type: 'submit' | 'reset' | 'button' = 'button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback used to format the counter text.
|
||||||
|
* By default the counter text is set to "itemLength / maxLength".
|
||||||
|
*/
|
||||||
|
@Prop() counterFormatter?: CounterFormatter;
|
||||||
|
|
||||||
@State() counterString: string | null | undefined;
|
@State() counterString: string | null | undefined;
|
||||||
|
|
||||||
|
@Watch('counterFormatter')
|
||||||
|
counterFormatterChanged() {
|
||||||
|
this.updateCounterOutput(this.getFirstInput());
|
||||||
|
}
|
||||||
|
|
||||||
@Listen('ionChange')
|
@Listen('ionChange')
|
||||||
handleIonChange(ev: CustomEvent<InputChangeEventDetail>) {
|
handleIonChange(ev: CustomEvent<InputChangeEventDetail>) {
|
||||||
if (this.counter && ev.target === this.getFirstInput()) {
|
if (this.counter && ev.target === this.getFirstInput()) {
|
||||||
@ -296,11 +310,26 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) {
|
private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) {
|
||||||
if (this.counter && !this.multipleInputs && inputEl?.maxlength !== undefined) {
|
const { counter, counterFormatter, defaultCounterFormatter } = this;
|
||||||
const length = inputEl?.value?.toString().length ?? '0';
|
if (counter && !this.multipleInputs && inputEl?.maxlength !== undefined) {
|
||||||
this.counterString = `${length} / ${inputEl.maxlength}`;
|
const length = inputEl?.value?.toString().length ?? 0;
|
||||||
|
if (counterFormatter === undefined) {
|
||||||
|
this.counterString = defaultCounterFormatter(length, inputEl.maxlength);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.counterString = counterFormatter(length, inputEl.maxlength);
|
||||||
|
} catch (e) {
|
||||||
|
printIonError('Exception in provided `counterFormatter`.', e);
|
||||||
|
// Fallback to the default counter formatter when an exception happens
|
||||||
|
this.counterString = defaultCounterFormatter(length, inputEl.maxlength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private defaultCounterFormatter(length: number, maxlength: number) {
|
||||||
|
return `${length} / ${maxlength}`;
|
||||||
|
}
|
||||||
|
|
||||||
private hasStartEl() {
|
private hasStartEl() {
|
||||||
const startEl = this.el.querySelector('[slot="start"]');
|
const startEl = this.el.querySelector('[slot="start"]');
|
||||||
|
@ -53,6 +53,10 @@ Items containing an input will highlight the bottom border of the input with a d
|
|||||||
|
|
||||||
The highlight color changes based on the item state, but all of the states use Ionic colors by default. When focused, the input highlight will use the `primary` color. If the input is valid it will use the `success` color, and invalid inputs will use the `danger` color. See the [CSS Custom Properties](#css-custom-properties) section below for the highlight color variables.
|
The highlight color changes based on the item state, but all of the states use Ionic colors by default. When focused, the input highlight will use the `primary` color. If the input is valid it will use the `success` color, and invalid inputs will use the `danger` color. See the [CSS Custom Properties](#css-custom-properties) section below for the highlight color variables.
|
||||||
|
|
||||||
|
### Counter Formatter
|
||||||
|
|
||||||
|
When using `counter`, the default behavior is to format the value that gets displayed as `itemLength / maxLength`. This behavior can be customized by passing in a formatter function to the `counterFormatter` property. See the [Usage](#usage) section for an example.
|
||||||
|
|
||||||
|
|
||||||
<!-- Auto Generated Below -->
|
<!-- Auto Generated Below -->
|
||||||
|
|
||||||
@ -397,6 +401,39 @@ The highlight color changes based on the item state, but all of the states use I
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item [counter]="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item [counter]="true" [counterFormatter]="counterFormatter">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({…})
|
||||||
|
export class MyComponent {
|
||||||
|
|
||||||
|
counterFormatter(inputLength: number, maxLength: number) {
|
||||||
|
return `${maxLength - inputLength} characters remaining`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Javascript
|
### Javascript
|
||||||
|
|
||||||
@ -736,6 +773,30 @@ The highlight color changes based on the item state, but all of the states use I
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item counter="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item counter="true" id="custom-item">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const customItem = document.querySelector('#custom-item');
|
||||||
|
customItem.counterFormatter = (inputLength, maxLength) => `${maxLength - inputLength} characters remaining`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### React
|
### React
|
||||||
|
|
||||||
@ -1058,6 +1119,18 @@ export const ItemExamples: React.FC = () => {
|
|||||||
<IonLabel>Range</IonLabel>
|
<IonLabel>Range</IonLabel>
|
||||||
<IonRange></IonRange>
|
<IonRange></IonRange>
|
||||||
</IonItem>
|
</IonItem>
|
||||||
|
|
||||||
|
{/*-- Item Counter --*/}
|
||||||
|
<IonItem counter={true}>
|
||||||
|
<IonLabel>Counter</IonLabel>
|
||||||
|
<IonInput maxlength="20"></IonInput>
|
||||||
|
</IonItem>
|
||||||
|
|
||||||
|
{/*-- Item Counter Formatter --*/}
|
||||||
|
<IonItem counter={true} counterFormatter={(inputLength, maxLength) => `${maxLength - inputLength} characters remaining`}>
|
||||||
|
<IonLabel>Counter</IonLabel>
|
||||||
|
<IonInput maxlength="20"></IonInput>
|
||||||
|
</IonItem>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
);
|
);
|
||||||
@ -1509,6 +1582,48 @@ export class ItemExample {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Component, h } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'item-example',
|
||||||
|
styleUrl: 'item-example.css'
|
||||||
|
})
|
||||||
|
export class ItemExample {
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-item counter={true}>
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Component, h } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'item-example',
|
||||||
|
styleUrl: 'item-example.css'
|
||||||
|
})
|
||||||
|
export class ItemExample {
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-item counter="true" counterFormatter={(inputLength, maxLength) => `${maxLength - inputLength} characters remaining`}>
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Vue
|
### Vue
|
||||||
|
|
||||||
@ -1925,15 +2040,51 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<ion-item :counter="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<ion-item :counter="true" :counter-formatter="counterFormatter">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IonInput, IonItem, IonLabel } from '@ionic/vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { IonItem, IonLabel, IonInput },
|
||||||
|
setup() {
|
||||||
|
const counterFormatter = (inputLength: number, maxLength: number) => `${maxLength - inputLength} characters remaining`;
|
||||||
|
|
||||||
|
return { counterFormatter };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
| Property | Attribute | Description | Type | Default |
|
| Property | Attribute | Description | Type | Default |
|
||||||
| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------- |
|
| ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------- |
|
||||||
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
|
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
|
||||||
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
|
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
|
||||||
| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` |
|
| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` |
|
||||||
|
| `counterFormatter` | -- | A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". | `((inputLength: number, maxLength: number) => string) \| undefined` | `undefined` |
|
||||||
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` |
|
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` |
|
||||||
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `chevronForward` |
|
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `chevronForward` |
|
||||||
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
|
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
|
||||||
|
@ -1,19 +1,111 @@
|
|||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { E2EPage, newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
test('item: counter', async () => {
|
describe('item: counter', () => {
|
||||||
|
|
||||||
|
it('should match existing visual screenshots', async () => {
|
||||||
const page = await newE2EPage({
|
const page = await newE2EPage({
|
||||||
url: '/src/components/item/test/counter?ionic:_testing=true'
|
url: '/src/components/item/test/counter?ionic:_testing=true'
|
||||||
});
|
});
|
||||||
|
|
||||||
const compare = await page.compareScreenshot();
|
const compare = await page.compareScreenshot();
|
||||||
expect(compare).toMatchScreenshot();
|
expect(compare).toMatchScreenshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('item: counter-rtl', async () => {
|
|
||||||
const page = await newE2EPage({
|
|
||||||
url: '/src/components/item/test/counter?ionic:_testing=true&rtl=true'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const compare = await page.compareScreenshot();
|
describe('custom formatter', () => {
|
||||||
expect(compare).toMatchScreenshot();
|
|
||||||
});
|
let page: E2EPage;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await newE2EPage({
|
||||||
|
url: '/src/components/item/test/counter?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format on load', async () => {
|
||||||
|
const itemCounter = await page.find('#customFormatter >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('20 characters left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format on input', async () => {
|
||||||
|
const input = await page.find('#customFormatter ion-input');
|
||||||
|
|
||||||
|
await input.click();
|
||||||
|
await input.type('abcde');
|
||||||
|
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
const itemCounter = await page.find('#customFormatter >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('15 characters left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format after changing the counterFormatter', async () => {
|
||||||
|
let itemCounter = await page.find('#customFormatter >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('20 characters left');
|
||||||
|
|
||||||
|
await page.$eval('#customFormatter', (el: any) => {
|
||||||
|
el.counterFormatter = () => {
|
||||||
|
return 'test label';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
itemCounter = await page.find('#customFormatter >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('test label');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an exception occurs', () => {
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await newE2EPage({
|
||||||
|
html: `
|
||||||
|
<ion-item counter="true"">
|
||||||
|
<ion-input maxlength="20" value=""></ion-input>
|
||||||
|
</ion-item>`
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('console', ev => {
|
||||||
|
if (ev.type() === 'error') {
|
||||||
|
logs.push(ev.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let itemCounter = await page.find('ion-item >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('0 / 20');
|
||||||
|
|
||||||
|
await page.$eval('ion-item', (el: any) => {
|
||||||
|
el.counterFormatter = () => {
|
||||||
|
throw new Error('This is an expected error');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default the formatting to length / maxlength', async () => {
|
||||||
|
const input = await page.find('ion-input');
|
||||||
|
|
||||||
|
await input.click();
|
||||||
|
await input.type('abcde');
|
||||||
|
|
||||||
|
const itemCounter = await page.find('ion-item >>> .item-counter');
|
||||||
|
|
||||||
|
expect(itemCounter.textContent).toBe('5 / 20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log an error', () => {
|
||||||
|
expect(logs.length).toBeGreaterThan(0);
|
||||||
|
expect(logs[0]).toMatch('[Ionic Error]: Exception in provided `counterFormatter`.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
@ -26,17 +26,29 @@
|
|||||||
|
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item counter="true">
|
<ion-item counter="true">
|
||||||
<ion-label>Counter</ion-label>
|
<ion-label position="stacked">Counter</ion-label>
|
||||||
<ion-input maxlength="20"></ion-input>
|
<ion-input maxlength="20"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item counter="true">
|
<ion-item counter="true">
|
||||||
<ion-label>Counter with value</ion-label>
|
<ion-label position="stacked">Counter with value</ion-label>
|
||||||
<ion-input maxlength="20" value="some value"></ion-input>
|
<ion-input maxlength="20" value="some value"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item counter="true" id="customFormatter">
|
||||||
|
<ion-label position="stacked">Counter with custom formatter</ion-label>
|
||||||
|
<ion-input maxlength="20" value=""></ion-input>
|
||||||
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const customFormatter = document.getElementById('customFormatter');
|
||||||
|
customFormatter.counterFormatter = (inputLength, maxLength) => {
|
||||||
|
return `${maxLength - inputLength} characters left`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -333,3 +333,36 @@
|
|||||||
<ion-range></ion-range>
|
<ion-range></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item [counter]="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item [counter]="true" [counterFormatter]="counterFormatter">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({…})
|
||||||
|
export class MyComponent {
|
||||||
|
|
||||||
|
counterFormatter(inputLength: number, maxLength: number) {
|
||||||
|
return `${maxLength - inputLength} characters remaining`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
@ -333,3 +333,27 @@
|
|||||||
<ion-range></ion-range>
|
<ion-range></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item counter="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ion-item counter="true" id="custom-item">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const customItem = document.querySelector('#custom-item');
|
||||||
|
customItem.counterFormatter = (inputLength, maxLength) => `${maxLength - inputLength} characters remaining`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
```
|
@ -317,6 +317,18 @@ export const ItemExamples: React.FC = () => {
|
|||||||
<IonLabel>Range</IonLabel>
|
<IonLabel>Range</IonLabel>
|
||||||
<IonRange></IonRange>
|
<IonRange></IonRange>
|
||||||
</IonItem>
|
</IonItem>
|
||||||
|
|
||||||
|
{/*-- Item Counter --*/}
|
||||||
|
<IonItem counter={true}>
|
||||||
|
<IonLabel>Counter</IonLabel>
|
||||||
|
<IonInput maxlength="20"></IonInput>
|
||||||
|
</IonItem>
|
||||||
|
|
||||||
|
{/*-- Item Counter Formatter --*/}
|
||||||
|
<IonItem counter={true} counterFormatter={(inputLength, maxLength) => `${maxLength - inputLength} characters remaining`}>
|
||||||
|
<IonLabel>Counter</IonLabel>
|
||||||
|
<IonInput maxlength="20"></IonInput>
|
||||||
|
</IonItem>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
);
|
);
|
||||||
|
@ -439,3 +439,45 @@ export class ItemExample {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Component, h } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'item-example',
|
||||||
|
styleUrl: 'item-example.css'
|
||||||
|
})
|
||||||
|
export class ItemExample {
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-item counter={true}>
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Component, h } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'item-example',
|
||||||
|
styleUrl: 'item-example.css'
|
||||||
|
})
|
||||||
|
export class ItemExample {
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-item counter="true" counterFormatter={(inputLength, maxLength) => `${maxLength - inputLength} characters remaining`}>
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -410,3 +410,38 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Item Counter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<ion-item :counter="true">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Counter Formatter
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<ion-item :counter="true" :counter-formatter="counterFormatter">
|
||||||
|
<ion-label>Counter</ion-label>
|
||||||
|
<ion-input maxlength="20"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IonInput, IonItem, IonLabel } from '@ionic/vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { IonItem, IonLabel, IonInput },
|
||||||
|
setup() {
|
||||||
|
const counterFormatter = (inputLength: number, maxLength: number) => `${maxLength - inputLength} characters remaining`;
|
||||||
|
|
||||||
|
return { counterFormatter };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
21
core/src/interface.d.ts
vendored
21
core/src/interface.d.ts
vendored
@ -11,6 +11,7 @@ export * from './components/checkbox/checkbox-interface';
|
|||||||
export * from './components/datetime/datetime-interface';
|
export * from './components/datetime/datetime-interface';
|
||||||
export * from './components/infinite-scroll/infinite-scroll-interface';
|
export * from './components/infinite-scroll/infinite-scroll-interface';
|
||||||
export * from './components/input/input-interface';
|
export * from './components/input/input-interface';
|
||||||
|
export * from './components/item/item-interface';
|
||||||
export * from './components/item-sliding/item-sliding-interface';
|
export * from './components/item-sliding/item-sliding-interface';
|
||||||
export * from './components/loading/loading-interface';
|
export * from './components/loading/loading-interface';
|
||||||
export * from './components/menu/menu-interface';
|
export * from './components/menu/menu-interface';
|
||||||
@ -43,13 +44,13 @@ export { Gesture, GestureConfig, GestureDetail } from './utils/gesture';
|
|||||||
|
|
||||||
// From: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
|
// From: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
|
||||||
export type AutocompleteTypes = (
|
export type AutocompleteTypes = (
|
||||||
| 'on' | 'off' | 'name' | 'honorific-prefix' | 'given-name' | 'additional-name' | 'family-name' | 'honorific-suffix'
|
| 'on' | 'off' | 'name' | 'honorific-prefix' | 'given-name' | 'additional-name' | 'family-name' | 'honorific-suffix'
|
||||||
| 'nickname' | 'email' | 'username' | 'new-password' | 'current-password' | 'one-time-code' | 'organization-title' | 'organization'
|
| 'nickname' | 'email' | 'username' | 'new-password' | 'current-password' | 'one-time-code' | 'organization-title' | 'organization'
|
||||||
| 'street-address' | 'address-line1' | 'address-line2' | 'address-line3' | 'address-level4' | 'address-level3' | 'address-level2'
|
| 'street-address' | 'address-line1' | 'address-line2' | 'address-line3' | 'address-level4' | 'address-level3' | 'address-level2'
|
||||||
| 'address-level1' | 'country' | 'country-name' | 'postal-code' | 'cc-name' | 'cc-given-name' | 'cc-additional-name' | 'cc-family-name'
|
| 'address-level1' | 'country' | 'country-name' | 'postal-code' | 'cc-name' | 'cc-given-name' | 'cc-additional-name' | 'cc-family-name'
|
||||||
| 'cc-family-name' | 'cc-number' | 'cc-exp' | 'cc-exp-month' | 'cc-exp-year' | 'cc-csc' | 'cc-type' | 'transaction-currency' | 'transaction-amount'
|
| 'cc-family-name' | 'cc-number' | 'cc-exp' | 'cc-exp-month' | 'cc-exp-year' | 'cc-csc' | 'cc-type' | 'transaction-currency' | 'transaction-amount'
|
||||||
| 'language' | 'bday' | 'bday-day' | 'bday-month' | 'bday-year' | 'sex' | 'tel' | 'tel-country-code' | 'tel-national' | 'tel-area-code' | 'tel-local'
|
| 'language' | 'bday' | 'bday-day' | 'bday-month' | 'bday-year' | 'sex' | 'tel' | 'tel-country-code' | 'tel-national' | 'tel-area-code' | 'tel-local'
|
||||||
| 'tel-extension' | 'impp' | 'url' | 'photo');
|
| 'tel-extension' | 'impp' | 'url' | 'photo');
|
||||||
|
|
||||||
|
|
||||||
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time' | 'week' | 'month' | 'datetime-local';
|
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time' | 'week' | 'month' | 'datetime-local';
|
||||||
@ -59,7 +60,7 @@ export type Color = PredefinedColors | string;
|
|||||||
export type Mode = "ios" | "md";
|
export type Mode = "ios" | "md";
|
||||||
export type ComponentTags = string;
|
export type ComponentTags = string;
|
||||||
export type ComponentRef = Function | HTMLElement | string | null;
|
export type ComponentRef = Function | HTMLElement | string | null;
|
||||||
export type ComponentProps<T = null> = {[key: string]: any};
|
export type ComponentProps<T = null> = { [key: string]: any };
|
||||||
export type CssClassMap = { [className: string]: boolean };
|
export type CssClassMap = { [className: string]: boolean };
|
||||||
export type BackButtonEvent = CustomEvent<BackButtonEventDetail>;
|
export type BackButtonEvent = CustomEvent<BackButtonEventDetail>;
|
||||||
|
|
||||||
@ -84,12 +85,12 @@ export { NavComponentWithProps } from "./components/nav/nav-interface";
|
|||||||
|
|
||||||
declare module "./components" {
|
declare module "./components" {
|
||||||
export namespace Components {
|
export namespace Components {
|
||||||
export interface IonIcon extends IoniconsComponents.IonIcon{}
|
export interface IonIcon extends IoniconsComponents.IonIcon { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "./components" {
|
declare module "./components" {
|
||||||
export namespace JSX {
|
export namespace JSX {
|
||||||
export interface IonIcon extends IoniconsJSX.IonIcon {}
|
export interface IonIcon extends IoniconsJSX.IonIcon { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,7 +262,8 @@ export const config: Config = {
|
|||||||
pixelmatchThreshold: 0.05,
|
pixelmatchThreshold: 0.05,
|
||||||
waitBeforeScreenshot: 20,
|
waitBeforeScreenshot: 20,
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"@utils/test": ["<rootDir>/src/utils/test/utils"]
|
"@utils/test": ["<rootDir>/src/utils/test/utils"],
|
||||||
|
"@utils/logging": ["<rootDir>/src/utils/logging"]
|
||||||
},
|
},
|
||||||
emulate: [
|
emulate: [
|
||||||
{
|
{
|
||||||
|
@ -429,7 +429,8 @@ export const IonItem = /*@__PURE__*/ defineContainer<JSX.IonItem>('ion-item', de
|
|||||||
'routerAnimation',
|
'routerAnimation',
|
||||||
'routerDirection',
|
'routerDirection',
|
||||||
'target',
|
'target',
|
||||||
'type'
|
'type',
|
||||||
|
'counterFormatter'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user