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

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

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

4 питання

v-model - двостороння прив'язка поля форми до стану: введене потрапляє в змінну, зміна змінної - у поле.

<script setup>
import { ref } from 'vue';

const name = ref('');
const agree = ref(false);
const city = ref('kyiv');
const tags = ref([]);
</script>

<template>
  <input v-model="name">
  <input type="checkbox" v-model="agree">
  <select v-model="city">
    <option value="kyiv">Київ</option>
    <option value="lviv">Львів</option>
  </select>
  <label><input type="checkbox" value="php" v-model="tags"> PHP</label>
  <label><input type="checkbox" value="vue" v-model="tags"> Vue</label>
</template>

Для різних елементів v-model використовує різні властивості й події: value + input для тексту, checked + change для чекбоксів і радіо, value + change для select. Чекбокси з value, прив'язані до масиву, додають і прибирають значення.

Модифікатори:

  • .lazy - оновлювати на change (вихід з поля), а не на кожне натискання;
  • .number - перетворювати введене на число (parseFloat). Якщо перетворити неможливо, лишається рядок. Для type="number" застосовується автоматично;
  • .trim - обрізати пробіли на початку й у кінці.
<input v-model.trim="email" type="email">
<input v-model.number="age" type="number">
<textarea v-model.lazy="bio"></textarea>

Пастки:

  • v-model ігнорує атрибути value, checked, selected у розмітці - джерело істини лише стан. Початкове значення задається в ref;
  • select без початкового значення, що збігається з опцією, в iOS не дає вибрати першу опцію - тому додають порожню <option disabled value="">Оберіть місто</option>;
  • IME (введення ієрогліфів, частина мобільних клавіатур) - v-model не оновлює значення під час складання символу; якщо потрібно - власний обробник input;
  • v-model на prop - не можна: props належать батькові. Для компонента-поля - defineModel().

Значення не лише рядки: :value="{ id: 1 }" на опціях select чи чекбоксах дає змогу прив'язувати об'єкти, а true-value/false-value на чекбоксі - власні значення замість true/false.

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

У Vue 3 на одному елементі v-if має вищий пріоритет, ніж v-for. Умова обчислюється раніше за цикл - і не має доступу до змінної циклу.

<!-- помилка: todo ще не визначено, коли перевіряється v-if -->
<li v-for="todo in todos" v-if="!todo.done">{{ todo.title }}</li>

У Vue 2 було навпаки (v-for першим), тож код, перенесений зі старих проєктів, ламається саме тут.

Правильні варіанти:

1. Фільтр - в обчислюваній властивості (найкращий варіант для фільтрації):

<script setup>
const activeTodos = computed(() => todos.value.filter((todo) => !todo.done));
</script>

<template>
  <li v-for="todo in activeTodos" :key="todo.id">{{ todo.title }}</li>
</template>

Фільтр виконується лише при зміні todos, а не на кожен рендер, і шаблон стає простішим.

2. <template> з v-for зовні, v-if усередині - коли умова для кожного елемента окрема:

<template v-for="todo in todos" :key="todo.id">
  <li v-if="!todo.done">{{ todo.title }}</li>
</template>

3. v-if зовні - для умови на весь список:

<ul v-if="todos.length">
  <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
<p v-else>Завдань немає</p>

Що ще варто знати про v-for:

  • :key обов'язковий - стабільний ідентифікатор, а не індекс;
  • перебір об'єкта: v-for="(value, key, index) in object";
  • діапазон: v-for="n in 10" дає числа від 1 до 10;
  • на <template> key ставиться на сам <template>.

Чому computed краще за фільтр у шаблоні: вираз v-for="todo in todos.filter(...)" створює новий масив на кожному рендері компонента - навіть якщо змінилося щось зовсім не пов'язане зі списком.

Докладніше в документації: v-for разом з v-if

Для class і style Vue має розширений синтаксис :class / :style: крім рядків, вони приймають об'єкти й масиви.

Об'єкт - клас за умовою:

<button :class="{ 'opacity-50': loading, 'bg-red-600': danger }">Видалити</button>

Ключ - клас, значення - умова. Статичний class і динамічний :class об'єднуються:

<div class="card" :class="{ 'ring-2': selected }"></div>

Масив - кілька джерел класів:

<div :class="[sizeClass, { 'font-bold': active }, isError ? 'text-red-600' : 'text-gray-900']"></div>

Обчислювана властивість - коли логіка складніша:

const badgeClass = computed(() => ({
  paid: 'bg-green-100 text-green-800',
  pending: 'bg-amber-100 text-amber-800',
  failed: 'bg-red-100 text-red-800',
})[props.status]);

Стилі - об'єкт з camelCase чи kebab-case властивостями:

<div :style="{ width: progress + '%', backgroundColor: color }"></div>
<div :style="{ '--accent': theme.accent }"></div>   <!-- CSS-змінна -->

Vue автоматично додає вендорні префікси, якщо властивість їх потребує.

На компонентах: class і style, передані компоненту, додаються до класів його кореневого елемента.

Пастки з Tailwind:

  • не збирати назви класів з частин: :class="`bg-${color}-500`" - Tailwind сканує файли й генерує лише ті класи, які бачить повністю. Склеєних класів у CSS не буде. Потрібна мапа з повними назвами (як у прикладі з badgeClass);
  • конфлікти класів: class="p-4" на компоненті і p-2 всередині дають обидва класи - переможе той, що пізніше в згенерованому CSS, а не той, що «ззовні». Для керованого злиття - утиліти на кшталт tailwind-merge.

Динамічні значення з даних (ширина прогрес-бару, колір користувача) краще через :style чи CSS-змінні, а не через генерацію класів.

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

v-on (скорочено @) підписує обробник на подію DOM чи компонента:

<button @click="count++">+1</button>                 <!-- вираз -->
<button @click="save">Зберегти</button>              <!-- метод: отримає об'єкт події -->
<button @click="remove(item.id)">Видалити</button>   <!-- виклик з аргументами -->
<button @click="remove(item.id, $event)">...</button><!-- $event - сама подія -->

Модифікатори подій замість ручного event.preventDefault():

Модифікатор Що робить
.prevent preventDefault() - форма не перезавантажує сторінку
.stop stopPropagation()
.self лише якщо подія виникла на самому елементі
.once спрацювати один раз
.capture слухати у фазі перехоплення
.passive пасивний слухач (прокрутка без затримок)
<form @submit.prevent="submit">...</form>
<div class="overlay" @click.self="close">...</div>

Модифікатори клавіш і миші:

<input @keyup.enter="search">
<input @keydown.esc="clear">
<textarea @keydown.ctrl.enter="send"></textarea>
<button @click.right.prevent="openMenu">...</button>
<input @keyup.enter.exact="send">   <!-- лише Enter, без Shift/Ctrl -->

Доступні .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, системні .ctrl, .alt, .shift, .meta, а також будь-яка назва клавіші з event.key у kebab-case (.page-down).

Порядок модифікаторів має значення: @click.prevent.self скасовує дію для кліків на будь-якому дочірньому елементі, а @click.self.prevent - лише на самому елементі.

Події компонентів слухаються так само: @save="onSave". Модифікатори на кшталт .prevent до них не застосовуються - це не DOM-події.

Пастки:

  • .passive разом з .prevent - конфлікт: пасивний слухач не може скасувати дію, браузер проігнорує .prevent і виведе попередження;
  • важкі обробники на scroll, input, mousemove - потрібне обмеження частоти (debounce/throttle), Vue сам цього не робить;
  • обробники на window/document у шаблоні не повісиш - їх додають в onMounted і знімають в onUnmounted (або useEventListener з VueUse).

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