feat(item): counter formatter to customize counter text display (#24336)

Resolves #24327
This commit is contained in:
Sean Perkins
2022-03-14 16:38:37 -04:00
committed by GitHub
parent bc4cad3e7b
commit 171020e9d2
16 changed files with 497 additions and 52 deletions

View File

@ -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;

View File

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

View File

@ -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.
*/ */

View File

@ -0,0 +1,2 @@
export type CounterFormatter = (inputLength: number, maxLength: number) => string;

View File

@ -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"]');

View File

@ -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` |

View File

@ -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`.');
});
});
});
})

View File

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

View File

@ -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`;
}
}
```

View File

@ -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>
```

View File

@ -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>
); );

View File

@ -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>
];
}
}
```

View File

@ -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>
```

View File

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

View File

@ -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: [
{ {

View File

@ -429,7 +429,8 @@ export const IonItem = /*@__PURE__*/ defineContainer<JSX.IonItem>('ion-item', de
'routerAnimation', 'routerAnimation',
'routerDirection', 'routerDirection',
'target', 'target',
'type' 'type',
'counterFormatter'
]); ]);