Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/vite.config.ts → examples/react/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@lucas-barake/effect-form-react": path.resolve(__dirname, "../packages/form-react/src"),
"@lucas-barake/effect-form": path.resolve(__dirname, "../packages/form/src")
"@lucas-barake/effect-form-react": path.resolve(__dirname, "../../packages/form-react/src"),
"@lucas-barake/effect-form": path.resolve(__dirname, "../../packages/form/src")
}
}
})
12 changes: 12 additions & 0 deletions examples/solid/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Effect Form Examples</title>
</head>
<body style="margin: 0; background-color: #0f0f0f;">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions examples/solid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@lucas-barake/effect-form-examples-solid",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build:app": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@lucas-barake/effect-form": "workspace:^",
"@lucas-barake/effect-form-solid": "workspace:^",
"@effect-atom/atom": "^0.5.0",
"@effectify/solid-effect-atom": "^0.2.3",
"effect": "^3.19.15",
"solid-js": "^1.9.11"
},
"devDependencies": {
"vite-plugin-solid": "^2.11.10",
"typescript": "^5.9.3",
"vite": "^6.0.0"
}
}
53 changes: 53 additions & 0 deletions examples/solid/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createSignal, For } from "solid-js"
import { Dynamic } from "solid-js/web"
import { BasicForm } from "./examples/01-basic-form"
import { ValidationModes } from "./examples/02-validation-modes"
import { ArrayFields } from "./examples/03-array-fields"
import { CrossFieldValidation } from "./examples/04-cross-field-validation"
import { AsyncValidation } from "./examples/05-async-validation"
import { AutoSubmit } from "./examples/06-auto-submit"
import { MultiStepWizard } from "./examples/07-multi-step-wizard"
import { RevertChanges } from "./examples/08-revert-changes"
import styles from "./styles/app.module.css"

const examples = [
{ id: "basic", label: "Basic Form", component: BasicForm },
{ id: "validation-modes", label: "Validation Modes", component: ValidationModes },
{ id: "array-fields", label: "Array Fields", component: ArrayFields },
{ id: "cross-field", label: "Cross-Field Validation", component: CrossFieldValidation },
{ id: "async", label: "Async Validation", component: AsyncValidation },
{ id: "auto-submit", label: "Auto-Submit", component: AutoSubmit },
{ id: "multi-step", label: "Multi-Step Wizard", component: MultiStepWizard },
{ id: "revert", label: "Revert Changes", component: RevertChanges }
] as const

export function App() {
const [activeExample, setActiveExample] = createSignal<string>("basic")

const activeComponent = () => examples.find((e) => e.id === activeExample())?.component ?? BasicForm

return (
<div class={styles.container}>
<nav class={styles.nav}>
<h2 class={styles.navTitle}>Effect Form Examples</h2>
<ul class={styles.navList}>
<For each={examples}>
{(example) => (
<li>
<button
onClick={() => setActiveExample(example.id)}
class={`${styles.navButton} ${activeExample() === example.id ? styles.active : ""}`}
>
{example.label}
</button>
</li>
)}
</For>
</ul>
</nav>
<main class={styles.main}>
<Dynamic component={activeComponent()} />
</main>
</div>
)
}
26 changes: 26 additions & 0 deletions examples/solid/src/components/text-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { FormSolid } from "@lucas-barake/effect-form-solid"
import * as Option from "effect/Option"
import { Show } from "solid-js"
import styles from "../styles/form.module.css"

export const TextInput: FormSolid.FieldComponent<string> = (props) => (
<div class={styles.fieldContainer}>
<input
type="text"
value={props.field.value}
onInput={(e) => props.field.onChange(e.currentTarget.value)}
onBlur={props.field.onBlur}
class={`${styles.input} ${Option.isSome(props.field.error) ? styles.error : ""}`}
/>
<Show when={props.field.isValidating}>
<span class={styles.validatingText}>
Validating...
</span>
</Show>
<Show when={Option.isSome(props.field.error)}>
<span class={styles.errorText}>
{Option.getOrElse(props.field.error, () => "")}
</span>
</Show>
</div>
)
220 changes: 220 additions & 0 deletions examples/solid/src/examples/01-basic-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { useAtomSet, useAtomValue } from "@effectify/solid-effect-atom"
import * as Result from "@effect-atom/atom/Result"
import { Field, FormBuilder, FormSolid } from "@lucas-barake/effect-form-solid"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import { Show } from "solid-js"
import styles from "../styles/form.module.css"

class InvalidCredentialsError extends Data.TaggedError("InvalidCredentialsError")<{
readonly email: string
}> {}

class AccountLockedError extends Data.TaggedError("AccountLockedError")<{
readonly email: string
readonly unlockAt: Date
}> {}

const EmailField = Field.makeField(
"email",
Schema.String.pipe(Schema.nonEmptyString({ message: () => "Email is required" }))
)

const PasswordField = Field.makeField(
"password",
Schema.String.pipe(Schema.minLength(8, { message: () => "Password must be at least 8 characters" }))
)

const loginFormBuilder = FormBuilder.empty
.addField(EmailField)
.addField(PasswordField)

const EmailInput: FormSolid.FieldComponent<string> = (props) => (
<div class={styles.fieldContainer}>
<label class={styles.label}>
Email
<Show when={props.field.isDirty}><span class={styles.dirtyIndicator}>*</span></Show>
</label>
<input
type="email"
value={props.field.value}
onInput={(e) => props.field.onChange(e.currentTarget.value)}
onBlur={props.field.onBlur}
class={`${styles.input} ${Option.isSome(props.field.error) ? styles.error : ""}`}
/>
<Show when={props.field.isValidating}>
<span class={styles.validatingText}>
Validating...
</span>
</Show>
<Show when={Option.isSome(props.field.error)}>
<span class={styles.errorText}>
{Option.getOrElse(props.field.error, () => "")}
</span>
</Show>
</div>
)

const PasswordInput: FormSolid.FieldComponent<string> = (props) => (
<div class={styles.fieldContainer}>
<label class={styles.label}>
Password
<Show when={props.field.isDirty}><span class={styles.dirtyIndicator}>*</span></Show>
</label>
<input
type="password"
value={props.field.value}
onInput={(e) => props.field.onChange(e.currentTarget.value)}
onBlur={props.field.onBlur}
class={`${styles.input} ${Option.isSome(props.field.error) ? styles.error : ""}`}
/>
<Show when={Option.isSome(props.field.error)}>
<span class={styles.errorText}>
{Option.getOrElse(props.field.error, () => "")}
</span>
</Show>
</div>
)

const loginForm = FormSolid.make(loginFormBuilder, {
fields: {
email: EmailInput,
password: PasswordInput
},
onSubmit: (_, { decoded }) =>
Effect.gen(function*() {
yield* Effect.sleep("500 millis")

if (decoded.email === "locked@example.com") {
return yield* new AccountLockedError({
email: decoded.email,
unlockAt: new Date(Date.now() + 1000 * 60 * 30)
})
}

if (decoded.email === "invalid@example.com") {
return yield* new InvalidCredentialsError({ email: decoded.email })
}

yield* Effect.log(`Login successful: ${decoded.email}`)
return { email: decoded.email }
})
})

function SubmitButton() {
const isDirty = useAtomValue(loginForm.isDirty)
const submitResult = useAtomValue(loginForm.submit)

return (
<button
type="submit"
disabled={!isDirty() || submitResult().waiting}
class={styles.button}
>
{submitResult().waiting ? "Logging in..." : "Login"}
</button>
)
}

function SubmitStatus() {
const submitResult = useAtomValue(loginForm.submit)

return (
<>
{Result.builder(submitResult())
.onWaiting(() => null)
.onSuccess((value) => (
<div class={styles.alertSuccess}>
Login successful! Welcome, {value.email}
</div>
))
.onErrorTag(
"InvalidCredentialsError",
(error) => (
<div class={styles.alertError}>
Invalid credentials for {error.email}. Please check your email and password.
</div>
)
)
.onErrorTag(
"AccountLockedError",
(error) => (
<div class={styles.alertWarning}>
Account {error.email} is locked. Try again at {error.unlockAt.toLocaleTimeString()}.
</div>
)
)
.onErrorTag(
"ParseError",
() => (
<div class={styles.alertError}>
Please fix the validation errors above.
</div>
)
)
.onDefect((defect) => (
<div class={styles.alertError}>
Unexpected error: {String(defect)}
</div>
))
.orNull()}
</>
)
}

function FormDebug() {
const isDirty = useAtomValue(loginForm.isDirty)
const submitCount = useAtomValue(loginForm.submitCount)
const values = useAtomValue(loginForm.values)

return (
<div class={styles.debugBox}>
<strong>Form State:</strong>
<pre class={styles.debugPre}>
{JSON.stringify(
{
isDirty: isDirty(),
submitCount: submitCount(),
values: Option.getOrNull(values()),
},
null,
2
)}
</pre>
</div>
)
}

export function BasicForm() {
const submit = useAtomSet(loginForm.submit)

return (
<div class={styles.pageContainer}>
<h1 class={styles.pageTitle}>Basic Form</h1>
<p class={styles.pageDescription}>
Simple login form with type-safe error handling using <code>Data.TaggedError</code> and{" "}
<code>Result.builder()</code>.
</p>
<p class={styles.pageHint}>
Try: <code>invalid@example.com</code> for credentials error, <code>locked@example.com</code> for account locked.
</p>

<loginForm.Initialize defaultValues={{ email: "", password: "" }}>
<form
onSubmit={(e) => {
e.preventDefault()
submit()
}}
>
<loginForm.email />
<loginForm.password />
<SubmitButton />
<SubmitStatus />
<FormDebug />
</form>
</loginForm.Initialize>
</div>
)
}
Loading