Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const ExampleForm = ({
<br />
<input type="checkbox" name="disableSelectionHandling" defaultChecked={disableSelectionHandling} /> Disable selection handling
<br />
{/* <input type="text" name="value" placeholder="Value" defaultValue={value} />Value<br /> */}
<input type="text" name="value" placeholder="Value" defaultValue={value} />Value<br />
<input
type="text"
name="decimalSeparator"
Expand Down Expand Up @@ -163,7 +163,7 @@ function FormContainer() {
setDisableSelectionHandling(
document.getElementsByName("disableSelectionHandling")[0].checked
);
// setValue(document.getElementsByName('value')[0].value);
setValue(document.getElementsByName('value')[0].value);
setDecimalSeparator(document.getElementsByName("decimalSeparator")[0].value);
setThousandSeparator(
document.getElementsByName("thousandSeparator")[0].value
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ericblade/react-currency-input",
"version": "1.4.4",
"version": "1.4.5",
"description": "React component for inputting currency amounts",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down Expand Up @@ -71,4 +71,4 @@
"publishConfig": {
"access": "public"
}
}
}
10 changes: 10 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export default defineConfig({
testMatch: '**/mask.spec.ts',
dependencies: ['base tests'],
},
{
name: 'controlled-value tests',
testMatch: '**/controlled-value.spec.ts',
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new 'controlled-value tests' project doesn't specify any dependencies, unlike other test projects like 'mask tests' which depends on 'base tests'. If there are any setup requirements or if these tests should run after base tests complete, consider adding a dependencies array to ensure proper test execution order.

Suggested change
testMatch: '**/controlled-value.spec.ts',
testMatch: '**/controlled-value.spec.ts',
dependencies: ['base tests'],

Copilot uses AI. Check for mistakes.
dependencies: ['base tests'],
},
{
name: 'input-type tests',
testMatch: '**/input-type.spec.ts',
dependencies: ['base tests'],
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
Expand Down
90 changes: 69 additions & 21 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type CurrencyInputState = {
disableSelectionHandling: boolean,
maskedValue: string,
value: number | string, // TODO: should be string? should also have a separate float field for 'pennies'
previousProps?: Readonly<CurrencyInputProps>, // Track previous props to detect changes
};

type SelectionSnapshot = {
Expand Down Expand Up @@ -124,22 +125,23 @@ class CurrencyInput extends React.Component<CurrencyInputProps, CurrencyInputSta
* General function used to cleanup and define the final props used for rendering
* @returns CurrencyInputState
*/
static prepareProps({
onChangeEvent,
value: propValue,
decimalSeparator,
thousandSeparator,
precision,
inputType,
allowNegative,
allowEmpty,
prefix,
suffix,
selectAllOnFocus,
autoFocus,
disableSelectionHandling: propDisableSelectionHandling,
...customProps
}: Readonly<CurrencyInputProps>): CurrencyInputState {
static prepareProps(props: Readonly<CurrencyInputProps>): CurrencyInputState {
const {
onChangeEvent,
value: propValue,
decimalSeparator,
thousandSeparator,
precision,
inputType,
allowNegative,
allowEmpty,
prefix,
suffix,
selectAllOnFocus,
autoFocus,
disableSelectionHandling: propDisableSelectionHandling,
...customProps
} = props;
let initialValue = propValue;
if (initialValue === null) {
initialValue = allowEmpty ? null : '';
Expand All @@ -166,7 +168,7 @@ class CurrencyInput extends React.Component<CurrencyInputProps, CurrencyInputSta
);

const disableSelectionHandling = propDisableSelectionHandling || inputType === 'number';
return { maskedValue, value, customProps, disableSelectionHandling };
return { maskedValue, value, customProps, disableSelectionHandling, previousProps: props };
}

/**
Expand All @@ -182,11 +184,57 @@ class CurrencyInput extends React.Component<CurrencyInputProps, CurrencyInputSta
* @see https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
*/
static getDerivedStateFromProps(nextProps: Readonly<CurrencyInputProps>, prevState: Readonly<CurrencyInputState>) {
const props = { ...nextProps };
if (nextProps.value !== prevState.value) {
props.value = prevState.value;
const previousProps = prevState.previousProps || nextProps; // First call uses the initial props snapshot

// Check if the VALUE prop itself changed (parent is controlling the input)
const valueChanged = nextProps.value !== previousProps.value;

// Check if allowNegative changed (affects whether negative values are allowed)
const allowNegativeChanged = nextProps.allowNegative !== previousProps.allowNegative;

// Check if separators or display formatting changed (these require reformatting the current value)
const formatChanged =
nextProps.decimalSeparator !== previousProps.decimalSeparator ||
nextProps.thousandSeparator !== previousProps.thousandSeparator ||
nextProps.precision !== previousProps.precision ||
nextProps.prefix !== previousProps.prefix ||
nextProps.suffix !== previousProps.suffix ||
allowNegativeChanged;

if (valueChanged) {
// Parent changed the value prop - use the new value
const newState = CurrencyInput.prepareProps(nextProps);
return { ...newState, previousProps: nextProps };
}
return CurrencyInput.prepareProps(props);

if (formatChanged) {
// Display formatting or allowNegative changed - reformat the current value
// First, determine the value to use. Start with the current state value,
// which may be adjusted below if allowNegative was disabled and value is negative.
let valueToFormat = prevState.value;

if (allowNegativeChanged && !nextProps.allowNegative) {
// allowNegative was disabled - if current value is negative, make it positive
const parsedValue = typeof prevState.value === 'number'
? prevState.value
: prevState.value === null
? 0
: CurrencyInput.stringValueToFloat(String(prevState.value), nextProps.thousandSeparator, nextProps.decimalSeparator);

if (parsedValue < 0) {
valueToFormat = Math.abs(parsedValue);
}
}

// Reformat with new formatting and potentially adjusted value
const propsWithCurrentValue = { ...nextProps, value: valueToFormat };
const newState = CurrencyInput.prepareProps(propsWithCurrentValue);
return { ...newState, previousProps: nextProps };
}

// Other props changed but value and display formatting didn't
// Just update the previousProps reference and preserve current state
return { ...prevState, previousProps: nextProps };
}

/**
Expand Down
Loading