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

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

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

4 питання

Повна форма директиви: v-назва:аргумент.модифікатор="значення". Наприклад, v-bind:href="url", v-on:submit.prevent="save".

Динамічний аргумент - назва атрибута чи події з виразу в квадратних дужках:

<a :[attributeName]="url">...</a>
<button @[eventName]="handler">...</button>

Якщо attributeName = 'href', це те саме, що :href="url". Значення null прибирає прив'язку.

Обмеження динамічних аргументів:

  • вираз має давати рядок або null;
  • без пробілів і лапок усередині дужок: :['data-' + key] - синтаксична помилка. Складні назви обчислюють у computed;
  • у шаблонах прямо в HTML-сторінці (не в .vue-файлах) браузер переводить атрибути в нижній регістр: :[someAttr] стане :[someattr] і не знайде змінну.

Прив'язка об'єкта атрибутів - v-bind без аргументу:

<input v-bind="{ id: 'email', type: 'email', required: true }">
<BaseInput v-bind="field" />   <!-- розгорнути об'єкт у props/атрибути -->

Скорочення з однаковою назвою (Vue 3.4+): якщо атрибут і змінна називаються однаково, значення можна опустити:

<img :src :alt>          <!-- те саме, що :src="src" :alt="alt" -->
<UserCard :user :role />

Kebab-case атрибут шукає camelCase змінну: :data-id → dataId.

Логічні атрибути: :disabled="isSaving" - атрибут з'являється при істинному значенні й зникає при хибному (false, null, undefined). Порожній рядок '' для логічних атрибутів вважається істинним, як у HTML.

Модифікатори v-bind:

  • .prop - встановити як властивість DOM, а не атрибут (:value.prop, скорочено .value);
  • .attr - навпаки, примусово як атрибут (:width.attr, скорочено ^width);
  • .camel - перетворити kebab-case на camelCase (для атрибутів SVG на кшталт viewBox у HTML-шаблонах).

Чому це корисно на практиці: спільні компоненти-обгортки (поле форми, кнопка-посилання), що приймають набір атрибутів ззовні, і генерація форм з конфігурації, де назви полів і подій приходять з даних.

Докладніше в документації: Синтаксис шаблонів

Vue сам керує DOM, але інколи потрібен прямий доступ до елемента: фокус, прокрутка, вимір розміру, ініціалізація сторонньої бібліотеки. Для цього - template ref.

Vue 3.5+ - useTemplateRef:

<script setup>
import { useTemplateRef, onMounted } from 'vue';

const input = useTemplateRef('search');

onMounted(() => input.value.focus());
</script>

<template>
  <input ref="search" type="search">
</template>

Рядок у useTemplateRef('search') збігається зі значенням атрибута ref.

До 3.5 - ref з тією самою назвою, що й атрибут:

const search = ref(null);   // <input ref="search">

Цей спосіб працює й далі, але useTemplateRef явніший: видно, що змінна - саме посилання на елемент, і не потрібно збігу назв змінних.

Коли ref заповнений:

  • після монтування - в setup і onBeforeMount він null;
  • під v-if стає null, коли елемент прибрано, - звідси перевірки input.value?.focus();
  • після зміни стану DOM оновлюється асинхронно: щоб звернутися до щойно показаного елемента, потрібен await nextTick():
async function openEditor() {
  editing.value = true;
  await nextTick();
  editorInput.value.focus();
}

Ref у v-for дає масив елементів (порядок не гарантовано збігається з масивом даних):

const items = useTemplateRef('items');   // <li v-for="..." ref="items">

Ref-функція - для повного контролю: :ref="(el) => { if (el) map.set(id, el) }" викликається з елементом при монтуванні й з null при видаленні.

Ref на компонент дає екземпляр компонента - але з <script setup> доступне лише те, що відкрито через defineExpose.

Коли НЕ треба ref: змінювати вміст, класи, атрибути напряму через DOM (el.textContent = ..., el.classList.add). Наступний рендер Vue перезапише зміни, а стан стане неузгодженим. Усе, що можна виразити через стан і шаблон, - через стан і шаблон.

Докладніше в документації: Template refs

Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в DOM усередині батьків, CSS батьків ламає позиціонування:

  • transform, filter, perspective на предку роблять position: fixed відносним до цього предка, а не до вікна;
  • overflow: hidden обрізає вікно;
  • z-index обмежений контекстом накладання (stacking context) батька.

<Teleport> рендерить вміст в інше місце DOM, лишаючи його логічно частиною компонента:

<script setup>
const open = ref(false);
</script>

<template>
  <button @click="open = true">Видалити акаунт</button>

  <Teleport to="body">
    <div v-if="open" class="fixed inset-0 z-50 grid place-items-center bg-black/50">
      <div role="dialog" aria-modal="true" class="rounded bg-white p-6">
        <p>Ви впевнені?</p>
        <button @click="open = false">Скасувати</button>
      </div>
    </div>
  </Teleport>
</template>

Що зберігається: реактивність, props, події, provide/inject - вміст лишається дочірнім у дереві компонентів. Змінюється лише місце в DOM.

Параметри:

  • to - CSS-селектор чи елемент; ціль має існувати на момент монтування;
  • :disabled="isMobile" - рендерити на місці (наприклад, на мобільних вікно стає звичайним блоком);
  • defer (Vue 3.5+) - відкласти пошук цілі до кінця поточного монтування, щоб ціллю міг бути елемент, який Vue рендерить пізніше в тому самому дереві.

Кілька Teleport в одну ціль дописують вміст по черзі - зручно для стеку сповіщень.

Що ще треба для якісного модального вікна (Teleport цього не робить):

  • фокус: перевести фокус у вікно при відкритті, втримувати його всередині (focus trap) і повертати на кнопку після закриття;
  • закриття по Escape і кліку на підкладку;
  • блокування прокрутки сторінки під вікном;
  • role="dialog", aria-modal, aria-labelledby для зчитувачів екрана.

Нативна альтернатива - елемент <dialog> з showModal(): браузер сам дає верхній шар (top layer, без проблем із z-index), фокус і Escape. У сучасних застосунках це часто простіше за Teleport.

SSR: телепортований вміст рендериться окремо - його треба вставити в HTML вручну, інакше буде розбіжність при гідратації.

Докладніше в документації: Teleport

<Transition> анімує один елемент чи компонент, коли той з'являється або зникає через v-if, v-show, зміну динамічного компонента чи key.

<Transition name="fade">
  <p v-if="visible">Збережено</p>
</Transition>

<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>

Класи, які Vue додає по черзі (name - префікс):

  • -enter-from → -enter-active → -enter-to - поява;
  • -leave-from → -leave-active → -leave-to - зникнення.

Vue сам визначає, коли анімація закінчилася (за transitionend/animationend), і лише тоді прибирає елемент з DOM.

З Tailwind - класи напряму через props:

<Transition
  enter-active-class="transition duration-200 ease-out"
  enter-from-class="opacity-0 -translate-y-2"
  leave-active-class="transition duration-150 ease-in"
  leave-to-class="opacity-0"
>

Режими для заміни одного елемента іншим: mode="out-in" - спершу зникає старий, потім з'являється новий (без накладання).

<TransitionGroup> - для списків (v-for): анімує додавання, видалення й переміщення елементів:

<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.title }}</li>
</TransitionGroup>

<style>
.list-move { transition: transform 0.3s; }   /* плавне переміщення при зміні порядку */
.list-leave-active { position: absolute; }  /* щоб сусіди плавно зсувалися */
</style>

Обов'язково :key на кожному елементі - без нього Vue не знає, що анімувати.

JavaScript-хуки (@before-enter, @enter, @leave з колбеком done) - для анімацій бібліотеками (GSAP, Web Animations API).

Пастки:

  • <Transition> очікує рівно один кореневий елемент - компонент з кількома коренями всередині не анімується;
  • анімація не запускається при першому рендері - для цього атрибут appear;
  • prefers-reduced-motion - користувачі, які вимкнули анімації в системі, мають їх не бачити: @media (prefers-reduced-motion: reduce) чи motion-reduce: у Tailwind;
  • для переходів між сторінками в сучасних браузерах можна використати View Transitions API.

Докладніше в документації: Transition