CaioQuirinoMedeiros / react-native-currency-input

A simple currency input component for both iOS and Android
https://www.npmjs.com/package/react-native-currency-input
MIT License
156 stars 25 forks source link

Patch for using a custom number formatter inside ... #33

Open ajp8164 opened 7 months ago

ajp8164 commented 7 months ago

This patch adds a new property called customFormatter: (value: number) => string;. If present it replaces the built-in formatter. The value input is the unformatted number value in the component. The output string is sent to the onChangeText() callback. Properties that are used outside of the built-in formatter are still enforced (e.g. precision, maxValue, maxLength, ...)

I can issue a PR if there is interest.

diff --git a/node_modules/react-native-currency-input/lib/typescript/src/props.d.ts b/node_modules/react-native-currency-input/lib/typescript/src/props.d.ts
index 9fcef7a..d6c46b4 100644
--- a/node_modules/react-native-currency-input/lib/typescript/src/props.d.ts
+++ b/node_modules/react-native-currency-input/lib/typescript/src/props.d.ts
@@ -1,118 +1,125 @@
 /// <reference types="react" />
-import type { TextInputProps, StyleProp, ViewStyle, TextProps, TextStyle } from 'react-native';
+
+import type { StyleProp, TextInputProps, TextProps, TextStyle, ViewStyle } from 'react-native';
+
 export interface FormatNumberOptions {
-    /**
-     * Character for thousands delimiter.
-     */
-    delimiter?: string;
-    /**
-     * Set this to `true` to disable negative values.
-     */
-    ignoreNegative?: boolean;
-    /**
-     * Decimal precision. Defaults to 2.
-     */
-    precision?: number;
-    /**
-     * Decimal separator character.
-     */
-    separator?: string;
-    /**
-     * Character to be prefixed on the value.
-     */
-    prefix?: string;
-    /**
-     * Character to be suffixed on the value.
-     */
-    suffix?: string;
-    /**
-     * Set this to `true` to show the `+` character on positive values.
-     */
-    showPositiveSign?: boolean;
-    /**
-     * Where the negative/positive sign (+/-) should be placed. Defaults to "afterPrefix".
-     * Use `showPositiveSign` if you want to show the `+` sign.
-     */
-    signPosition?: 'beforePrefix' | 'afterPrefix';
+  /**
+   * Character for thousands delimiter.
+   */
+  delimiter?: string;
+  /**
+   * Set this to `true` to disable negative values.
+   */
+  ignoreNegative?: boolean;
+  /**
+   * Decimal precision. Defaults to 2.
+   */
+  precision?: number;
+  /**
+   * Decimal separator character.
+   */
+  separator?: string;
+  /**
+   * Character to be prefixed on the value.
+   */
+  prefix?: string;
+  /**
+   * Character to be suffixed on the value.
+   */
+  suffix?: string;
+  /**
+   * Set this to `true` to show the `+` character on positive values.
+   */
+  showPositiveSign?: boolean;
+  /**
+   * Where the negative/positive sign (+/-) should be placed. Defaults to "afterPrefix".
+   * Use `showPositiveSign` if you want to show the `+` sign.
+   */
+  signPosition?: 'beforePrefix' | 'afterPrefix';
 }
 export interface CurrencyInputProps extends Omit<TextInputProps, 'value'> {
-    renderTextInput?: (props: TextInputProps) => JSX.Element;
-    /**
-     * Character for thousands delimiter.
-     */
-    delimiter?: string;
-    /**
-     * Max value allowed on input.
-     * Notice that this might cause unexpected behavior if you pass a value higher than this on input `value`. In that case, consider do your own validation instead of using this property
-     */
-    maxValue?: number;
-    /**
-     * Min value allowed on input.
-     * Notice that this might cause unexpected behavior if you pass a value lower than this on input `value`. In that case, consider do your own validation instead of using this property
-     */
-    minValue?: number;
-    /**
-     * Callback that is called when the input's value changes.
-     * @param value The number value.
-     */
-    onChangeValue?(value: number | null): void;
-    /**
-     * Decimal precision. Defaults to 2.
-     */
-    precision?: number;
-    /**
-     * Decimal separator character.
-     */
-    separator?: string;
-    /**
-     * Character to be prefixed on the value.
-     */
-    prefix?: string;
-    /**
-     * Character to be suffixed on the value.
-     */
-    suffix?: string;
-    /**
-     * @deprecated. Use `prefix` instead.
-     */
-    unit?: string;
-    /**
-     * The number value of the input.
-     * IMPORTANT: This is used to control the component, but keep in mind that this is not the final `value` property of the `TextInput`
-     */
-    value: number | null;
-    /**
-     * Set this to `true` to show the `+` character on positive values.
-     */
-    showPositiveSign?: boolean;
-    /**
-     * Where the negative/positive sign (+/-) should be placed. Defaults to "afterPrefix".
-     * Use `showPositiveSign` if you want to show the `+` sign.
-     */
-    signPosition?: 'beforePrefix' | 'afterPrefix';
+  renderTextInput?: (props: TextInputProps) => JSX.Element;
+  /**
+   * Replaces the built-in formatter. This callback is called after input changes and before onChangeValue().
+   * @param value The number value.
+   */
+  customFormatter?(value: number): string;
+  /**
+   * Character for thousands delimiter.
+   */
+  delimiter?: string;
+  /**
+   * Max value allowed on input.
+   * Notice that this might cause unexpected behavior if you pass a value higher than this on input `value`. In that case, consider do your own validation instead of using this property
+   */
+  maxValue?: number;
+  /**
+   * Min value allowed on input.
+   * Notice that this might cause unexpected behavior if you pass a value lower than this on input `value`. In that case, consider do your own validation instead of using this property
+   */
+  minValue?: number;
+  /**
+   * Callback that is called when the input's value changes.
+   * @param value The number value.
+   */
+  onChangeValue?(value: number | null): void;
+  /**
+   * Decimal precision. Defaults to 2.
+   */
+  precision?: number;
+  /**
+   * Decimal separator character.
+   */
+  separator?: string;
+  /**
+   * Character to be prefixed on the value.
+   */
+  prefix?: string;
+  /**
+   * Character to be suffixed on the value.
+   */
+  suffix?: string;
+  /**
+   * @deprecated. Use `prefix` instead.
+   */
+  unit?: string;
+  /**
+   * The number value of the input.
+   * IMPORTANT: This is used to control the component, but keep in mind that this is not the final `value` property of the `TextInput`
+   */
+  value: number | null;
+  /**
+   * Set this to `true` to show the `+` character on positive values.
+   */
+  showPositiveSign?: boolean;
+  /**
+   * Where the negative/positive sign (+/-) should be placed. Defaults to "afterPrefix".
+   * Use `showPositiveSign` if you want to show the `+` sign.
+   */
+  signPosition?: 'beforePrefix' | 'afterPrefix';
 }
 export interface FakeCurrencyInputProps extends CurrencyInputProps {
-    /**
-     * Style for the container View that wraps the Text
-     */
-    containerStyle?: StyleProp<ViewStyle>;
-    /**
-     * Color of the caret. Defaults to #6495ed
-     */
-    caretColor?: string;
-    /**
-     * Style for the caret text component
-     */
-    caretStyle?: StyleProp<TextStyle>;
+  /**
+   * Style for the container View that wraps the Text
+   */
+  containerStyle?: StyleProp<ViewStyle>;
+  /**
+   * Color of the caret. Defaults to #6495ed
+   */
+  caretColor?: string;
+  /**
+   * Style for the caret text component
+   */
+  caretStyle?: StyleProp<TextStyle>;
 }
 export interface TextWithCursorProps extends TextProps {
-    children?: React.ReactNode;
-    /**
-     * Show or hides the cursor. Defaults to false
-     */
-    cursorVisible?: boolean;
-    /**
-     * Props for the cursor. Use this to set a custom `style` prop.
-     */
-    cursorProps?: TextProps;
+  children?: React.ReactNode;
+  /**
+   * Show or hides the cursor. Defaults to false
+   */
+  cursorVisible?: boolean;
+  /**
+   * Props for the cursor. Use this to set a custom `style` prop.
+   */
+  cursorProps?: TextProps;
 }
diff --git a/node_modules/react-native-currency-input/src/CurrencyInput.tsx b/node_modules/react-native-currency-input/src/CurrencyInput.tsx
index 6c7e844..39e8c4f 100644
--- a/node_modules/react-native-currency-input/src/CurrencyInput.tsx
+++ b/node_modules/react-native-currency-input/src/CurrencyInput.tsx
@@ -15,6 +16,7 @@ export const CurrencyInput = React.forwardRef<TextInput, CurrencyInputProps>(fun
     value,
     onChangeText,
     onChangeValue,
+    customFormatter,
     separator,
     delimiter,
     prefix = '',
@@ -34,16 +35,20 @@ export const CurrencyInput = React.forwardRef<TextInput, CurrencyInputProps>(fun

   const formattedValue = React.useMemo(() => {
     if (!!value || value === 0 || value === -0) {
-      return formatNumber(value, {
-        separator,
-        prefix,
-        suffix,
-        precision,
-        delimiter,
-        ignoreNegative: noNegativeValues,
-        signPosition,
-        showPositiveSign,
-      });
+      if (customFormatter) {
+        return customFormatter(value);
+      } else {
+        return formatNumber(value, {
+          separator,
+          prefix,
+          suffix,
+          precision,
+          delimiter,
+          ignoreNegative: noNegativeValues,
+          signPosition,
+          showPositiveSign,
+        });
+      }
     } else {
       return '';
     }
@@ -120,7 +125,7 @@ export const CurrencyInput = React.forwardRef<TextInput, CurrencyInputProps>(fun

       const zerosOnValue = textNumericValue.replace(/[^0]/g, '').length;

-      let newValue: number | null;
+      let newValue: number | null = numberValue;

       if (!textNumericValue || (!numberValue && zerosOnValue === precision)) {
         // Allow to clean the value instead of beign 0
diff --git a/node_modules/react-native-currency-input/src/props.ts b/node_modules/react-native-currency-input/src/props.ts
index 0c2f069..ece33fe 100644
--- a/node_modules/react-native-currency-input/src/props.ts
+++ b/node_modules/react-native-currency-input/src/props.ts
@@ -51,6 +51,12 @@ export interface FormatNumberOptions {

 export interface CurrencyInputProps extends Omit<TextInputProps, 'value'> {
   renderTextInput?: (props: TextInputProps) => JSX.Element;
+  /**
+   * Replaces the built-in formatter. This callback is called after input changes and before onChangeValue().
+   * @param value The number value.
+   */
+  customFormatter?(value: number): string;
+
   /**
    * Character for thousands delimiter.
    */
CaioQuirinoMedeiros commented 7 months ago

@ajp8164 Thanks for your contribution but... the whole point of this lib is the formatNumber function, why would you want to use a customFormatter? If someone needs a custom formatter, why not just use it with the standard TextInput?

ajp8164 commented 7 months ago

It's a fair point. I've already turtled this lib into my stack and adding this small extension saves a lot of work having to extend other middleware libs. I may reconsider my implementation but this is cheaper and faster for sure. Better? idk. One use case is RTL entry for hh:mm:ss.

UPDATE - reviewing the implementation of this library and looking at some of the available masking libraries I believe this is the best library for my use cases (just sort of a nit that this library is named and focused on only currency). If this library was forked and renamed for more general formatting then I can see multiple built-in formatters plus a custom callback. The RTL input (TextWithCursor) is a nice feature. Using this patch I added a formatter as follows to handle entering sec, min, then hrs. With this formatter I get a mask input as well.

export const maskToHMS = (value: number) => {
  return value.toString().padStart(6, '0').split(/(?=(?:..)*$)/).join(':');
};

Anyway, I like the library! Thanks!