Перейти к содержанию

Свойства и события карточки сотрудника

Оглавление серии «Vue 3: кабинет сотрудников «Команда»»

Строка сотрудника теперь содержит имя, роль и действие. Когда кабинет получит несколько экранов, такую карточку понадобится использовать повторно. Выделим MemberCard, сохранив коллекцию у родителя. Результат урока — движение данных вниз через props и движение намерения вверх через событие. Компоненту карточки не нужно знать, где хранится команда и каким образом позднее сервер подтвердит изменение.

Новый компонент получает весь объект участника, но использует только поля для отображения. Выбор целого объекта делает вызов коротким и подходит нашей небольшой модели. В крупном интерфейсе можно передавать отдельные поля, чтобы уменьшить зависимость карточки от структуры записи. В любом варианте договор должен оставаться явным: родитель владеет данными, карточка сообщает о нажатии.

Свойство и событие

Создадим полный файл src/components/MemberCard.vue. defineProps и defineEmits являются макросами script setup; их не импортируют из vue. Проверка типа Object не описывает все вложенные поля, поэтому она не заменяет проверку ответа API. На этом шаге объект происходит из известного учебного массива.

<script setup>
defineProps({ member: { type: Object, required: true } })
const emit = defineEmits(['toggle'])
</script>

<template>
  <article>
    <h2>{{ member.name }}</h2>
    <p>{{ member.role }}</p>
    <p>{{ member.available ? 'Доступен' : 'Занят' }}</p>
    <button type="button" @click="emit('toggle', member.id)">
      Изменить доступность {{ member.name }}
    </button>
  </article>
</template>

Событие toggle передаёт id. Оно не передаёт новое значение available, поскольку переход пока определяется родителем. Если операция позже станет «назначить конкретную доступность», договор события следует изменить и назвать соответствующим образом. Смысл аргумента должен быть понятен без чтения внутренностей карточки. Документация событий компонентов описывает объявление и обработку таких сообщений.

Полностью заменим src/App.vue. Для ясности временное несохраняемое поле заметки убрано, остальные условия поиска и сортировки продолжают действовать.

<script setup>
import { computed, ref } from 'vue'
import { seedMembers } from './data/members.js'
import MemberCard from './components/MemberCard.vue'
const members = ref(seedMembers.map(member => ({ ...member })))
const query = ref('')
const onlyAvailable = ref(false)
const descending = ref(false)
const visibleMembers = computed(() => members.value.filter(member =>
  `${member.name} ${member.role}`.toLocaleLowerCase('ru')
    .includes(query.value.trim().toLocaleLowerCase('ru'))
  && (!onlyAvailable.value || member.available)
).sort((a, b) => (descending.value ? -1 : 1) *
  (a.name.localeCompare(b.name, 'ru') || a.id.localeCompare(b.id))))
function toggle(id) {
  const member = members.value.find(item => item.id === id)
  if (member) member.available = !member.available
}
</script>
<template>
  <main>
    <h1>Команда</h1>
    <label>Поиск <input v-model="query" type="search"></label>
    <label><input v-model="onlyAvailable" type="checkbox"> Только доступные</label>
    <label><input v-model="descending" type="checkbox"> Обратный порядок</label>
    <p>Показано {{ visibleMembers.length }} из {{ members.length }}</p>
    <p v-if="!visibleMembers.length">Подходящих участников нет.</p>
    <MemberCard v-for="member in visibleMembers" :key="member.id"
      :member="member" @toggle="toggle" />
  </main>
</template>

Ожидаемый результат внешне похож на прежний список, но каждый участник становится отдельной карточкой. Нажатие всё ещё меняет только выбранного человека. Путь операции читается последовательно: событие кнопки, emit с id, обработчик App, изменение members, обновлённое свойство карточки. Такое движение помогает найти место ошибки: если id верен, но значение не меняется, следует смотреть обработчик и общий источник.

Почему карточка не меняет props

Props имеют односторонний поток. Родитель обновляет свойство при изменении данных, дочерний компонент читает его. Руководство Vue о props объясняет эту границу. Для объекта остаётся важная тонкость: вложенное поле технически может измениться через общую ссылку. Это не превращает такое присваивание в хороший договор нашего приложения.

Если написать в карточке member.available = !member.available, родительская коллекция изменится без вызова toggle. Пока результат может выглядеть правильно. Но позднее родитель добавит право действия или запрос на сервер, и карточка обойдёт эту логику. Ошибочный случай связан с архитектурой, а не с обязательным немедленным исключением. Поэтому не стоит проверять правильность решения только по тому, изменился ли текст на экране.

Наш компонент сообщает именно намерение. Родитель может принять его, отклонить, показать ожидание или передать действие другому сервису. Благодаря этому карточка не должна изменяться при переходе с локального массива на HTTP. Сам event не всплывает автоматически через любую глубину дерева так же, как браузерный click. Если появится промежуточный компонент, он должен явно обработать или передать сообщение дальше либо получить другой механизм взаимодействия.

При вызове :member="member" имя слева является названием свойства, а выражение справа читает текущий объект цикла. Без двоеточия карточка получила бы строку member. У импорта MemberCard есть ещё один эффект script setup: он становится доступен как тег шаблона. Регистрационный объект components не нужен. Это удобство относится к выбранному способу объявления компонента; смешивать его с примерами Options API без объяснения не следует.

Граница оформления и ответственности

Мы используем article и h2, потому что карточка представляет самостоятельную запись внутри страницы с h1. При появлении вложенных разделов уровень заголовка может потребовать настройки. Не называйте компонент универсальным для любого контекста, пока его разметка рассчитана на конкретный экран. Повторное использование полезно тогда, когда сохраняется смысл, а не только внешнее сходство блока.

Выделение карточки также не означает, что каждый span следует превратить в компонент. Отдельная единица оправдана наличием устойчивого договора: данные участника и действие доступности. Количество строк само по себе не задаёт такую границу. Если начать передавать десятки флагов для совсем разных вариантов использования, можно потерять ясность исходной задачи. В нашем кабинете следующей самостоятельной единицей станет форма, потому что у неё собственное состояние черновика и подтверждение.

Для мысленной проверки замените обработчик toggle у родителя на функцию, которая только получает id, без изменения коллекции. Карточка должна остаться с прежним состоянием: событие само не мутирует данные. Затем верните обработчик и проследите обновление props. Такое рассуждение помогает отличить сообщение о намерении от готового результата. В следующем уроке форма будет передавать проверенные поля нового участника, а родитель решит, как добавить запись в команду.

Уточнение договора карточки

Предположим, что руководитель просит скрыть кнопку у занятых сотрудников. Такое условие можно вычислить из member.available, но оно меняет допустимые намерения интерфейса и требует предметного объяснения. Лучше спросить себя, действительно ли занятый человек не может стать доступным: в нашем договоре как раз может. Значит внешне удобное условие сломало бы полезное действие. Сначала определяют правило, затем связывают его с состоянием кнопки.

Событие toggle не является браузерным событием click. Его полезная нагрузка — строковый идентификатор, а не MouseEvent. Если родитель начнёт читать event.target, он перепутает две границы. Для клавиатуры пользовательский результат останется тем же, потому что native button инициирует действие привычным способом. Не нужно заменять кнопку на div с обработчиком только ради переноса кода в компонент.

Карточку можно разместить дважды с одной записью, например в списке и отдельной панели избранного. При общем родительском источнике обе должны показать новое available после одной операции. Если один экземпляр копирует props в собственный ref для простого отображения, он может перестать следовать обновлению. Локальная копия оправдана для черновика, но для обычной подписи она создаёт лишний источник состояния.

Для самостоятельного воспроизведения результата «Разделить родительское состояние и намерение дочернего компонента» выберите lesson-05 в архиве исходников. Статус выполнения этого состояния: not_run.