Tiny form library
This commit is contained in:
parent
8c7e278042
commit
13afe2f8da
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
internalRef.current.value = {
|
||||
...internalRef.current.value,
|
||||
[target.name]: target.value,
|
||||
} as TValue)
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue