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 {
Component,
JSX,
Show,
mergeProps,
splitProps,
@ -14,6 +13,11 @@ import AsteriskIcon from "~icons/ph/asterisk-bold";
import MaximizeIcon from "~icons/carbon/maximize";
import MinimizeIcon from "~icons/carbon/minimize";
import autosize from "autosize";
import type { JSX } from "solid-js";
import {
createNativeInputValue,
createOptionalNumberInputHandler,
} from "~/util";
export const TextInput: Component<
{
@ -90,7 +94,6 @@ export const TextInput: Component<
[props.class || ""]: true,
}}
type="text"
lang={rest.type === "number" ? "en" : undefined}
placeholder={props.placeholder}
{...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 createAccordion from "../Accordion";
import { Checkbox, TextArea, TextInput, UnixDateInput } from "../Form";
import {
Checkbox,
NumberInput,
TextArea,
TextInput,
UnixDateInput,
} from "../Form";
import { autoAnimate } from "~/directives/autoAnimate";
import {
LocalStoreContext,
@ -322,21 +328,18 @@ const SettingsOverlay: Component = () => {
</div>
</div>
<FullWidthAccordionInput
required
type="number"
label="Standard Einzelpreis"
min="0"
step="0.01"
value={state.defaultItemPrice}
onInput={(evt) =>
setState(
"defaultItemPrice",
parseFloat(evt.currentTarget.value) || 0
)
}
onBlur={resetInput(0)}
/>
<div class="col-span-2">
<NumberInput
required
label="Standard Einzelpreis"
labelMinWidth={fullWidthLabelWidth}
value={state.defaultItemPrice}
onInput={(v) =>
v != null && setState("defaultItemPrice", v)
}
onBlur={resetInput(0)}
/>
</div>
<div class="col-span-2">
<TextArea
@ -587,19 +590,16 @@ const SettingsOverlay: Component = () => {
/>
</div>
<div class="col-span-2">
<TextInput
<NumberInput
required
label="MwST-Satz"
type="number"
step="0.1"
max="100"
suffix="%"
value={new Big(localState.vatRate).mul(100).toNumber()}
onInput={(evt) => {
evt.currentTarget.value !== "" &&
onInput={(v) => {
v != null &&
setLocalState(
"vatRate",
new Big(evt.currentTarget.value).div(100).toNumber()
new Big(v).div(100).toNumber()
);
}}
onBlur={resetInput(0)}

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

@ -1,6 +1,6 @@
import Big from "big.js";
import { fromUnixTime, intlFormat } from "date-fns";
import { JSX } from "solid-js";
import { createMemo, JSX } from "solid-js";
export const sleep = (timeout: number) =>
new Promise((res) => setTimeout(res, timeout));
@ -90,9 +90,51 @@ export const createOptionalNumberInputHandler = (
return;
}
const value =
e.currentTarget.value == "" ? undefined : e.currentTarget.valueAsNumber;
let value =
e.currentTarget.value == ""
? undefined
: parseNumberInput(e.currentTarget.value);
if (Number.isNaN(value)) {
return;
}
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