Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/add-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ilokesto/form": patch
---

Add Vue/Solid/Svelte login form examples and React validation flow example
25 changes: 25 additions & 0 deletions examples/react-validation-flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# React validation flow example

This Vite + React + TypeScript example demonstrates sync and async validation with `validateOn: ['change', 'blur', 'submit']`.

## What it demonstrates

- `useForm` with inline options (no pre-created `CreateForm` instance)
- `useField` for field binding with reactive `value`, `errors`, `dirty`, and `touched`
- Async Standard Schema validators that simulate server-side checks with a delay
- `validateOn: ['change', 'blur', 'submit']` so fields validate on every keystroke, blur, and submit
- `useFormState` for form-wide state (isDirty, isValid, submitCount)
- `handleSubmit` with type-safe onValid callback

Async validation uses a generation counter internally, so rapid typing always reflects the most recent values. Stale async results are discarded.

## Run

From the repository root:

```sh
pnpm --dir examples/react-validation-flow install
pnpm --dir examples/react-validation-flow dev
```

The example aliases `@ilokesto/form` and `@ilokesto/form/react` to the local `src/` files, so it can run before the package is built.
12 changes: 12 additions & 0 deletions examples/react-validation-flow/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>@ilokesto/form React validation flow example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/react-validation-flow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@ilokesto/form-react-validation-flow-example",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ilokesto/form": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.2.6",
"typescript": "^6.0.2",
"vite": "^8.1.0"
},
"devDependencies": {
"@babel/core": "^8.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@types/babel__core": "^7.20.5",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "^1.0.0"
}
}
139 changes: 139 additions & 0 deletions examples/react-validation-flow/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { CreateForm } from '@ilokesto/form';
import { useForm } from '@ilokesto/form/react';
import { useState } from 'react';
import './styles.css';

type FormValues = {
username: string;
email: string;
};

const DELAY_MS = 400;

function asyncCheck(
check: (value: string) => string | null,
): {
'~standard': {
version: 1;
vendor: 'example-async';
validate: (value: unknown) => Promise<{ value: unknown } | { issues: { message: string; path: never[] }[] }>;
};
} {
return {
'~standard': {
version: 1,
vendor: 'example-async',
async validate(value: unknown) {
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
const message = check(typeof value === 'string' ? value : '');
if (message) {
return { issues: [{ message, path: [] }] };
}
return { value };
},
},
};
}

const usernameSchema = asyncCheck((v) => {
if (v.trim().length < 3) return 'Username must be at least 3 characters';
if (!/^[a-z]/.test(v)) return 'Username must start with a lowercase letter';
return null;
});

const emailSchema = asyncCheck((v) => {
if (v.trim() === '') return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return 'Enter a valid email address';
return null;
});

function ValidationFlowExample() {
const [submitted, setSubmitted] = useState<FormValues | null>(null);
const { useField, useFormState, handleSubmit } = useForm<FormValues>({
defaultValues: { username: '', email: '' },
validateOn: ['change', 'blur', 'submit'],
});

const username = useField({ name: 'username', schema: usernameSchema });
const email = useField({ name: 'email', schema: emailSchema });
const state = useFormState();

return (
<main className="page-shell">
<section className="example-card" aria-labelledby="example-title">
<p className="eyebrow">React + Vite + TypeScript</p>
<h1 id="example-title">Validation flow example</h1>
<p className="description">
Demonstrates <code>validateOn: ['change', 'blur', 'submit']</code> with async
Standard Schema validators. Each field validates on every keystroke (change),
blur, and submit. The async schemas simulate server-side checks with a
{DELAY_MS}ms delay.
</p>

<form
className="form-grid"
onSubmit={handleSubmit(
(values) => setSubmitted(values),
)}
>
<label>
Username
<input {...username.props} placeholder="e.g. ada" />
<span className="field-hint">Validates on change, blur, and submit</span>
{username.errors.length > 0 && (
<ul className="field-errors">
{username.errors.map((e) => (
<li key={e.message}>{e.message}</li>
))}
</ul>
)}
</label>

<label>
Email
<input {...email.props} placeholder="ada@example.com" />
<span className="field-hint">Validates on change, blur, and submit</span>
{email.errors.length > 0 && (
<ul className="field-errors">
{email.errors.map((e) => (
<li key={e.message}>{e.message}</li>
))}
</ul>
)}
</label>

<div className="status-bar">
<span className="status-pill">isDirty: {String(state.isDirty)}</span>
<span className="status-pill">isValid: {String(state.isValid)}</span>
<span className="status-pill">submitCount: {state.submitCount}</span>
</div>

<button type="submit" disabled={!state.isDirty}>
Submit
</button>
</form>

{submitted && (
<div className="result-panel">
<strong>Submitted values</strong>
<pre>{JSON.stringify(submitted, null, 2)}</pre>
<button type="button" onClick={() => setSubmitted(null)}>
Clear result
</button>
</div>
)}

<pre>{JSON.stringify({
values: { username: username.value, email: email.value },
isDirty: state.isDirty,
isValid: state.isValid,
submitCount: state.submitCount,
dirtyFields: state.dirtyFields,
touchedFields: state.touchedFields,
}, null, 2)}</pre>
</section>
</main>
);
}

export default ValidationFlowExample;
10 changes: 10 additions & 0 deletions examples/react-validation-flow/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles.css';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
164 changes: 164 additions & 0 deletions examples/react-validation-flow/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
:root {
color: #172033;
background: #f4f7fb;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
}

button,
input {
font: inherit;
}

.page-shell {
display: grid;
min-height: 100vh;
place-items: center;
padding: 32px;
}

.example-card {
width: min(100%, 560px);
padding: 32px;
border: 1px solid #dce4f0;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 24px 70px rgb(23 32 51 / 12%);
}

.eyebrow {
margin: 0 0 8px;
color: #54617a;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}

h1 {
margin: 0;
font-size: clamp(2rem, 7vw, 3rem);
line-height: 1;
}

.description {
margin: 16px 0 28px;
color: #54617a;
line-height: 1.6;
}

.form-grid {
display: grid;
gap: 16px;
}

label {
display: grid;
gap: 6px;
color: #2b3448;
font-weight: 700;
}

input {
width: 100%;
border: 1px solid #c9d4e5;
border-radius: 12px;
padding: 12px 14px;
color: #172033;
background: #f9fbff;
}

input:focus-visible {
border-color: #4f7cff;
outline: 3px solid rgb(79 124 255 / 20%);
}

.field-hint {
color: #7a87a0;
font-size: 0.82rem;
font-weight: 400;
}

.field-errors {
margin: 0;
padding: 0 0 0 18px;
color: #d93025;
font-size: 0.88rem;
font-weight: 600;
}

.status-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}

.status-pill {
border: 1px solid #dce4f0;
border-radius: 999px;
padding: 8px 12px;
color: #41506a;
background: #f9fbff;
font-size: 0.9rem;
font-weight: 700;
}

button {
border: 0;
border-radius: 12px;
padding: 12px 16px;
color: #ffffff;
background: #345cf6;
font-weight: 800;
cursor: pointer;
}

button:disabled {
background: #aab4c8;
cursor: not-allowed;
}

button[type="button"] {
color: #213159;
background: #e9efff;
}

.result-panel {
display: grid;
gap: 10px;
margin-top: 20px;
border: 1px solid #bce0c9;
border-radius: 16px;
padding: 16px;
background: #eef9f1;
color: #1a5c2e;
}

.result-panel strong {
color: #0f3d1a;
}

.result-panel pre {
margin: 0;
border-radius: 12px;
padding: 12px;
color: #dce7ff;
background: #172033;
}

pre {
margin: 24px 0 0;
overflow: auto;
border-radius: 16px;
padding: 16px;
color: #dce7ff;
background: #172033;
}
1 change: 1 addition & 0 deletions examples/react-validation-flow/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Loading
Loading