Tiny form library
This commit is contained in:
parent
8c7e278042
commit
13afe2f8da
|
|
@ -1,6 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
--border-radius: 0.25rem;
|
--border-radius: 0.15rem;
|
||||||
--main-bg-color: #eee;
|
--main-bg-color: #eee;
|
||||||
--main-fg-color: #000;
|
--main-fg-color: #000;
|
||||||
--button-bg-color: #3988ff;
|
--button-bg-color: #3988ff;
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,9 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
|
|
||||||
const [data, setData] = useState(() => value.data)
|
const [data, setData] = useState(() => value.data)
|
||||||
|
|
||||||
const {
|
const { register, watch, handleSubmit } = useForm({
|
||||||
value: formState,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
} = useForm({
|
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
title: value.title,
|
title: value.title ?? '',
|
||||||
type: value.data?.type ?? '',
|
type: value.data?.type ?? '',
|
||||||
}),
|
}),
|
||||||
onSubmit: async (v) => {
|
onSubmit: async (v) => {
|
||||||
|
|
@ -44,6 +40,8 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const type = watch('type')
|
||||||
|
|
||||||
const deleteConfirm = useConfirmModal({
|
const deleteConfirm = useConfirmModal({
|
||||||
content: 'Are you sure you want to delete the box?',
|
content: 'Are you sure you want to delete the box?',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
|
|
@ -58,26 +56,17 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
<>
|
<>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input
|
<input {...register('title')} />
|
||||||
name="title"
|
|
||||||
value={formState.title}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
<select
|
<select {...register('type')} required>
|
||||||
name="type"
|
|
||||||
value={formState.type}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="graph">Graph</option>
|
<option value="graph">Graph</option>
|
||||||
<option value="dial">Dial</option>
|
<option value="dial">Dial</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formState.type === 'graph' && (
|
{type === 'graph' && (
|
||||||
<GraphSettings
|
<GraphSettings
|
||||||
value={data as DashboardGraphData}
|
value={data as DashboardGraphData}
|
||||||
onChange={setData}
|
onChange={setData}
|
||||||
|
|
@ -85,7 +74,7 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{formState.type === 'dial' && (
|
{type === 'dial' && (
|
||||||
<DialSettings
|
<DialSettings
|
||||||
value={data as DashboardDialData}
|
value={data as DashboardDialData}
|
||||||
onChange={setData}
|
onChange={setData}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { SensorInfo } from '@/api/sensors'
|
||||||
import { DashboardDialData } from '@/utils/dashboard/parseDashboard'
|
import { DashboardDialData } from '@/utils/dashboard/parseDashboard'
|
||||||
import { useForm } from '@/utils/hooks/useForm'
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
import { omit } from '@/utils/omit'
|
import { omit } from '@/utils/omit'
|
||||||
import { useEffect } from 'preact/hooks'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sensors: SensorInfo[]
|
sensors: SensorInfo[]
|
||||||
|
|
@ -11,26 +10,18 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialSettings = ({ sensors, value, onChange }: Props) => {
|
export const DialSettings = ({ sensors, value, onChange }: Props) => {
|
||||||
const { value: formState, handleChange } = useForm({
|
const { register } = useForm({
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
...(value && omit(value, ['type'])),
|
...(value && omit(value, ['type'])),
|
||||||
}),
|
}),
|
||||||
|
onChange: (v) => onChange({ ...v, type: 'dial' }),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange({ ...formState, type: 'dial' })
|
|
||||||
}, [formState])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Sensor</label>
|
<label>Sensor</label>
|
||||||
<select
|
<select required {...register('sensor')}>
|
||||||
name="sensor"
|
|
||||||
value={formState.sensor || ''}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
{sensors.map((s) => (
|
{sensors.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.name}
|
{s.name}
|
||||||
|
|
@ -40,26 +31,15 @@ export const DialSettings = ({ sensors, value, onChange }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Unit</label>
|
<label>Unit</label>
|
||||||
<input name="unit" value={formState.unit} onChange={handleChange} />
|
<input {...register('unit')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Decimals</label>
|
<label>Decimals</label>
|
||||||
<input
|
<input type="number" {...register('decimals')} />
|
||||||
type="number"
|
|
||||||
name="decimals"
|
|
||||||
value={formState.decimals}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Multiplier</label>
|
<label>Multiplier</label>
|
||||||
<input
|
<input type="number" step="any" {...register('multiplier')} />
|
||||||
type="number"
|
|
||||||
name="multiplier"
|
|
||||||
value={formState.multiplier}
|
|
||||||
onChange={handleChange}
|
|
||||||
step="any"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { SensorInfo } from '@/api/sensors'
|
||||||
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
|
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
|
||||||
import { useForm } from '@/utils/hooks/useForm'
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
import { omit } from '@/utils/omit'
|
import { omit } from '@/utils/omit'
|
||||||
import { useEffect } from 'preact/hooks'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sensors: SensorInfo[]
|
sensors: SensorInfo[]
|
||||||
|
|
@ -11,26 +10,20 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GraphSettings = ({ value, onChange, sensors }: Props) => {
|
export const GraphSettings = ({ value, onChange, sensors }: Props) => {
|
||||||
const { value: formState, handleChange } = useForm({
|
const { register, watch } = useForm({
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
...(value && omit(value, ['type'])),
|
...(value && omit(value, ['type'])),
|
||||||
}),
|
}),
|
||||||
|
onChange: (v) => onChange({ ...v, type: 'graph' }),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
const colorMode = watch('colorMode')
|
||||||
onChange({ ...formState, type: 'graph' })
|
|
||||||
}, [formState])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Sensor</label>
|
<label>Sensor</label>
|
||||||
<select
|
<select {...register('sensor')} required>
|
||||||
name="sensor"
|
|
||||||
value={formState.sensor || ''}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
{sensors.map((s) => (
|
{sensors.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.name}
|
{s.name}
|
||||||
|
|
@ -41,11 +34,7 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Graph Type</label>
|
<label>Graph Type</label>
|
||||||
<select
|
<select {...register('graphType')}>
|
||||||
name="graphType"
|
|
||||||
value={formState.graphType || 'line'}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="line">Line</option>
|
<option value="line">Line</option>
|
||||||
<option value="points">Points</option>
|
<option value="points">Points</option>
|
||||||
<option value="lineAndPoints">Line + Points</option>
|
<option value="lineAndPoints">Line + Points</option>
|
||||||
|
|
@ -55,11 +44,7 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Fill area</label>
|
<label>Fill area</label>
|
||||||
<select
|
<select {...register('fill')}>
|
||||||
name="fill"
|
|
||||||
value={formState.fill || ''}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
<option value="tozeroy">To zero</option>
|
<option value="tozeroy">To zero</option>
|
||||||
<option value="tonexty">To next value</option>
|
<option value="tonexty">To next value</option>
|
||||||
|
|
@ -69,49 +54,28 @@ export const GraphSettings = ({ value, onChange, sensors }: Props) => {
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Unit</label>
|
<label>Unit</label>
|
||||||
<input name="unit" value={formState.unit} onChange={handleChange} />
|
<input {...register('unit')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Min value</label>
|
<label>Min value</label>
|
||||||
<input
|
<input type="number" step="any" {...register('min')} />
|
||||||
type="number"
|
|
||||||
step="any"
|
|
||||||
name="min"
|
|
||||||
value={formState.min}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Max value</label>
|
<label>Max value</label>
|
||||||
<input
|
<input type="number" step="any" {...register('max')} />
|
||||||
type="number"
|
|
||||||
step="any"
|
|
||||||
name="max"
|
|
||||||
value={formState.max}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Color mode</label>
|
<label>Color mode</label>
|
||||||
<select
|
<select {...register('colorMode')}>
|
||||||
name="colorMode"
|
|
||||||
value={formState.colorMode ?? ''}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
<option value="static">Static</option>
|
<option value="static">Static</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formState.colorMode === 'static' && (
|
{colorMode === 'static' && (
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Max value</label>
|
<label>Max value</label>
|
||||||
<input
|
<input type="color" {...register('staticColor')} />
|
||||||
type="color"
|
|
||||||
name="staticColor"
|
|
||||||
value={formState.staticColor}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
|
||||||
const createMutation = useMutation(createSensor)
|
const createMutation = useMutation(createSensor)
|
||||||
const updateMutation = useMutation(updateSensor)
|
const updateMutation = useMutation(updateSensor)
|
||||||
|
|
||||||
const { value, handleSubmit, handleChange } = useForm({
|
const { handleSubmit, register } = useForm({
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
name: sensor?.name ?? '',
|
name: sensor?.name ?? '',
|
||||||
}),
|
}),
|
||||||
|
|
@ -47,12 +47,10 @@ export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
|
||||||
minLength={1}
|
minLength={1}
|
||||||
required
|
required
|
||||||
onChange={handleChange}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value.name}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
onSubmit?: (value: TValue) => void
|
||||||
|
onChange?: (value: TValue) => void
|
||||||
defaultValue: TValue | (() => TValue)
|
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,
|
defaultValue,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onChange,
|
||||||
}: Props<TValue>) => {
|
}: Props<TValue>) => {
|
||||||
const [formState, setFormState] = useState(defaultValue)
|
const firstRenderRef = useRef(true)
|
||||||
|
|
||||||
const submitRef = useRef(onSubmit)
|
const internalRef = useRef(
|
||||||
const stateRef = useRef(formState)
|
(firstRenderRef.current
|
||||||
|
? {
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
subscribers: [],
|
||||||
|
value:
|
||||||
|
typeof defaultValue === 'function'
|
||||||
|
? (defaultValue as () => TValue)()
|
||||||
|
: defaultValue,
|
||||||
|
}
|
||||||
|
: {}) as InternalState<TValue>
|
||||||
|
)
|
||||||
|
|
||||||
submitRef.current = onSubmit
|
internalRef.current = {
|
||||||
stateRef.current = formState
|
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
|
const target = e.target as HTMLSelectElement | HTMLInputElement
|
||||||
|
|
||||||
setFormState(
|
internalRef.current.value = {
|
||||||
(previous) =>
|
...internalRef.current.value,
|
||||||
({
|
[target.name]: target.value,
|
||||||
...previous,
|
}
|
||||||
[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) => {
|
const handleSubmit = useCallback((e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
submitRef.current?.(stateRef.current)
|
internalRef.current.onSubmit?.(internalRef.current.value)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: formState,
|
register,
|
||||||
handleChange,
|
watch,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue