Tiny form library

This commit is contained in:
Jan Zípek 2022-08-29 09:48:30 +02:00
parent 8c7e278042
commit 13afe2f8da
Signed by: kamen
GPG Key ID: A17882625B33AC31
6 changed files with 125 additions and 117 deletions

View File

@ -1,6 +1,6 @@
:root {
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--border-radius: 0.25rem;
--border-radius: 0.15rem;
--main-bg-color: #eee;
--main-fg-color: #000;
--button-bg-color: #3988ff;

View File

@ -24,13 +24,9 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
const [data, setData] = useState(() => value.data)
const {
value: formState,
handleChange,
handleSubmit,
} = useForm({
const { register, watch, handleSubmit } = useForm({
defaultValue: () => ({
title: value.title,
title: value.title ?? '',
type: value.data?.type ?? '',
}),
onSubmit: async (v) => {
@ -44,6 +40,8 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
},
})
const type = watch('type')
const deleteConfirm = useConfirmModal({
content: 'Are you sure you want to delete the box?',
onConfirm: () => {
@ -58,26 +56,17 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
<>
<div className="input">
<label>Title</label>
<input
name="title"
value={formState.title}
onChange={handleChange}
/>
<input {...register('title')} />
</div>
<div className="input">
<label>Type</label>
<select
name="type"
value={formState.type}
onChange={handleChange}
required
>
<select {...register('type')} required>
<option value="graph">Graph</option>
<option value="dial">Dial</option>
</select>
</div>
{formState.type === 'graph' && (
{type === 'graph' && (
<GraphSettings
value={data as DashboardGraphData}
onChange={setData}
@ -85,7 +74,7 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
/>
)}
{formState.type === 'dial' && (
{type === 'dial' && (
<DialSettings
value={data as DashboardDialData}
onChange={setData}

View File

@ -2,7 +2,6 @@ import { SensorInfo } from '@/api/sensors'
import { DashboardDialData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit'
import { useEffect } from 'preact/hooks'
type Props = {
sensors: SensorInfo[]
@ -11,26 +10,18 @@ type Props = {
}
export const DialSettings = ({ sensors, value, onChange }: Props) => {
const { value: formState, handleChange } = useForm({
const { register } = useForm({
defaultValue: () => ({
...(value && omit(value, ['type'])),
}),
onChange: (v) => onChange({ ...v, type: 'dial' }),
})
useEffect(() => {
onChange({ ...formState, type: 'dial' })
}, [formState])
return (
<>
<div className="input">
<label>Sensor</label>
<select
name="sensor"
value={formState.sensor || ''}
onChange={handleChange}
required
>
<select required {...register('sensor')}>
{sensors.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
@ -40,26 +31,15 @@ export const DialSettings = ({ sensors, value, onChange }: Props) => {
</div>
<div className="input">
<label>Unit</label>
<input name="unit" value={formState.unit} onChange={handleChange} />
<input {...register('unit')} />
</div>
<div className="input">
<label>Decimals</label>
<input
type="number"
name="decimals"
value={formState.decimals}
onChange={handleChange}
/>
<input type="number" {...register('decimals')} />
</div>
<div className="input">
<label>Multiplier</label>
<input
type="number"
name="multiplier"
value={formState.multiplier}
onChange={handleChange}
step="any"
/>
<input type="number" step="any" {...register('multiplier')} />
</div>
</>
)

View File

@ -2,7 +2,6 @@ import { SensorInfo } from '@/api/sensors'
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit'
import { useEffect } from 'preact/hooks'
type Props = {
sensors: SensorInfo[]
@ -11,26 +10,20 @@ type Props = {
}
export const GraphSettings = ({ value, onChange, sensors }: Props) => {
const { value: formState, handleChange } = useForm({
const { register, watch } = useForm({
defaultValue: () => ({
...(value && omit(value, ['type'])),
}),
onChange: (v) => onChange({ ...v, type: 'graph' }),
})
useEffect(() => {
onChange({ ...formState, type: 'graph' })
}, [formState])
const colorMode = watch('colorMode')
return (
<>
<div className="input">
<label>Sensor</label>
<select
name="sensor"
value={formState.sensor || ''}
onChange={handleChange}
required
>
<select {...register('sensor')} required>
{sensors.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
@ -41,11 +34,7 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
<div className="input">
<label>Graph Type</label>
<select
name="graphType"
value={formState.graphType || 'line'}
onChange={handleChange}
>
<select {...register('graphType')}>
<option value="line">Line</option>
<option value="points">Points</option>
<option value="lineAndPoints">Line + Points</option>
@ -55,11 +44,7 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
<div className="input">
<label>Fill area</label>
<select
name="fill"
value={formState.fill || ''}
onChange={handleChange}
>
<select {...register('fill')}>
<option value="">None</option>
<option value="tozeroy">To zero</option>
<option value="tonexty">To next value</option>
@ -69,49 +54,28 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
<div className="input">
<label>Unit</label>
<input name="unit" value={formState.unit} onChange={handleChange} />
<input {...register('unit')} />
</div>
<div className="input">
<label>Min value</label>
<input
type="number"
step="any"
name="min"
value={formState.min}
onChange={handleChange}
/>
<input type="number" step="any" {...register('min')} />
</div>
<div className="input">
<label>Max value</label>
<input
type="number"
step="any"
name="max"
value={formState.max}
onChange={handleChange}
/>
<input type="number" step="any" {...register('max')} />
</div>
<div className="input">
<label>Color mode</label>
<select
name="colorMode"
value={formState.colorMode ?? ''}
onChange={handleChange}
>
<select {...register('colorMode')}>
<option value="">None</option>
<option value="static">Static</option>
</select>
</div>
{formState.colorMode === 'static' && (
{colorMode === 'static' && (
<div className="input">
<label>Max value</label>
<input
type="color"
name="staticColor"
value={formState.staticColor}
onChange={handleChange}
/>
<input type="color" {...register('staticColor')} />
</div>
)}
</>

View File

@ -14,7 +14,7 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
const createMutation = useMutation(createSensor)
const updateMutation = useMutation(updateSensor)
const { value, handleSubmit, handleChange } = useForm({
const { handleSubmit, register } = useForm({
defaultValue: () => ({
name: sensor?.name ?? '',
}),
@ -47,12 +47,10 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
<label>Name</label>
<input
type="text"
name="name"
minLength={1}
required
onChange={handleChange}
autoFocus
value={value.name}
{...register('name')}
/>
</div>

View File

@ -1,44 +1,121 @@
import { useCallback, useRef, useState } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
type Props<TValue> = {
type BaseValue = Record<string, string>
type Props<TValue extends BaseValue> = {
onSubmit?: (value: TValue) => void
onChange?: (value: TValue) => void
defaultValue: TValue | (() => TValue)
}
export const useForm = <TValue = unknown>({
type InternalState<TValue extends BaseValue> = {
onSubmit?: (value: TValue) => void
onChange?: (value: TValue) => void
subscribers: ((value: TValue) => void)[]
value: TValue
}
export const useForm = <TValue extends BaseValue = BaseValue>({
defaultValue,
onSubmit,
onChange,
}: Props<TValue>) => {
const [formState, setFormState] = useState(defaultValue)
const firstRenderRef = useRef(true)
const submitRef = useRef(onSubmit)
const stateRef = useRef(formState)
const internalRef = useRef(
(firstRenderRef.current
? {
onSubmit,
onChange,
subscribers: [],
value:
typeof defaultValue === 'function'
? (defaultValue as () => TValue)()
: defaultValue,
}
: {}) as InternalState<TValue>
)
submitRef.current = onSubmit
stateRef.current = formState
internalRef.current = {
onSubmit,
onChange,
subscribers: internalRef.current.subscribers,
value: internalRef.current.value,
}
const handleChange = useCallback((e: Event) => {
firstRenderRef.current = false
const handleInputRef = useCallback(
(ref: HTMLInputElement | HTMLSelectElement | null) => {
if (ref) {
const name = ref.name
const predefinedValue = internalRef.current.value[name]
if (predefinedValue) {
ref.value = predefinedValue
}
}
},
[]
)
const handleInputChange = useCallback((e: Event) => {
const target = e.target as HTMLSelectElement | HTMLInputElement
setFormState(
(previous) =>
({
...previous,
[target.name]: target.value,
} as TValue)
)
internalRef.current.value = {
...internalRef.current.value,
[target.name]: target.value,
}
internalRef.current.onChange?.(internalRef.current.value)
internalRef.current.subscribers.forEach((s) => s(internalRef.current.value))
}, [])
const register = useCallback(
(name: keyof TValue) => {
return {
name,
ref: handleInputRef,
onChange: handleInputChange,
value: internalRef.current.value[name] ?? '',
}
},
[handleInputChange]
)
const watch = useCallback(
(name: keyof TValue) => {
const [value, setValue] = useState(internalRef.current.value[name] ?? '')
useEffect(() => {
const cb = (v: TValue) => setValue(v[name])
internalRef.current.subscribers.push(cb)
return () => {
const index = internalRef.current.subscribers.indexOf(cb)
if (index >= 0) {
internalRef.current.subscribers.splice(index, 1)
}
}
}, [])
return value
},
[handleInputChange]
)
const handleSubmit = useCallback((e: Event) => {
e.preventDefault()
e.stopPropagation()
submitRef.current?.(stateRef.current)
internalRef.current.onSubmit?.(internalRef.current.value)
}, [])
return {
value: formState,
handleChange,
register,
watch,
handleSubmit,
}
}