163 lines
4.8 KiB
TypeScript
163 lines
4.8 KiB
TypeScript
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { FormControl, FormDescription, FormItem, InputProps } from "@/ui";
|
||
|
|
|
||
|
|
import { Quantity, QuantityObject } from "@shared/contexts";
|
||
|
|
import { createElement, forwardRef, useState } from "react";
|
||
|
|
import {
|
||
|
|
Controller,
|
||
|
|
FieldPath,
|
||
|
|
FieldValues,
|
||
|
|
UseControllerProps,
|
||
|
|
useFormContext,
|
||
|
|
} from "react-hook-form";
|
||
|
|
import { FormErrorMessage } from "./FormErrorMessage";
|
||
|
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||
|
|
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
||
|
|
|
||
|
|
export type FormQuantityFieldProps<
|
||
|
|
TFieldValues extends FieldValues = FieldValues,
|
||
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||
|
|
> = {
|
||
|
|
button?: (props?: React.PropsWithChildren) => React.ReactNode;
|
||
|
|
defaultValue?: any;
|
||
|
|
} & InputProps &
|
||
|
|
FormInputProps &
|
||
|
|
Partial<FormLabelProps> &
|
||
|
|
FormInputWithIconProps &
|
||
|
|
UseControllerProps<TFieldValues, TName>;
|
||
|
|
|
||
|
|
export const FormQuantityField = forwardRef<
|
||
|
|
HTMLDivElement,
|
||
|
|
React.HTMLAttributes<HTMLDivElement> & FormQuantityFieldProps
|
||
|
|
>((props, ref) => {
|
||
|
|
const {
|
||
|
|
name,
|
||
|
|
label,
|
||
|
|
hint,
|
||
|
|
placeholder,
|
||
|
|
description,
|
||
|
|
|
||
|
|
required,
|
||
|
|
className,
|
||
|
|
leadIcon,
|
||
|
|
trailIcon,
|
||
|
|
button,
|
||
|
|
defaultValue,
|
||
|
|
} = props;
|
||
|
|
|
||
|
|
const { control } = useFormContext();
|
||
|
|
|
||
|
|
const [precision, setPrecision] = useState<number>(Quantity.DEFAULT_PRECISION);
|
||
|
|
|
||
|
|
const transform = {
|
||
|
|
input: (value: QuantityObject) => {
|
||
|
|
const quantityOrError = Quantity.create(value);
|
||
|
|
|
||
|
|
if (quantityOrError.isFailure) {
|
||
|
|
throw quantityOrError.error;
|
||
|
|
}
|
||
|
|
|
||
|
|
const quantityValue = quantityOrError.object;
|
||
|
|
setPrecision(quantityValue.getPrecision());
|
||
|
|
return quantityValue.toString();
|
||
|
|
},
|
||
|
|
output: (event: React.ChangeEvent<HTMLInputElement>): QuantityObject => {
|
||
|
|
const value = parseFloat(event.target.value);
|
||
|
|
const output = !isNaN(value) ? value : 0;
|
||
|
|
|
||
|
|
const quantityOrError = Quantity.create({
|
||
|
|
amount: output * Math.pow(10, precision),
|
||
|
|
precision,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (quantityOrError.isFailure) {
|
||
|
|
throw quantityOrError.error;
|
||
|
|
}
|
||
|
|
|
||
|
|
return quantityOrError.object.toObject();
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Controller
|
||
|
|
defaultValue={defaultValue}
|
||
|
|
control={control}
|
||
|
|
name={name}
|
||
|
|
rules={{
|
||
|
|
required,
|
||
|
|
}}
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
|
render={({ field, fieldState, formState }) => {
|
||
|
|
return (
|
||
|
|
<input
|
||
|
|
type='number'
|
||
|
|
{...field}
|
||
|
|
className='text-right'
|
||
|
|
placeholder={placeholder}
|
||
|
|
onChange={(e) => field.onChange(transform.output(e))}
|
||
|
|
value={transform.input(field.value)}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
||
|
|
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||
|
|
<div className={cn(button ? "flex" : null)}>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{leadIcon && (
|
||
|
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
||
|
|
{createElement(
|
||
|
|
leadIcon,
|
||
|
|
{
|
||
|
|
className: "h-5 w-5 text-muted-foreground",
|
||
|
|
"aria-hidden": true,
|
||
|
|
},
|
||
|
|
null
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<FormControl
|
||
|
|
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type='number'
|
||
|
|
placeholder={placeholder}
|
||
|
|
className={cn(
|
||
|
|
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
|
||
|
|
)}
|
||
|
|
{...field}
|
||
|
|
onInput={(e) => field.onChange(transform.output(e))}
|
||
|
|
value={transform.input(field.value)}
|
||
|
|
/>
|
||
|
|
</FormControl>
|
||
|
|
|
||
|
|
{trailIcon && (
|
||
|
|
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
|
||
|
|
{createElement(
|
||
|
|
trailIcon,
|
||
|
|
{
|
||
|
|
className: "h-5 w-5 text-muted-foreground",
|
||
|
|
"aria-hidden": true,
|
||
|
|
},
|
||
|
|
null
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{button && <>{createElement(button)}</>}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{description && <FormDescription>{description}</FormDescription>}
|
||
|
|
<FormErrorMessage />
|
||
|
|
</FormItem>
|
||
|
|
);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
});
|