Перейти к содержимому

19. Формы и валидация

Яша, работа с формами — одна из самых частых задач. В Solid есть несколько подходов: управляемые и неуправляемые формы, createStore для сложного состояния, библиотека @modular-forms/solid и серверные action. Разберём всё от простого к сложному! 🎯


import { createSignal } from 'solid-js';
function ControlledForm() {
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
console.log({ name: name(), email: email() });
};
return (
<form onSubmit={handleSubmit}>
<input
value={name()}
onInput={(e) => setName(e.target.value)}
placeholder="Имя"
/>
<input
type="email"
value={email()}
onInput={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Отправить</button>
</form>
);
}

⚠️ Важно: В Solid используй onInput (не onChange!) для управляемых инпутов. onChange срабатывает только при потере фокуса.


function UncontrolledForm() {
let formRef: HTMLFormElement;
const handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
const data = new FormData(formRef);
console.log({
name: data.get('name'),
email: data.get('email'),
role: data.get('role'),
});
};
return (
<form ref={formRef!} onSubmit={handleSubmit}>
<input name="name" placeholder="Имя" />
<input name="email" type="email" placeholder="Email" />
<select name="role">
<option value="user">Пользователь</option>
<option value="admin">Администратор</option>
</select>
<button type="submit">Отправить</button>
</form>
);
}

💡 Неуправляемые формы проще и быстрее — нет лишних сигналов. Отлично для простых форм без валидации в реальном времени.


import { createStore } from 'solid-js/store';
interface FormState {
values: {
username: string;
email: string;
password: string;
confirmPassword: string;
};
errors: {
username?: string;
email?: string;
password?: string;
confirmPassword?: string;
};
touched: {
username?: boolean;
email?: boolean;
password?: boolean;
confirmPassword?: boolean;
};
isSubmitting: boolean;
}
function RegistrationForm() {
const [form, setForm] = createStore<FormState>({
values: { username: '', email: '', password: '', confirmPassword: '' },
errors: {},
touched: {},
isSubmitting: false,
});
const validate = (field: string, value: string) => {
if (field === 'username') {
if (!value) return 'Имя обязательно';
if (value.length < 3) return 'Минимум 3 символа';
if (!/^[a-z0-9_]+$/.test(value)) return 'Только a-z, 0-9, _';
}
if (field === 'email') {
if (!value) return 'Email обязателен';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Неверный формат';
}
if (field === 'password') {
if (!value) return 'Пароль обязателен';
if (value.length < 8) return 'Минимум 8 символов';
}
return undefined;
};
const handleInput = (field: keyof FormState['values']) => (e: InputEvent) => {
const value = (e.target as HTMLInputElement).value;
setForm('values', field, value);
// Валидация при вводе (если поле уже тронуто)
if (form.touched[field]) {
setForm('errors', field, validate(field, value));
}
};
const handleBlur = (field: keyof FormState['values']) => () => {
setForm('touched', field, true);
setForm('errors', field, validate(field, form.values[field]));
};
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
// Валидируем все поля
const errors = {};
for (const [field, value] of Object.entries(form.values)) {
const error = validate(field, value);
if (error) errors[field] = error;
}
if (Object.keys(errors).length > 0) {
setForm('errors', errors);
return;
}
setForm('isSubmitting', true);
await submitForm(form.values);
setForm('isSubmitting', false);
};
return (
<form onSubmit={handleSubmit}>
{/* Инпуты с валидацией */}
<For each={['username', 'email', 'password'] as const}>
{(field) => (
<div>
<input
value={form.values[field]}
onInput={handleInput(field)}
onBlur={handleBlur(field)}
/>
<Show when={form.errors[field]}>
<span class="error">{form.errors[field]}</span>
</Show>
</div>
)}
</For>
</form>
);
}

import { createForm, required, email, minLength } from '@modular-forms/solid';
type LoginForm = {
email: string;
password: string;
remember: boolean;
};
function LoginForm() {
const [loginForm, { Form, Field }] = createForm<LoginForm>();
const handleSubmit = (values: LoginForm) => {
console.log(values);
};
return (
<Form onSubmit={handleSubmit}>
<Field
name="email"
validate={[required('Email обязателен'), email('Неверный email')]}
>
{(field, props) => (
<div>
<input {...props} type="email" />
{field.error && <span>{field.error}</span>}
</div>
)}
</Field>
<Field
name="password"
validate={[required('Пароль обязателен'), minLength(8, 'Минимум 8 символов')]}
>
{(field, props) => (
<div>
<input {...props} type="password" />
{field.error && <span>{field.error}</span>}
</div>
)}
</Field>
<button type="submit" disabled={loginForm.submitting}>
{loginForm.submitting ? 'Вход...' : 'Войти'}
</button>
</Form>
);
}

// SolidStart: форма, работающая без JavaScript!
import { action, redirect } from '@solidjs/router';
const signUp = action(async (formData: FormData) => {
'use server';
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Валидация на сервере
if (!email || !email.includes('@')) {
return { error: 'Неверный email' };
}
// Создаём пользователя
await db.createUser({ email, password: await hash(password) });
// Редирект после успеха
throw redirect('/dashboard');
});
function SignUpForm() {
const [result, submit] = useAction(signUp);
return (
<form action={signUp} method="post">
<input name="email" type="email" required />
<input name="password" type="password" minlength="8" />
<Show when={result()?.error}>
<p class="error">{result()?.error}</p>
</Show>
<button type="submit">Зарегистрироваться</button>
</form>
);
}


// ❌ ОШИБКА: onChange вместо onInput
<input onChange={(e) => setName(e.target.value)} />
// Срабатывает только при потере фокуса!
// ✅ ПРАВИЛЬНО
<input onInput={(e) => setName(e.target.value)} />
// ❌ ОШИБКА: мутация store напрямую
const [form, setForm] = createStore({ name: '' });
form.name = 'Яша'; // Не реактивно!
// ✅ ПРАВИЛЬНО
setForm('name', 'Яша'); // или setForm({ name: 'Яша' })
// ❌ ОШИБКА: валидация при каждом нажатии (плохой UX)
<input onInput={(e) => {
setValue(e.target.value);
setError(validate(e.target.value)); // Ошибка мелькает при вводе
}}
/>