feat: implement custom NumberInput component

master
Katja Lutz 2 years ago
parent a935e6fe56
commit cf2a1c4d90

@ -1,7 +1,6 @@
import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns"; import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns";
import { import {
Component, Component,
JSX,
Show, Show,
mergeProps, mergeProps,
splitProps, splitProps,
@ -14,6 +13,11 @@ import AsteriskIcon from "~icons/ph/asterisk-bold";
import MaximizeIcon from "~icons/carbon/maximize"; import MaximizeIcon from "~icons/carbon/maximize";
import MinimizeIcon from "~icons/carbon/minimize"; import MinimizeIcon from "~icons/carbon/minimize";
import autosize from "autosize"; import autosize from "autosize";
import type { JSX } from "solid-js";
import {
createNativeInputValue,
createOptionalNumberInputHandler,
} from "~/util";
export const TextInput: Component< export const TextInput: Component<
{ {
@ -90,7 +94,6 @@ export const TextInput: Component<
[props.class || ""]: true, [props.class || ""]: true,
}} }}
type="text" type="text"
lang={rest.type === "number" ? "en" : undefined}
placeholder={props.placeholder} placeholder={props.placeholder}
{...rest} {...rest}
/> />
@ -218,3 +221,27 @@ export const UnixDateInput: Component<
/> />
); );
}; };
export const NumberInput: Component<
{ value?: number; onInput: (v: number | undefined) => void } & Omit<
Parameters<typeof TextInput>[0],
"onInput"
>
> = (p) => {
let el: HTMLInputElement = undefined!;
const [props, rest] = splitProps(p, ["value", "onInput"]);
const value = createNativeInputValue(
() => el,
() => props.value
);
return (
<TextInput
ref={el}
value={value()}
maxLength={9}
onInput={createOptionalNumberInputHandler(props.onInput)}
{...rest}
/>
);
};

@ -19,7 +19,13 @@ import { generate } from "node-iso11649";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import createAccordion from "../Accordion"; import createAccordion from "../Accordion";
import { Checkbox, TextArea, TextInput, UnixDateInput } from "../Form"; import {
Checkbox,
NumberInput,
TextArea,
TextInput,
UnixDateInput,
} from "../Form";
import { autoAnimate } from "~/directives/autoAnimate"; import { autoAnimate } from "~/directives/autoAnimate";
import { import {
LocalStoreContext, LocalStoreContext,
@ -322,21 +328,18 @@ const SettingsOverlay: Component = () => {
</div> </div>
</div> </div>
<FullWidthAccordionInput <div class="col-span-2">
required <NumberInput
type="number" required
label="Standard Einzelpreis" label="Standard Einzelpreis"
min="0" labelMinWidth={fullWidthLabelWidth}
step="0.01" value={state.defaultItemPrice}
value={state.defaultItemPrice} onInput={(v) =>
onInput={(evt) => v != null && setState("defaultItemPrice", v)
setState( }
"defaultItemPrice", onBlur={resetInput(0)}
parseFloat(evt.currentTarget.value) || 0 />
) </div>
}
onBlur={resetInput(0)}
/>
<div class="col-span-2"> <div class="col-span-2">
<TextArea <TextArea
@ -587,19 +590,16 @@ const SettingsOverlay: Component = () => {
/> />
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<TextInput <NumberInput
required required
label="MwST-Satz" label="MwST-Satz"
type="number"
step="0.1"
max="100"
suffix="%" suffix="%"
value={new Big(localState.vatRate).mul(100).toNumber()} value={new Big(localState.vatRate).mul(100).toNumber()}
onInput={(evt) => { onInput={(v) => {
evt.currentTarget.value !== "" && v != null &&
setLocalState( setLocalState(
"vatRate", "vatRate",
new Big(evt.currentTarget.value).div(100).toNumber() new Big(v).div(100).toNumber()
); );
}} }}
onBlur={resetInput(0)} onBlur={resetInput(0)}

@ -21,9 +21,13 @@ import AddIcon from "~icons/carbon/add-filled";
import DeleteIcon from "~icons/carbon/trash-can"; import DeleteIcon from "~icons/carbon/trash-can";
import DragVerticalIcon from "~icons/carbon/drag-vertical"; import DragVerticalIcon from "~icons/carbon/drag-vertical";
import PositionSettingsIcon from "~icons/carbon/settings-adjust"; import PositionSettingsIcon from "~icons/carbon/settings-adjust";
import { Checkbox, TextArea, TextInput } from "../Form"; import { Checkbox, NumberInput, TextArea, TextInput } from "../Form";
import { MarkdownHelpLabel } from "../Markdown"; import { MarkdownHelpLabel } from "../Markdown";
import { createOptionalNumberInputHandler } from "~/util"; import {
createOptionalNumberInputHandler,
createNativeInputValue,
resetInput,
} from "~/util";
export const PositionsSettings: Component = () => { export const PositionsSettings: Component = () => {
const [state, setState] = useContext(StoreContext)!; const [state, setState] = useContext(StoreContext)!;
@ -125,6 +129,12 @@ export const PositionsSettings: Component = () => {
); );
}; };
let quantityInputEl: HTMLInputElement = undefined!;
const quantityValue = createNativeInputValue(
() => quantityInputEl,
() => position.quantity
);
return ( return (
<div class="indicator w-full"> <div class="indicator w-full">
<div class="indicator-item indicator-middle indicator-end flex items-center"> <div class="indicator-item indicator-middle indicator-end flex items-center">
@ -243,24 +253,24 @@ export const PositionsSettings: Component = () => {
<Show when={position.type === POSITION_TYPE_QUANTITY}> <Show when={position.type === POSITION_TYPE_QUANTITY}>
<div class="flex-1"> <div class="flex-1">
<input <input
ref={quantityInputEl}
class="w-full input input-bordered input-xs" class="w-full input input-bordered input-xs"
value={position.quantity} value={quantityValue()}
placeholder="Menge" placeholder="Menge"
min="0"
step="0.01"
required required
type="number" name="Menge"
lang="en"
onInput={createOptionalNumberInputHandler( onInput={createOptionalNumberInputHandler(
(v) => { (v) => {
setState( v != null &&
"positions", setState(
idx(), "positions",
"quantity", idx(),
v || 0 "quantity",
); v
);
} }
)} )}
onBlur={resetInput(0)}
/> />
</div> </div>
</Show> </Show>
@ -293,7 +303,7 @@ export const PositionsSettings: Component = () => {
</Show> </Show>
</div> </div>
</div> </div>
<TextInput <NumberInput
size="xs" size="xs"
value={position.itemPrice} value={position.itemPrice}
label="Einzelpreis" label="Einzelpreis"
@ -302,12 +312,9 @@ export const PositionsSettings: Component = () => {
? state.defaultItemPrice + "" ? state.defaultItemPrice + ""
: undefined : undefined
} }
step="0.01" onInput={(v) =>
min="0"
type="number"
onInput={createOptionalNumberInputHandler((v) =>
setState("positions", idx(), "itemPrice", v) setState("positions", idx(), "itemPrice", v)
)} }
/> />
<div <div
use:autoAnimate use:autoAnimate
@ -348,21 +355,18 @@ export const PositionsSettings: Component = () => {
/> />
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<TextInput <NumberInput
label="Aktionspreis" label="Aktionspreis"
suffix="CHF" suffix="CHF"
type="number"
step="0.01"
min="0"
value={position.fixedDiscountPrice} value={position.fixedDiscountPrice}
onInput={createOptionalNumberInputHandler((v) => onInput={(v) =>
setState( setState(
"positions", "positions",
idx(), idx(),
"fixedDiscountPrice", "fixedDiscountPrice",
v v
) )
)} }
/> />
</div> </div>
<div class="col-span-2"> <div class="col-span-2">

@ -1,6 +1,6 @@
import Big from "big.js"; import Big from "big.js";
import { fromUnixTime, intlFormat } from "date-fns"; import { fromUnixTime, intlFormat } from "date-fns";
import { JSX } from "solid-js"; import { createMemo, JSX } from "solid-js";
export const sleep = (timeout: number) => export const sleep = (timeout: number) =>
new Promise((res) => setTimeout(res, timeout)); new Promise((res) => setTimeout(res, timeout));
@ -90,9 +90,51 @@ export const createOptionalNumberInputHandler = (
return; return;
} }
const value = let value =
e.currentTarget.value == "" ? undefined : e.currentTarget.valueAsNumber; e.currentTarget.value == ""
? undefined
: parseNumberInput(e.currentTarget.value);
if (Number.isNaN(value)) {
return;
}
onInput(value); onInput(value);
}; };
}; };
const parseNumberInput = (v: string): number => parseFloat(v.replace(",", "."));
export const createNativeInputValue = (
getEl: () => HTMLInputElement,
signal: () => any
) =>
createMemo(function (prev) {
const value = signal();
const el = getEl();
if (!el) {
return value != null ? value : "";
}
const elValue = parseNumberInput(el.value);
// If the element value and signal value are equal, we can skip triggering the memo change by reusing the prev value
let result = elValue == value ? prev : value;
// NaN is always != NaN in js, we have to replace it with a value which has a proper identity
if (Number.isNaN(result)) {
result = undefined;
}
if (result == null) {
result = "";
}
// If both the value and prev value are the same, but the element value is different, we have to update it manually
if (value === prev && elValue != value) {
el.value = result;
}
return result;
});

Loading…
Cancel
Save