Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Форми й валідація»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Найпростіший підхід - один реактивний об'єкт з усіма полями форми і v-model на кожному полі:

<script setup lang="ts">
import { reactive, ref } from 'vue'

const initial = () => ({ name: '', email: '', role: 'editor', agree: false })

const form = reactive(initial())
const submitting = ref(false)

async function submit() {
  submitting.value = true
  try {
    await api.post('/users', { ...form })
    Object.assign(form, initial())   // скидання
  } finally {
    submitting.value = false
  }
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model.trim="form.name" />
    <input v-model="form.email" type="email" />
    <select v-model="form.role">
      <option value="editor">Редактор</option>
      <option value="admin">Адміністратор</option>
    </select>
    <input v-model="form.agree" type="checkbox" />
    <button :disabled="submitting">Зберегти</button>
  </form>
</template>

reactive чи ref:

reactive({...}) ref({...})
доступ у <script> form.email form.value.email
замінити весь об'єкт ні - лише Object.assign так: form.value = initial()
деструктуризація ламає реактивність теж ламає (form.value - реактивний об'єкт)

ref зручніший, коли форму треба повністю замінити (завантажили дані для редагування). reactive - коротший запис полів.

Модифікатори v-model: .trim прибирає пробели, .number перетворює на число, .lazy оновлює значення на change, а не на кожен введений символ.

Типові помилки:

  • @submit без .prevent - браузер відправляє форму звичайним запитом і перезавантажує сторінку;
  • скидання через form = initial() для reactive - зміниться лише локальна змінна, а шаблон лишиться прив'язаним до старого об'єкта;
  • відправка самого реактивного проксі - бібліотеки зазвичай серіалізують його коректно, але безпечніше передавати звичайну копію ({ ...form } чи toRaw(form));
  • checkbox для масиву: кілька чекбоксів з одним v-model і масивом у стані дають список вибраних значень;
  • немає захисту від подвійного кліку - кнопка має бути вимкнена на час відправки.

Для складних форм (валідація, вкладені масиви, стан «змінено») варто взяти бібліотеку форм, а не винаходити все самому.

Докладніше в документації: Vue: прив'язка полів форми

Коли валідація Laravel не проходить у запиті, що очікує JSON, сервер відповідає статусом 422 і тілом:

{
  "message": "The email has already been taken. (and 1 more error)",
  "errors": {
    "email": ["The email has already been taken."],
    "items.0.qty": ["The items.0.qty field must be at least 1."]
  }
}

Ключі - назви полів, для вкладених даних - через крапку. Значення - масив повідомлень.

Обробка в Vue:

<script setup lang="ts">
import { reactive, ref } from 'vue'
import axios from 'axios'

const form = reactive({ name: '', email: '' })
const errors = ref<Record<string, string[]>>({})

async function submit() {
  errors.value = {}
  try {
    await axios.post('/api/users', form, { headers: { Accept: 'application/json' } })
  } catch (e) {
    if (axios.isAxiosError(e) && e.response?.status === 422) {
      errors.value = e.response.data.errors
      return
    }
    throw e   // інші помилки - далі, до глобального обробника
  }
}
</script>

<template>
  <input v-model="form.email" :aria-invalid="!!errors.email" />
  <p v-if="errors.email" class="error">{{ errors.email[0] }}</p>
</template>

З fetch код схожий, але fetch не кидає виняток на 422 - перевіряти треба response.status самостійно, а тіло читати через await response.json().

Важливі деталі:

  • заголовок Accept: application/json обов'язковий: без нього Laravel вважає запит звичайним переходом форми й відповідає редиректом назад з помилками в сесії, а не JSON;
  • очищайте помилки перед новою відправкою, інакше старі повідомлення лишаються на екрані;
  • прибирайте помилку поля при його зміні - watch на поле чи очищення в @input - користувач бачить, що виправлення прийнято;
  • вкладені ключі (items.0.qty) треба зіставити з полем у списку - зберігайте помилки за тим самим шаблоном назв;
  • 401 і 419 - це не валідація: 401 - не автентифікований, 419 - прострочений CSRF-токен (для SPA на Sanctum спершу потрібен запит до /sanctum/csrf-cookie).

Типізація: опишіть тип відповіді 422 ({ message: string; errors: Record<string, string[]> }) один раз і використовуйте в усіх формах - через спільну функцію чи composable useServerErrors().

Докладніше в документації: Axios: обробка помилок

Доступна форма - та, яку можна заповнити з клавіатури й зрозуміти з екранним читачем. Більшість потрібного дає правильний HTML, а Vue лише має не зламати його.

1. Мітка для кожного поля, пов'язана з полем:

<script setup>
import { useId } from 'vue'
const id = useId()
</script>

<template>
  <label :for="id">Email</label>
  <input :id="id" v-model="form.email" type="email" autocomplete="email" required />
</template>

Плейсхолдер - не мітка: він зникає при введенні й часто має низький контраст. useId() (Vue 3.5) генерує стабільний id, однаковий на сервері й у браузері при SSR.

2. Помилка, пов'язана з полем:

<input
  :id="id"
  v-model="form.email"
  :aria-invalid="errors.email ? 'true' : undefined"
  :aria-describedby="errors.email ? `${id}-error` : undefined"
/>
<p v-if="errors.email" :id="`${id}-error`">{{ errors.email[0] }}</p>
  • aria-invalid повідомляє, що значення некоректне;
  • aria-describedby - екранний читач прочитає текст помилки разом з назвою поля;
  • помилка не повинна передаватися лише кольором (червона рамка) - потрібен текст.

3. Фокус на першому полі з помилкою після відправки:

import { nextTick } from 'vue'

async function onInvalid() {
  await nextTick()   // дочекатися рендеру повідомлень
  document.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus()
}

Без цього користувач клавіатури чи екранного читача натискає «Зберегти» - і нічого не відбувається з його точки зору.

4. Зведення помилок для довгих форм: блок з role="alert" нагорі зі списком помилок-посилань на поля.

Ще кілька правил:

  • <button type="submit"> у <form>, а не div з @click - тоді працює Enter і екранні читачі розуміють, що це форма;
  • групи перемикачів - у <fieldset> з <legend>;
  • autocomplete (email, new-password, one-time-code) - менеджери паролів і мобільні клавіатури;
  • стан відправки - aria-busy чи текст на кнопці, а не лише спінер;
  • не вимикайте кнопку відправки до заповнення всіх полів: користувач не розуміє, чого бракує. Краще дозволити натиснути й показати помилки.

Перевірка: пройти форму лише з клавіатури (Tab, Enter, Space) і запустити axe DevTools чи Lighthouse.

Докладніше в документації: MDN: aria-invalid