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

Reactive forms и независимый черновик

На странице rq-101 диспетчер должен изменить заголовок и статус, а затем решить, сохранять ли изменения. Пока он вводит текст, исходная заявка остаётся основой. Создадим отдельный черновик с reactive forms: FormGroup объединит поля, FormControl задаст значение каждого поля, а шаблон свяжется с моделью формы. Результат — редактирование без немедленной мутации полученного Ticket и с явной операцией принятия новой основы.

Такое разделение решает практическую задачу отмены и отказа. Если пользователь написал «Заменить проектор в зале 2», а сервер затем отклонил запись, старый Ticket всё ещё описывает подтверждённые данные. Черновик содержит намерение человека и может быть исправлен или отправлен после выбора новой основы. Если менять сам Ticket при каждом символе, интерфейс потеряет эту разницу. Тогда надпись «сохранено» легко появится до того, как API вообще получил запрос.

Модель формы

Фрагмент ниже — поле form из конечного DetailPage. Для учебного этапа import включает FormControl, FormGroup и Validators, а Component imports содержит ReactiveFormsModule. Status берётся из model.ts. После получения Ticket выполняется setValue с title и status. Это полный набор полей группы: отсутствие одного поля при setValue будет ошибкой структуры. Для частичной операции существует patchValue, но здесь полная загрузка основы лучше выражает намерение.

readonly form = new FormGroup({
  title: new FormControl('', {
    nonNullable:true,
    validators:[Validators.required, Validators.minLength(3), Validators.maxLength(120)]
  }),
  status: new FormControl<Status>('new', {nonNullable:true})
});

NonNullable выбран явно. Без него reset может приводить к null в значении поля, и код обработки должен учитывать дополнительное состояние. Для заголовка лаборатории пустая строка обозначает пустой черновик; null самостоятельного предметного значения не имеет. Для status допустимы только три машинных значения. Типизация помогает корректно собрать будущий TicketPatch, но не заменяет проверку внешних значений в браузере и DTO на сервере. Уровни доверия останутся отдельными.

В шаблоне форма связывается через [formGroup]="form", а элементы получают formControlName title и status. Смена текста ожидаемо меняет значение соответствующего control. Она не вызывает PATCH сама по себе: HTTP-операция расположена в обработчике ngSubmit. Обычный button внутри form без type по умолчанию является submit, что используется для кнопки сохранения. Дополнительные действия следует размещать с явным type button или за пределами формы, чтобы случайный щелчок не отправлял черновик.

Основа и изменённые поля

После загрузки rq-101 ожидаются title «Заменить проектор» и status new. Метод setValue переносит данные в форму, а markAsPristine обозначает, что новые поля считаются текущей основой редактора. Pristine не означает, что сервер гарантированно доступен или что форма валидна при любых условиях. Это состояние взаимодействия: пользователь ещё не изменял принятую основу. Validity определяется правилами полей, а подтверждение записи — успешным ответом API.

Ticket.version хранится отдельно в ticket, а не в редактируемом input. Пользователь не должен выбирать номер версии как обычное поле содержания. При save форма отдаст title и status, а version будет добавлен из основы. Именно этот номер сообщает серверу, по какой записи пользователь начал изменение. Если новая загрузка автоматически заменит основу, сохранив старый текст, отправленное значение version будет выглядеть актуальным, хотя черновик основан на другом содержании. Поэтому правила reload должны быть явными.

Кнопка «Загрузить основу, заменив черновик» честно описывает последствия. В конечной лаборатории она снова читает Ticket и устанавливает оба поля. Это простая политика, которую легко понять: прежний введённый текст будет заменён. Для настоящего кабинета можно добавить отдельное сравнение изменений или подтверждение при dirty-форме. Но такой интерфейс надо разработать и проверить отдельно; наличие флага dirty само не создаёт безопасную стратегию объединения двух версий.

Получение значения для записи

GetRawValue возвращает значения всех controls, включая disabled. Метод value у FormGroup может иметь частичную форму, поскольку отключённые элементы исключаются. В лаборатории title использует readOnly для наблюдателя, а status остаётся доступным локальному черновику; кнопка и save ограничены учебной ролью. Мы не отключаем всю группу только ради получения компактного UI. Эта политика позволяет разбирать форму независимо от серверной авторизации и не выдаёт локальный черновик за разрешённую запись.

Перед сохранением title обрезается по краям, затем все поля помечаются touched и проверяется invalid. Такой порядок делает окончательную проверку ближе к договору API: строка из пробелов не проходит длину после trim. Во время ввода встроенный minLength работает с текущей строкой, поэтому « а » до отправки может вести себя иначе. На следующем шаге подробно обсудим сообщение об этой границе. Сервер дополнительно нормализует и проверит значение, независимо от браузера.

Не связывайте машинный status с русским текстом option. Атрибут value содержит new, active или done, а видимое содержимое option — «Новая», «В работе», «Завершена». Тогда getRawValue отдаёт договорные значения, которые понимает Nest DTO. Если вместо active отправить «В работе», сервер правильно отклонит запрос, даже если браузер выглядел убедительно. Формы соединяют представление и модель, поэтому каждая такая граница должна иметь определённое значение.

Разберите ожидаемый цикл без HTTP: получили основу rq-101, ввели другой title, прочитали getRawValue, затем снова вызвали setValue исходными полями. В первом состоянии Ticket и форма совпадают, во втором расходятся, в третьем совпадают снова. Это не ошибка синхронизации, а намеренно независимый черновик. Если полученная заявка обновилась в фоновом месте, редактор должен отдельно решить, принимать ли её. Конечный пример не делает автоматического фонового merge.

Reactive forms также предоставляют единое место для просмотра ошибок, touched и dirty. Не нужно искать текст в DOM перед сохранением или восстанавливать выбор select из CSS-класса. Шаблон отображает модель формы, а обработчик работает с её значениями. Далее мы добавим проверку и доступные сообщения, затем проведём через HttpClient ту же операцию. Ни маршруты, ни исходные идентификаторы, ни правило отдельной version-основы для этого менять не потребуется.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

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