Skip to content
Snippets Groups Projects
Commit 83f80b4e authored by Loïc Correnson's avatar Loïc Correnson
Browse files

[dome] number field

parent 1f785b8d
No related branches found
No related tags found
No related merge requests found
...@@ -102,6 +102,23 @@ export function useDefault<A>( ...@@ -102,6 +102,23 @@ export function useDefault<A>(
return [value ?? defaultValue, error, setState]; return [value ?? defaultValue, error, setState];
} }
export function useRequired<A>(
state: FieldState<A>,
): FieldState<A | undefined> {
const [value, error, setState] = state;
const cache = React.useRef(value);
const update = React.useCallback(
(newValue: A | undefined, newError: Error) => {
if (newValue === undefined) {
setState(cache.current, newError || 'Required field');
} else {
setState(newValue, newError);
}
}, [cache, setState],
);
return [value, error, update];
}
export function useChecker<A>( export function useChecker<A>(
state: FieldState<A>, state: FieldState<A>,
checker?: Checker<A>, checker?: Checker<A>,
...@@ -114,6 +131,37 @@ export function useChecker<A>( ...@@ -114,6 +131,37 @@ export function useChecker<A>(
return [value, error, update]; return [value, error, update];
} }
export function useFilter<A, B>(
state: FieldState<A>,
input: (value: A) => B,
output: (value: B) => A,
defaultValue: B,
): FieldState<B> {
const [value, error, setState] = state;
const cacheA = React.useRef<A>(value);
const cacheB = React.useRef<B>(defaultValue);
const update = React.useCallback(
(newValue: B, newError: Error) => {
try {
const outValue = output(newValue);
setState(outValue, newError);
} catch (outErr) {
const outError = newError || outErr.toString() || 'Invalid value';
setState(cacheA.current, outError);
}
}, [cacheA, output, setState],
);
cacheA.current = value;
try {
const inValue = input(value);
cacheB.current = inValue;
return [inValue, error, update];
} catch (inErr) {
const inError = error || inErr.toString() || 'Invalid value';
return [cacheB.current, inError, update];
}
}
export function useProperty<A, K extends keyof A>( export function useProperty<A, K extends keyof A>(
state: FieldState<A>, state: FieldState<A>,
property: K, property: K,
...@@ -448,19 +496,24 @@ export interface FieldProps<A> extends FilterProps { ...@@ -448,19 +496,24 @@ export interface FieldProps<A> extends FilterProps {
onError?: string; onError?: string;
} }
type InputEvent = { target: { value: string } }; type InputEvent<A> = { target: { value: A } };
type InputState = [string, Error, (evt: InputEvent) => void]; type InputState<A> = [string, Error, (evt: InputEvent<A>) => void];
function useChangeEvent<A>(setState: Callback<A>) {
return React.useCallback(
(evt: InputEvent<A>) => { setState(evt.target.value, undefined); },
[setState],
);
}
function useTextInputField( function useTextInputField(
props: FieldTextProps, props: FieldTextProps,
defaultLatency: number, defaultLatency: number,
): InputState { ): InputState<string> {
const checked = useChecker(props.state, props.checker); const checked = useChecker(props.state, props.checker);
const period = props.latency ?? defaultLatency; const period = props.latency ?? defaultLatency;
const [value, error, setState] = useLatency(checked, period); const [value, error, setState] = useLatency(checked, period);
const onChange = (evt: InputEvent) => { const onChange = useChangeEvent(setState);
setState(evt.target.value, undefined);
};
return [value || '', error, onChange]; return [value || '', error, onChange];
} }
...@@ -629,4 +682,67 @@ export const FieldCodeArea = (props: FieldTextAreaProps) => { ...@@ -629,4 +682,67 @@ export const FieldCodeArea = (props: FieldTextAreaProps) => {
); );
}; };
/* --------------------------------------------------------------------------*/
/* --- Number Field ---*/
/* --------------------------------------------------------------------------*/
export interface FieldNumberProps extends FieldProps<number | undefined> {
units?: string;
placeholder?: string;
className?: string;
style?: React.CSSProperties;
latency?: number;
}
function TEXT_OF_NUMBER(n: number | undefined): string {
if (n === undefined) return '';
if (Number.isNaN(n)) throw new Error('Invalid number');
return Number(n).toLocaleString('en');
}
function NUMBER_OF_TEXT(s: string): number | undefined {
if (s === '') return undefined;
const n = Number.parseFloat(s.replace(/[ ,]/g, ''));
if (Number.isNaN(n)) throw new Error('Invalid number');
return n;
}
/**
Text Field.
@category Form Fields
*/
export const FieldNumber = (props: FieldNumberProps) => {
const { units, latency = 600 } = props;
const { disabled } = useContext(props);
const id = useHtmlFor();
const css = Utils.classes('dome-xForm-number-field', props.className);
const checked = useChecker(props.state, props.checker);
const filtered = useFilter(checked, TEXT_OF_NUMBER, NUMBER_OF_TEXT, '');
const [value, error, setState] = useLatency(filtered, latency);
const onChange = useChangeEvent(setState);
const UNITS = units && (
<label className="dome-text-label dome-xForm-units">{units}</label>
);
return (
<Field
{...props}
offset={4}
htmlFor={id}
error={error}
>
<input
id={id}
type="text"
value={value}
className={css}
style={props.style}
disabled={disabled}
placeholder={props.placeholder}
onChange={onChange}
/>
{UNITS}
</Field>
);
};
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment