Skip to content

Commit

Permalink
Merge pull request #16 from khmm12/chore/migrate-to-tanstack-forms
Browse files Browse the repository at this point in the history
chore: migrate to tanstack forms
  • Loading branch information
khmm12 authored Jan 17, 2025
2 parents 81758c0 + 3b510a2 commit d4fbf6d
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"*.css"
],
"dependencies": {
"@modular-forms/solid": "^0.25.0",
"@tanstack/solid-form": "^0.41.2",
"@total-typescript/ts-reset": "^0.6.1",
"date-fns": "^4.1.0",
"focus-trap": "^7.6.2",
Expand Down
51 changes: 36 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 73 additions & 26 deletions src/components/SettingsDialog/components/SettingsForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createEffect, For, type JSX, on } from 'solid-js'
import { css } from 'styled-system/css'
import { createForm, reset } from '@modular-forms/solid'
import type { Simplify } from 'type-fest'
import { createForm } from '@tanstack/solid-form'
import createUniqueIds from '@/hooks/createUniqueIds'
import { MilestoneProgressStyle, type Settings, ThemeColorMode } from '@/shared/settings'
import type { ISODate } from '@/utils/brands'
Expand All @@ -26,15 +25,24 @@ const MilestoneProgressStyleOptions = [
]

export default function SettingsForm(props: SettingsFormProps): JSX.Element {
const [form, { Form, Field }] = createForm<Simplify<Settings>>({
initialValues: props.initialValues,
})
const form = createForm<Settings>(() => ({
defaultValues: { ...props.initialValues },
async onSubmit({ value: values }) {
const nextValues: Settings = {
milestoneProgressStyle: values.milestoneProgressStyle,
themeColorMode: values.themeColorMode,
}
if (values.birthDate != null) nextValues.birthDate = values.birthDate

await props.onSubmit(nextValues)
},
}))

createEffect(
on(
() => ({ ...props.initialValues }), // Subscribe to all fields
(initialValues) => {
if (!form.dirty) reset(form, { initialValues })
if (!form.state.isDirty) form.reset(initialValues)
},
{ defer: true },
),
Expand All @@ -43,68 +51,107 @@ export default function SettingsForm(props: SettingsFormProps): JSX.Element {
const ids = createUniqueIds(['birthDate', 'milestoneProgressStyle', 'themeColorMode'])

return (
<Form class={css(s.container)} role="form" aria-label="Settings" onSubmit={props.onSubmit}>
<Field name="themeColorMode">
{(field, input) => (
<form class={css(s.container)} aria-label="Settings" onSubmit={withCanceled(form.handleSubmit)}>
<form.Field name="themeColorMode">
{(field) => (
<div class={css(s.formGroup)}>
<label for={ids.themeColorMode} class={css(s.label)}>
Theme color mode
</label>
<select {...input} id={ids.themeColorMode} class={css(s.select)} style={s.selectInline}>
<select
name={field().name}
id={ids.themeColorMode}
class={css(s.select)}
style={s.selectInline}
onChange={selectHandler(ThemeColorModeOptions, field().handleChange)}
onBlur={field().handleBlur}
>
<For each={ThemeColorModeOptions}>
{({ label, value }) => (
<option value={value} selected={field.value === value}>
<option value={value} selected={field().state.value === value}>
{label}
</option>
)}
</For>
</select>
</div>
)}
</Field>
<Field name="milestoneProgressStyle">
{(field, input) => (
</form.Field>
<form.Field name="milestoneProgressStyle">
{(field) => (
<div class={css(s.formGroup)}>
<label for={ids.milestoneProgressStyle} class={css(s.label)}>
Milestone progress style
</label>
<select {...input} id={ids.milestoneProgressStyle} class={css(s.select)} style={s.selectInline}>
<select
id={ids.milestoneProgressStyle}
class={css(s.select)}
style={s.selectInline}
name={field().name}
onChange={selectHandler(MilestoneProgressStyleOptions, field().handleChange)}
onBlur={field().handleBlur}
>
<For each={MilestoneProgressStyleOptions}>
{({ label, value }) => (
<option value={value} selected={field.value === value}>
<option value={value} selected={field().state.value === value}>
{label}
</option>
)}
</For>
</select>
</div>
)}
</Field>
<Field name="birthDate" transform={parseDateValue}>
{(field, input) => (
</form.Field>
<form.Field name="birthDate">
{(field) => (
<div class={css(s.formGroup)}>
<label for={ids.birthDate} class={css(s.label)}>
Birth date
</label>
<input
{...input}
id={ids.birthDate}
class={css(s.input)}
placeholder="Birthdate"
type="date"
value={field.value ?? ''}
name={field().name}
value={field().state.value ?? ''}
onChange={(e) => {
field().handleChange(parseDateValue(e.target.value))
}}
onBlur={field().handleBlur}
/>
</div>
)}
</Field>
<button class={css(s.button)} disabled={form.submitting} type="submit">
Save
</button>
</Form>
</form.Field>
<form.Subscribe selector={(state) => ({ isSubmitting: state.isSubmitting, canSubmit: state.canSubmit })}>
{(state) => (
<button class={css(s.button)} disabled={!state().canSubmit} type="submit">
{state().isSubmitting ? 'Saving' : 'Save'}
</button>
)}
</form.Subscribe>
</form>
)
}

function parseDateValue(value: string | undefined): ISODate | undefined {
const parsed = value != null && value !== '' ? new Date(value) : null
return parsed != null && !Number.isNaN(parsed.valueOf()) ? toISODate(parsed) : undefined
}

function withCanceled(fn: () => void | Promise<void>): (e: SubmitEvent) => void {
return (e) => {
e.preventDefault()
e.stopPropagation()
Promise.resolve(fn()).catch(() => {})
}
}

function selectHandler<T extends string>(
_options: ReadonlyArray<{ value: T }>,
onChange: (value: T) => void,
): (e: Event & { target: HTMLSelectElement }) => void {
return (e) => {
onChange(e.target.value as T)
}
}

0 comments on commit d4fbf6d

Please sign in to comment.