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

7. Шаблоны и директивы

Шаблоны Vue — это расширенный HTML. Всё что ты знаешь про HTML — работает. Плюс добавляются Vue-директивы: специальные атрибуты с префиксом v-, которые добавляют интерактивность. Компилятор Vue превращает шаблон в оптимизированный JavaScript — быстрее, чем ты напишешь сам! 🚀


<template>
<!-- {{ }} — текстовая интерполяция -->
<p>{{ message }}</p>
<p>{{ user.name }}</p>
<p>{{ count * 2 }}</p>
<p>{{ isLoggedIn ? 'Привет!' : 'Войти' }}</p>
<p>{{ formatDate(createdAt) }}</p>
<!-- v-html — вставка HTML (осторожно с XSS!) -->
<div v-html="rawHtml"></div>
<!-- v-text — то же что {{ }}, но без interpolation -->
<span v-text="message"></span>
</template>

<template>
<!-- v-if полностью убирает/добавляет элемент в DOM -->
<div v-if="status === 'loading'">
<Spinner />
</div>
<div v-else-if="status === 'error'">
<ErrorMessage :message="errorText" />
</div>
<div v-else-if="status === 'empty'">
<EmptyState />
</div>
<div v-else>
<UserList :users="users" />
</div>
<!-- <template> как wrapper — не создаёт DOM элемент! -->
<template v-if="isLoggedIn">
<UserMenu />
<Notifications />
<ProfileLink />
</template>
</template>

v-if vs v-show:

<template>
<!-- v-if — элемент УДАЛЯЕТСЯ из DOM (дороже при переключении) -->
<!-- Используй для редко меняющихся условий -->
<div v-if="isVisible">Условный контент</div>
<!-- v-show — элемент скрыт через display:none (дешевле при переключении) -->
<!-- Используй для часто меняющейся видимости -->
<div v-show="isVisible">Показываемый/скрываемый контент</div>
</template>

<template>
<!-- Массив объектов — ВСЕГДА добавляй :key! -->
<ul>
<li
v-for="user in users"
:key="user.id"
>
{{ user.name }} — {{ user.email }}
</li>
</ul>
<!-- Индекс в цикле -->
<div
v-for="(item, index) in items"
:key="item.id"
>
{{ index + 1 }}. {{ item.name }}
</div>
<!-- Объект (key, value, index) -->
<dl>
<div
v-for="(value, key, index) in userProfile"
:key="key"
>
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</div>
</dl>
<!-- Диапазон чисел -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- Выведет: 1 2 3 4 5 -->
<!-- <template> в v-for — несколько элементов на итерацию -->
<template v-for="user in users" :key="user.id">
<tr><td>{{ user.name }}</td></tr>
<tr><td>{{ user.email }}</td></tr>
</template>
</template>

Важность :key:

<!-- ❌ Без key — Vue путается при сортировке/фильтрации -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ❌ Index как key — плохо при изменении порядка! -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
<!-- ✅ Уникальный стабильный id как key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

<template>
<!-- Полный синтаксис -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt" />
<!-- Сокращение : (рекомендуется) -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- Динамический класс -->
<div :class="activeClass">Текст</div>
<div :class="{ active: isActive, error: hasError }">Текст</div>
<div :class="[baseClass, isActive ? 'active' : '']">Текст</div>
<div :class="['btn', isLarge ? 'btn-lg' : 'btn-sm', { 'btn-primary': isPrimary }]">Кнопка</div>
<!-- Динамические стили -->
<div :style="{ color: textColor, fontSize: '16px' }">Текст</div>
<div :style="styleObject">Текст</div>
<div :style="[baseStyles, additionalStyles]">Текст</div>
<!-- Привязка всех attrs объекта сразу -->
<input v-bind="inputAttrs" />
<!-- Эквивалентно: <input :type="type" :placeholder="placeholder" :disabled="disabled" /> -->
<!-- Динамическое имя атрибута -->
<a :[linkAttrName]="linkAttrValue">Ссылка</a>
<!-- Если linkAttrName = 'href', linkAttrValue = '/about' → <a href="/about"> -->
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const imageUrl = ref('/images/avatar.jpg')
const isActive = ref(true)
const hasError = ref(false)
const styleObject = reactive({
color: '#42b883',
fontWeight: 'bold',
padding: '8px 16px'
})
const inputAttrs = reactive({
type: 'email',
placeholder: 'Введи email...',
disabled: false
})
</script>

<template>
<!-- Полный синтаксис -->
<button v-on:click="handleClick">Нажми</button>
<!-- Сокращение @ (рекомендуется) -->
<button @click="handleClick">Нажми</button>
<!-- Инлайн-выражение -->
<button @click="count++">{{ count }}</button>
<!-- Передача аргументов -->
<button @click="greet('Яша')">Поздороваться</button>
<!-- С доступом к событию ($event) -->
<input @input="handleInput($event)" />
<button @click="deleteItem(item.id, $event)">Удалить</button>
<!-- Несколько обработчиков -->
<button @click="handler1(), handler2()">Два обработчика</button>
<!-- Клавиатурные события -->
<input
@keydown.enter="submitForm"
@keydown.escape="cancelEdit"
@keydown.ctrl.z="undo"
/>
<!-- Мышь -->
<div @click.right="showContextMenu">Правый клик</div>
<div @click.middle="openNewTab">Средний клик</div>
<!-- Динамическое событие -->
<button @[eventName]="handler">Динамическое событие</button>
</template>
<template>
<!-- .stop — stopPropagation() -->
<div @click="outerClick">
<button @click.stop="innerClick">Клик не всплывает</button>
</div>
<!-- .prevent — preventDefault() -->
<form @submit.prevent="submitForm">
<button type="submit">Отправить</button>
</form>
<!-- .once — обработать только один раз -->
<button @click.once="doOnce">Только один раз</button>
<!-- .self — только если клик на самом элементе (не дочернем) -->
<div @click.self="closeModal">
<div class="modal-content">Контент</div>
</div>
<!-- .passive — улучшает производительность скролла -->
<div @scroll.passive="handleScroll">Скроллируемый контент</div>
<!-- Комбинирование модификаторов -->
<a @click.stop.prevent="handleLink">Ссылка</a>
</template>

<template>
<!-- Текстовый input — привязка к строке -->
<input v-model="name" />
<!-- Эквивалентно: <input :value="name" @input="name = $event.target.value" /> -->
<!-- Number input -->
<input v-model.number="age" type="number" />
<!-- Trim пробелов -->
<input v-model.trim="email" />
<!-- Обновление при blur (не при каждом вводе) -->
<input v-model.lazy="search" />
<!-- Textarea -->
<textarea v-model="description"></textarea>
<!-- Checkbox — boolean -->
<input v-model="isAccepted" type="checkbox" />
<!-- Checkbox — массив значений -->
<input v-model="selectedFruits" value="apple" type="checkbox" />
<input v-model="selectedFruits" value="banana" type="checkbox" />
<input v-model="selectedFruits" value="cherry" type="checkbox" />
<!-- Radio -->
<input v-model="gender" value="male" type="radio" />
<input v-model="gender" value="female" type="radio" />
<!-- Select -->
<select v-model="selectedCountry">
<option v-for="country in countries" :key="country.code" :value="country.code">
{{ country.name }}
</option>
</select>
<!-- Multiple select — массив -->
<select v-model="selectedTags" multiple>
<option v-for="tag in tags" :key="tag" :value="tag">{{ tag }}</option>
</select>
</template>

<template>
<!-- Передача контента в именованный слот -->
<BaseLayout>
<template #header>
<h1>Заголовок страницы</h1>
</template>
<template #default>
<p>Основной контент</p>
</template>
<template #footer>
<p>Подвал страницы</p>
</template>
</BaseLayout>
<!-- Scoped slot — получаем данные из дочернего компонента -->
<DataTable :items="users">
<template #row="{ item, index }">
<tr>
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.email }}</td>
</tr>
</template>
</DataTable>
</template>

<template>
<!-- v-pre — не компилировать, вывести как есть (для документации) -->
<span v-pre>{{ это не интерполяция }}</span>
<!-- Выведет: {{ это не интерполяция }} -->
<!-- v-once — отрендерить один раз, больше не обновлять -->
<h1 v-once>{{ appTitle }}</h1>
<!-- appTitle изменится, но h1 — нет -->
<!-- v-memo — мемоизация поддерева шаблона -->
<!-- Перерендер только если items[i].selected изменился -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<p>{{ item.name }}</p>
<p>{{ item.description }}</p>
<!-- Много DOM элементов — мемоизируем для скорости -->
</div>
</template>

<template>
<!-- ref — прямая ссылка на DOM элемент -->
<input ref="inputEl" placeholder="Фокус при монтировании" />
<!-- ref на компонент -->
<UserForm ref="formRef" />
<!-- Dynamic ref в v-for -->
<li v-for="item in items" :key="item.id" :ref="el => { if (el) itemRefs[item.id] = el }">
{{ item.name }}
</li>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// Типизация template ref
const inputEl = ref<HTMLInputElement | null>(null)
const formRef = ref<InstanceType<typeof UserForm> | null>(null)
const itemRefs = ref<Record<number, HTMLElement>>({})
onMounted(() => {
// DOM доступен в onMounted
inputEl.value?.focus()
// Доступ к expose методам компонента
formRef.value?.resetForm()
})
</script>

Составной шаблон: пример Todo приложения 📝

Заголовок раздела «Составной шаблон: пример Todo приложения 📝»
<template>
<div class="todo-app">
<!-- Форма добавления -->
<form @submit.prevent="addTodo" class="add-form">
<input
v-model.trim="newTodo"
placeholder="Новая задача..."
@keydown.enter="addTodo"
/>
<button type="submit" :disabled="!newTodo">Добавить</button>
</form>
<!-- Фильтры -->
<div class="filters">
<button
v-for="filter in filters"
:key="filter.value"
@click="currentFilter = filter.value"
:class="{ active: currentFilter === filter.value }"
>
{{ filter.label }}
</button>
</div>
<!-- Список задач -->
<template v-if="filteredTodos.length">
<TransitionGroup name="list" tag="ul">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ done: todo.done }"
>
<input
type="checkbox"
v-model="todo.done"
/>
<span>{{ todo.text }}</span>
<button @click.stop="removeTodo(todo.id)">✕</button>
</li>
</TransitionGroup>
</template>
<div v-else class="empty">
<p v-if="currentFilter === 'all'">Задач нет. Добавь первую! 🎉</p>
<p v-else>Нет задач в этой категории</p>
</div>
<!-- Статистика -->
<footer>
Всего: {{ todos.length }} |
Выполнено: {{ doneTodos.length }} |
Осталось: {{ activeTodos.length }}
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([])
const newTodo = ref('')
const currentFilter = ref<'all' | 'active' | 'done'>('all')
const filters = [
{ value: 'all', label: 'Все' },
{ value: 'active', label: 'Активные' },
{ value: 'done', label: 'Выполненные' }
]
const filteredTodos = computed(() => {
if (currentFilter.value === 'active') return todos.value.filter(t => !t.done)
if (currentFilter.value === 'done') return todos.value.filter(t => t.done)
return todos.value
})
const doneTodos = computed(() => todos.value.filter(t => t.done))
const activeTodos = computed(() => todos.value.filter(t => !t.done))
const addTodo = () => {
if (!newTodo.value) return
todos.value.push({ id: Date.now(), text: newTodo.value, done: false })
newTodo.value = ''
}
const removeTodo = (id: number) => {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>

Директивы Vue 3:

  • v-if/v-else-if/v-else — условный рендеринг (удаляет из DOM)
  • v-show — скрытие через display:none (остаётся в DOM)
  • v-for — итерация по спискам (всегда с :key!)
  • v-bind (:) — привязка атрибутов, классов, стилей
  • v-on (@) — обработка событий с модификаторами
  • v-model — двустороннее связывание данных
  • v-once/v-memo — оптимизация рендеринга
  • ref — ссылки на DOM элементы

Следующий урок — компоненты! 🧱