11. Формы и zod-форма
Qwik City предоставляет мощную систему работы с формами через routeAction$, Form компонент и валидацию через Zod. Всё это работает как с JavaScript, так и без JavaScript (progressive enhancement).
Базовая форма
Заголовок раздела «Базовая форма»import { component$ } from '@builder.io/qwik';import { routeAction$, Form } from '@builder.io/qwik-city';
export const useSubscribeAction = routeAction$(async (data) => { await subscribe(data.email as string); return { success: true };});
export default component$(() => { const action = useSubscribeAction();
return ( <Form action={action}> <button type="submit">Подписаться</button>
{action.value?.success && <p>✅ Подписка оформлена!</p>} </Form> );});Валидация с Zod
Заголовок раздела «Валидация с Zod»import { routeAction$, zod$, z } from '@builder.io/qwik-city';
const SignUpSchema = z.object({ name: z.string() .min(2, 'Имя должно быть не менее 2 символов') .max(50, 'Имя не может быть длиннее 50 символов'), email: z.string() .email('Введите корректный email'), password: z.string() .min(8, 'Пароль должен быть не менее 8 символов') .regex(/[A-Z]/, 'Должна быть хотя бы одна заглавная буква') .regex(/[0-9]/, 'Должна быть хотя бы одна цифра'), confirmPassword: z.string(),}).refine(data => data.password === data.confirmPassword, { message: 'Пароли не совпадают', path: ['confirmPassword'],});
export const useSignUp = routeAction$( async (data, { redirect }) => { await createUser(data); throw redirect(302, '/dashboard'); }, zod$(SignUpSchema));Отображение ошибок валидации
Заголовок раздела «Отображение ошибок валидации»export default component$(() => { const signUp = useSignUp();
return ( <Form action={signUp}> <div> <input name="email" type="email" /> {/* Ошибки валидации автоматически типизированы */} {signUp.value?.fieldErrors?.email && ( <span class="error">{signUp.value.fieldErrors.email}</span> )} </div>
<div> <input name="password" type="password" /> {signUp.value?.fieldErrors?.password && ( <span class="error">{signUp.value.fieldErrors.password[0]}</span> )} </div>
<button type="submit" disabled={signUp.isRunning}> {signUp.isRunning ? 'Загрузка...' : 'Зарегистрироваться'} </button> </Form> );});fail() — возврат ошибки
Заголовок раздела «fail() — возврат ошибки»export const useLogin = routeAction$(async (data, { fail }) => { const user = await findUser(data.email as string);
if (!user || user.password !== data.password) { return fail(401, { message: 'Неверный email или пароль' }); }
// Успешный вход return { userId: user.id };});
// В компоненте:{loginAction.value?.message && ( <div class="alert-error">{loginAction.value.message}</div>)}Оптимистичный UI
Заголовок раздела «Оптимистичный UI»export const useLikePost = routeAction$(async (data) => { await db.posts.update({ id: data.id, likes: { increment: 1 } });});
export const LikeButton = component$<{ postId: string; likes: number }>((props) => { const likeAction = useLikePost();
// Оптимистично показываем +1 до ответа сервера const optimisticLikes = likeAction.isRunning ? props.likes + 1 : props.likes;
return ( <Form action={likeAction}> <input type="hidden" name="id" value={props.postId} /> <button type="submit">❤️ {optimisticLikes}</button> </Form> );});