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

Форма добавления участника и проверка полей

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

Добавим в «Команду» форму нового участника. В отличие от карточки, которая только показывает запись, форма должна временно хранить введённое имя и роль. Пока пользователь исправляет текст, коллекция сотрудников не меняется. Результат урока — отделённый черновик, предметная проверка полей и событие create, после которого родитель добавляет новую локальную запись.

Мы не создаём аккаунт и не назначаем права доступа. Добавленный человек существует только в учебном массиве. Это важная граница названия действия: интерфейс показывает «Добавить участника», а не «Пригласить по почте». Настоящее приглашение потребовало бы сервера, проверки прав и отдельного состояния доставки. Здесь форма учит согласованию ввода и модели, без выдуманного результата внешней операции.

Черновик принадлежит форме

Создадим полный src/components/MemberForm.vue. Отдельные ref name и role живут столько же, сколько экземпляр формы. После удачного события они очищаются. Ошибка остаётся текстом, который объясняет требуемое исправление, а не превращает некорректный ввод в случайно созданного сотрудника.

<script setup>
import { ref } from 'vue'
const emit = defineEmits(['create'])
const name = ref('')
const role = ref('')
const error = ref('')
function submit() {
  const draft = { name: name.value.trim(), role: role.value.trim() }
  if (!draft.name || !draft.role || draft.name.length > 80 || draft.role.length > 80) {
    error.value = 'Введите имя и роль длиной от 1 до 80 символов.'
    return
  }
  emit('create', draft)
  name.value = ''
  role.value = ''
  error.value = ''
}
</script>
<template>
  <form @submit.prevent="submit">
    <h2>Новый участник</h2>
    <label>Имя <input v-model="name" maxlength="80" required></label>
    <label>Роль <input v-model="role" maxlength="80" required></label>
    <p v-if="error" role="alert">{{ error }}</p>
    <button type="submit">Добавить участника</button>
  </form>
</template>

Директива .prevent отменяет стандартную отправку браузером, позволяя обработать форму в памяти. Нажатие Enter также проходит через submit, если браузер инициирует отправку формы. Поэтому проверка размещена у события формы, а не только у click кнопки. Документация обработки событий Vue объясняет модификаторы; предметное решение здесь состоит в едином пути подтверждения.

Для подключения формы заменим App.vue целиком. Поиск и карточки продолжают использовать прежние имена.

<script setup>
import { computed, ref } from 'vue'
import { seedMembers } from './data/members.js'
import MemberCard from './components/MemberCard.vue'
import MemberForm from './components/MemberForm.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')))
function toggle(id) {
  const member = members.value.find(item => item.id === id)
  if (member) member.available = !member.available
}
function create(draft) {
  members.value.push({ id: crypto.randomUUID(), ...draft, available: true, version: 1 })
}
</script>
<template>
  <main>
    <h1>Команда</h1>
    <MemberForm @create="create" />
    <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>

Родитель добавляет id, доступность и версию. Форма не может произвольно передать эти значения, поскольку событие содержит только name и role. В нашем локальном окружении crypto.randomUUID создаёт идентификатор, который не зависит от номера строки. При самостоятельном запуске нужен поддерживаемый безопасный контекст, например localhost. Для реального API окончательный id должен приходить от сервера; локальный идентификатор не доказывает существование аккаунта.

Две проверки одного ввода

Атрибуты required и maxlength дают браузеру сведения об ограничениях. Но JavaScript также проверяет нормализованные значения. Строка из одних пробелов может выглядеть непустой для браузера, а после trim оказаться пустой. Именно поэтому проверка повторяет смысл правила, а не просто наличие атрибута. Клиентские ограничения помогают пользователю, однако не являются защитой сервера: при переходе к HTTP сервер обязан проверить собственный запрос.

Используемый length считает кодовые единицы JavaScript. Это достаточно для выбранного учебного правила, но не обещает точное количество видимых графем для любого имени. Сложные символы могут занимать несколько единиц. Если продукт требует ограничение по видимым символам, правило следует определить иначе и согласовать с сервером. Нельзя молча назвать текущую проверку универсальным измерителем любого человеческого имени.

При событии отправляется новый объект draft, а не сам ref формы. Родитель получает обычные данные и не зависит от будущей очистки полей. Если передать один постоянно меняемый объект и затем очистить его поля, можно неожиданно очистить и добавленную запись через общую ссылку. Явное формирование полезно именно своей узкой границей: имя и роль фиксируются в момент подтверждения, а дальнейший ввод становится следующим черновиком.

Ожидаемый результат для « Максим » и «Разработчик» — новая запись с именем «Максим», доступностью true и новой личностью строки. Если включён поиск «Анна», добавленный Максим не обязан сразу оказаться видимым. Счётчик всей команды вырастет, но отфильтрованный список сохранит условие. Это не потеря записи. После очистки поиска сотрудник появится; сообщение о результате в будущем должно учитывать действующие фильтры.

Ошибка без изменения команды

Полезный неправильный случай — роль, состоящая только из пробелов. После trim она пуста, ошибка появляется, событие create не отправляется, members не меняется. Введённое имя остаётся, чтобы человек не повторял уже выполненную работу. Если очищать все поля до проверки, пользователю было бы труднее исправить один неверный фрагмент. Ошибка формы относится к текущему черновику и не должна стирать успешные записи команды.

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

Следите за именем события и названием обработчика: @create="create" связывает определённый контракт, а не автоматически обнаруживает любую функцию рядом. Для самостоятельного разбора запишите состояние name, role, error и members перед неверной отправкой и после неё. Затем проделайте ту же последовательность для правильного имени. Различие покажет границу между вводом, проверенным намерением и новой записью. Следующий шаг оформит форму и список общей панелью через слоты.

Созданная запись и повторная отправка

После успешного добавления поля пусты, но это не запрет одинаковых имён. Второй участник может иметь то же имя и роль, получив другой id. Если продукт требует защиту от дублей, следует определить, какое поле действительно уникально. Запрет одинакового имени был бы ошибкой для реальной команды. Адрес почты также потребовал бы отдельного поля, проверки и серверного ограничения. Текущий результат не скрывает эти требования под случайным условием массива.

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

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

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