Skip to content

Commit

Permalink
feat(item): counter formatter to customize counter text display (#24336)
Browse files Browse the repository at this point in the history
Resolves #24327
  • Loading branch information
sean-perkins committed Mar 14, 2022
1 parent bc4cad3 commit 171020e
Show file tree
Hide file tree
Showing 16 changed files with 497 additions and 52 deletions.
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,13 +834,13 @@ export declare interface IonItem extends Components.IonItem {}

@ProxyCmp({
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({
selector: 'ion-item',
changeDetection: ChangeDetectionStrategy.OnPush,
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 {
protected el: HTMLElement;
Expand Down
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ ion-item,shadow
ion-item,prop,button,boolean,false,false,false
ion-item,prop,color,string | undefined,undefined,false,true
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,detailIcon,string,chevronForward,false,false
ion-item,prop,disabled,boolean,false,false,false
Expand Down
9 changes: 9 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { IonicSafeString } from "./utils/sanitization";
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 { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces";
import { PinFormatter } from "./components/range/range-interface";
Expand Down Expand Up @@ -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`.
*/
"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.
*/
Expand Down Expand Up @@ -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`.
*/
"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.
*/
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/item/item-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export type CounterFormatter = (inputLength: number, maxLength: number) => string;
37 changes: 33 additions & 4 deletions core/src/components/item/item.tsx
Original file line number Diff line number Diff line change
@@ -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 { getIonMode } from '../../global/ionic-global';
Expand All @@ -8,6 +9,8 @@ import { raf } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
import { InputChangeEventDetail } from '../input/input-interface';

import { CounterFormatter } from './item-interface';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
Expand Down Expand Up @@ -134,8 +137,19 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
*/
@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;

@Watch('counterFormatter')
counterFormatterChanged() {
this.updateCounterOutput(this.getFirstInput());
}

@Listen('ionChange')
handleIonChange(ev: CustomEvent<InputChangeEventDetail>) {
if (this.counter && ev.target === this.getFirstInput()) {
Expand Down Expand Up @@ -296,12 +310,27 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
}

private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) {
if (this.counter && !this.multipleInputs && inputEl?.maxlength !== undefined) {
const length = inputEl?.value?.toString().length ?? '0';
this.counterString = `${length} / ${inputEl.maxlength}`;
const { counter, counterFormatter, defaultCounterFormatter } = this;
if (counter && !this.multipleInputs && inputEl?.maxlength !== undefined) {
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() {
const startEl = this.el.querySelector('[slot="start"]');
if (startEl !== null) {
Expand Down

0 comments on commit 171020e

Please sign in to comment.