feat: implement form components
parent
50b736e4a7
commit
f1e73d40c7
@ -0,0 +1,217 @@
|
||||
import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns";
|
||||
import {
|
||||
Component,
|
||||
JSX,
|
||||
Show,
|
||||
mergeProps,
|
||||
splitProps,
|
||||
FlowComponent,
|
||||
createSignal,
|
||||
createEffect,
|
||||
} from "solid-js";
|
||||
import { validateInput } from "~/hooks/validation";
|
||||
import AsteriskIcon from "~icons/ph/asterisk-bold";
|
||||
import MaximizeIcon from "~icons/carbon/maximize";
|
||||
import MinimizeIcon from "~icons/carbon/minimize";
|
||||
import autosize from "autosize";
|
||||
|
||||
export const TextInput: Component<
|
||||
{
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
labelMinWidth?: string;
|
||||
suffix?: string;
|
||||
size?: string;
|
||||
vertical?: boolean;
|
||||
} & JSX.InputHTMLAttributes<HTMLInputElement>
|
||||
> = (p) => {
|
||||
p = mergeProps(
|
||||
{
|
||||
placeholder: p.label,
|
||||
labelMinWidth: "95px",
|
||||
size: "sm",
|
||||
vertical: false,
|
||||
},
|
||||
p
|
||||
);
|
||||
const [props, rest] = splitProps(p, [
|
||||
"name",
|
||||
"size",
|
||||
"class",
|
||||
"label",
|
||||
"placeholder",
|
||||
"suffix",
|
||||
"vertical",
|
||||
"labelMinWidth",
|
||||
]);
|
||||
const sizes: Record<string, string> = {
|
||||
xs: "input-xs",
|
||||
sm: "input-sm",
|
||||
lg: "input-lg",
|
||||
};
|
||||
|
||||
const [validate, vState] = validateInput({ value: () => rest.value });
|
||||
|
||||
return (
|
||||
<div class="shrink form-control">
|
||||
<label
|
||||
classList={{
|
||||
"input-group": true,
|
||||
"input-group-vertical": props.vertical,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
"min-width":
|
||||
!props.vertical && props.labelMinWidth
|
||||
? props.labelMinWidth
|
||||
: undefined,
|
||||
}}
|
||||
classList={{
|
||||
"gap-1": true,
|
||||
"h-8": props.vertical,
|
||||
"font-bold": rest.required,
|
||||
"bg-red-100": !vState().valid,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
<Show when={rest.required}>
|
||||
<AsteriskIcon class="text-primary text-[0.5rem]" />
|
||||
</Show>
|
||||
</span>
|
||||
<input
|
||||
name={props.name || props.label}
|
||||
use:validate
|
||||
classList={{
|
||||
"peer flex-1 input invalid:input-error input-bordered": true,
|
||||
"w-0": !props.vertical,
|
||||
[(props.size && sizes[props.size]) || ""]: true,
|
||||
[props.class || ""]: true,
|
||||
}}
|
||||
type="text"
|
||||
placeholder={props.placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
<Show when={props.suffix}>
|
||||
<span
|
||||
classList={{
|
||||
"bg-red-100": !vState().valid,
|
||||
}}
|
||||
>
|
||||
{props.suffix}
|
||||
</span>
|
||||
</Show>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Checkbox: FlowComponent<
|
||||
JSX.InputHTMLAttributes<HTMLInputElement>
|
||||
> = (p) => {
|
||||
const [props, rest] = splitProps(p, ["children"]);
|
||||
|
||||
return (
|
||||
<div class="form-control">
|
||||
<label class="label py-0 cursor-pointer">
|
||||
<span class="label-text">{props.children}</span>
|
||||
<input type="checkbox" class="checkbox" {...rest} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TextArea: Component<
|
||||
{
|
||||
label: string;
|
||||
labelSuffixJsx?: JSX.Element;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
} & JSX.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
> = (p) => {
|
||||
p = mergeProps({ rows: 3, placeholder: p.label }, p);
|
||||
const [props, rest] = splitProps(p, ["label", "labelSuffixJsx", "value"]);
|
||||
const [autosizeEnabled, setAutosize] = createSignal(false);
|
||||
let textareaEl: HTMLTextAreaElement = undefined!;
|
||||
createEffect(function () {
|
||||
if (autosizeEnabled()) {
|
||||
autosize(textareaEl);
|
||||
} else {
|
||||
autosize.destroy(textareaEl);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="form-control relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAutosize(!autosizeEnabled())}
|
||||
class="absolute right-3 top-2 hover:text-accent transition-all hover:scale-110"
|
||||
>
|
||||
<Show when={!autosizeEnabled()} fallback={<MinimizeIcon />}>
|
||||
<MaximizeIcon />
|
||||
</Show>
|
||||
</button>
|
||||
<label class="input-group input-group-vertical">
|
||||
<span class="h-8 flex gap-2 justify-between pr-14">
|
||||
{props.label}
|
||||
{props.labelSuffixJsx}
|
||||
</span>
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
ref={textareaEl}
|
||||
classList={{
|
||||
"textarea h-auto py-2 textarea-bordered leading-normal": true,
|
||||
"min-h-[150px]": autosizeEnabled(),
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{props.value || ""}
|
||||
</textarea>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const UnixDateInput: Component<
|
||||
{
|
||||
required?: boolean;
|
||||
label: string;
|
||||
value?: number;
|
||||
onInput: (v: number) => void;
|
||||
} & Parameters<typeof TextInput>[0]
|
||||
> = (p) => {
|
||||
const [props, rest] = splitProps(p, [
|
||||
"required",
|
||||
"label",
|
||||
"value",
|
||||
"onInput",
|
||||
]);
|
||||
return (
|
||||
<TextInput
|
||||
required={props.required}
|
||||
type="date"
|
||||
label={props.label}
|
||||
max="9999-12-31"
|
||||
value={
|
||||
props.value != null
|
||||
? formatISO9075(fromUnixTime(props.value), {
|
||||
representation: "date",
|
||||
})
|
||||
: (null as any)
|
||||
}
|
||||
onInput={(evt) => {
|
||||
if (!evt.currentTarget.valueAsDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.currentTarget.valueAsDate.getFullYear() > 9999) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onInput(getUnixTime(evt.currentTarget.valueAsDate));
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue