7. Шаблоны и директивы
Шаблоны и директивы Vue 3 📄
Заголовок раздела «Шаблоны и директивы Vue 3 📄»Шаблоны 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>v-if / v-else-if / v-else — условный рендеринг 🔀
Заголовок раздела «v-if / v-else-if / v-else — условный рендеринг 🔀»<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>v-for — списки 📋
Заголовок раздела «v-for — списки 📋»<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>v-bind — привязка атрибутов 🔗
Заголовок раздела «v-bind — привязка атрибутов 🔗»<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>v-on — обработка событий 🎯
Заголовок раздела «v-on — обработка событий 🎯»<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>v-model — двустороннее связывание 🔄
Заголовок раздела «v-model — двустороннее связывание 🔄»<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>v-slot — именованные слоты 🎰
Заголовок раздела «v-slot — именованные слоты 🎰»<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>v-pre, v-once, v-memo — оптимизация 🚀
Заголовок раздела «v-pre, v-once, v-memo — оптимизация 🚀»<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 refs — доступ к DOM 🎯
Заголовок раздела «Template refs — доступ к DOM 🎯»<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 refconst 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 элементы
Следующий урок — компоненты! 🧱