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

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}>
<input type="email" name="email" placeholder="[email protected]" />
<button type="submit">Подписаться</button>
{action.value?.success && <p>✅ Подписка оформлена!</p>}
</Form>
);
});
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>
);
});
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>
)}
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>
);
});