Fabien Franzen
Designer & Developer
Fabien is located at Belgium
Visit Fabien Franzen's profile
Parse, format and mask numeric values with ease
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useId } from '@reach/auto-id';
import { FormField } from '@sanity/base/components';
import { TextInput } from '@sanity/ui';
import PatchEvent, { set } from 'part:@sanity/form-builder/patch-event';
import NumberFormat from 'react-number-format';
const INPUT_PROPS = [
'value',
'onChange',
'onKeyDown',
'onMouseUp',
'onFocus',
'onBlur',
];
const NUMBER_FORMAT_PROPS = Object.keys(
omit(NumberFormat.propTypes, [
'displayType',
'defaultValue',
'customInput',
'getInputRef',
'onValueChange',
...INPUT_PROPS,
])
);
const NumberInput = (props) => {
const { id, type, markers, readOnly, forwardedRef } = props;
const forwardedProps = pick(props, INPUT_PROPS);
const errors = useMemo(() => {
return markers.filter(
(marker) => marker.type === 'validation' && marker.level === 'error'
);
}, [markers]);
return (
<TextInput
id={id}
ref={forwardedRef}
customValidity={errors && errors.length > 0 ? errors[0].item.message : ''}
readOnly={Boolean(readOnly)}
placeholder={type.placeholder}
inputMode={props.isDecimal ? 'decimal' : 'numeric'}
{...forwardedProps}
/>
);
};
const NumericInput = React.forwardRef((props, forwardedRef) => {
const { type, markers, level, presence, onChange } = props;
const numberFormatProps = {
allowEmptyFormatting: true,
allowedDecimalSeparators: ['.', ','],
...pick(type.options, NUMBER_FORMAT_PROPS),
};
const forwardedProps = omit(props, ['value', 'onChange']);
// Use local state and effects to prevent issue with
// NumberFormat triggering additional onChangeValue
// calls, causing unwanted patches (reverting values).
const [value, setValue] = useState(props.value ?? 0);
const onValueChange = useCallback(
({ floatValue }) => {
setValue(floatValue);
if (props.value !== floatValue) {
onChange(PatchEvent.from(set(Number(floatValue))));
}
},
[onChange, props.value]
);
// Required for external changes, like reverting a document.
useEffect(() => {
if (props.value !== value) {
setValue(typeof props.value === 'undefined' ? 0 : props.value);
}
}, [props.value, value]);
const id = useId();
return (
<FormField
inputId={id}
level={level}
title={type.title}
description={type.description}
__unstable_markers={markers}
__unstable_presence={presence}
>
<NumberFormat
id={id}
forwardedRef={forwardedRef}
customInput={NumberInput}
value={value}
onValueChange={onValueChange}
isDecimal={numberFormatProps.decimalScale !== 0}
{...forwardedProps}
{...numberFormatProps}
/>
</FormField>
);
});
export default NumericInput;
// Helpers
function pick(obj, keys) {
return Object.fromEntries(
Object.entries(obj ?? {}).filter(([key]) => keys.includes(key))
);
}
function omit(obj, keys) {
return Object.fromEntries(
Object.entries(obj ?? {}).filter(([key]) => !keys.includes(key))
);
}
import NumericInput from '../components/NumericInput';
export default {
title: 'Numeric',
name: 'numeric',
type: 'number',
inputComponent: NumericInput,
options: {
decimalScale: 2,
fixedDecimalScale: false,
},
};
import NumericInput from '../components/NumericInput';
export default {
title: 'Float',
name: 'float',
type: 'number',
inputComponent: NumericInput,
options: {
decimalScale: 2,
fixedDecimalScale: true,
},
};
import NumericInput from '../components/NumericInput';
export default {
title: 'Integer',
name: 'integer',
type: 'number',
inputComponent: NumericInput,
validation: (Rule) => Rule.precision(0).positive(),
options: {
decimalScale: 0,
fixedDecimalScale: true,
allowNegative: false,
},
};
import NumericInput from '../components/NumericInput';
export default {
title: 'Price',
name: 'price',
type: 'number',
inputComponent: NumericInput,
validation: (Rule) => Rule.positive(),
options: {
decimalScale: 2,
fixedDecimalScale: true,
allowNegative: false,
decimalSeparator: ',',
thousandSeparator: '.',
prefix: '€ ',
},
};
import NumericInput from '../components/NumericInput';
export default {
title: 'Percentage',
name: 'percentage',
type: 'number',
inputComponent: NumericInput,
validation: (Rule) => Rule.positive(),
options: {
decimalScale: 0,
fixedDecimalScale: true,
allowNegative: false,
suffix: ' %',
},
};
This custom input leverages react-number-format to parse, format and mask numeric values. The value is stored as a float.
Most options of react-number-format can be used, and a few common numeric type preset schemas are provided as well.
Designer & Developer