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 перезапише зміни, а стан стане неузгодженим. Усе, що можна виразити через стан і шаблон, - через стан і шаблон.
Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в 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 вручну, інакше буде розбіжність при гідратації.
<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.