ADR-UI-009. Формы
Приложению нужны формы — ввод идентификаторов, поля значений, фильтры. Фиксируем один способ: формы на Signal Forms (signals-first), единое базовое поле с плавающей меткой, единый стиль валидации, показа ошибок и связки со значением.
1. Контекст и постановка задачи
Нужен один способ реализации форм, чтобы стиль валидации, показа ошибок и связки со значением был единым, а разработчик и ИИ-ассистент не выбирали подход в каждой задаче.
Фронт строится signals-first (ADR-UI-001), поэтому формы делаем на Signal Forms
(@angular/forms/signals) — значения и валидность и так сигналы, без моста к реактивным
потокам. Базовое поле и его поведение берём по единому визуальному образцу (плавающая метка).
2. Драйверы решения
- Signals-first — форма выражена сигналами; не заводим параллельный поток и подписки ради отображения.
- Единый стиль — одинаковые валидация, показ ошибок, submit во всех формах.
- Типизация — значения и валидаторы типизированы, без
any. - Кросс-полевая и асинхронная валидация — поддерживаются из коробки подхода.
3. Рассмотренные варианты
- A. Template-driven (
ngModel) — недостаточно для составных форм и сложной валидации. - B. Reactive Forms + мост в сигналы (
toSignal(valueChanges)) — стабильный API, но вводит двойной слой (реактивная форма + сигналы-отображение). - C. Signal Forms (
@angular/forms/signals) — signal-native, без моста; стабильный публичный API в актуальной версии Angular.
4. Решение
Выбран Вариант C: формы на Signal Forms. Значения, валидность и состояния — сигналы;
шаблон использует их напрямую (@if, [disabled]) без async-моста. Самодельных оболочек
поверх (createForm() и т. п.) не вводим — используем нативный API (form, field, Control,
validate, правила required/min/max/minLength/email/pattern, validateAsync,
submit).
4.1. Базовое поле — Field (плавающая метка)
Общий атом всех полей ввода: рамка-капсула (скругление, padding, фон/обводка по состоянию)
с плавающей меткой:
- пусто и не в фокусе — метка крупным плейсхолдером внутри контрола;
- в фокусе или есть значение — метка поднимается к верхней кромке, шрифт мельче, под ней — значение.
Состояния (различаются фоном/обводкой, геометрия одна): default, hover, focus
(кольцо), error, error-focus, disable, autofill.
Под контролом — под-блок description: сюда идут пояснение к полю (helper, опц.) и
сообщение об ошибке. То есть подпись-пояснение и ошибка — ниже контрола.
Слоты prefix / suffix; суффикс задаёт вариант поля: Input, Select (▾),
Date (календарь) — всё на одной рамке. Поле — base-компонент набора ui/.
4.2. Раскладка форм
- Дефолт — поля с плавающей меткой, в столбец. Отдельные представления (метка слева/сверху) пока не вводим — по необходимости (открытый вопрос).
- Интервалы — только токенами (поле↔поле, поле↔футер), без «магических» px (ADR-UI-004).
- Футер кнопок — сразу под полями формы, не приклеен к низу; порядок/типы кнопок — по ADR о кнопках.
4.3. Валидация
- Стандартные правила (
required/min/max/minLength/email/pattern) + кастомные валидаторы-функции вsrc/app/validators/(имена глагольные, чистые функции с явным контрактом ошибок). - Кросс-полевые — на уровне формы; асинхронные — через per-page service
(
validateAsync/validateHttp), API в валидатор напрямую не тащим. - Сообщение об ошибке — рядом с полем, в его
description; маппинг кодов ошибок в русский текст — в компоненте поля / общем помощнике.
4.4. Submit
- показать ошибки (пометить поля «тронутыми» / submitted);
- если форма невалидна — выйти (без «тихого» отказа);
- вызвать метод per-page service со значением формы (или после маппинга в API-модель).
На время отправки — сигнал submitting (блокировка формы), по завершении — снять.
4.5. Что не используем
- Reactive Forms и Template-driven (
ngModel) — кроме тривиального одиночного контрола вне формы (например, строка поиска над списком). - Самодельные оболочки поверх форм-API.
5. Положительные следствия
- Форма — сигналы; шаблон работает с ними напрямую, без
async-моста и лишних подписок. - Единое базовое поле (плавающая метка + состояния + описание) — одинаковый вид и поведение во всех формах.
- Ошибки и пояснения — предсказуемо под полем.
6. Отрицательные следствия и компромиссы
- Базовое поле с плавающей меткой и всеми состояниями — заметный объём разовой работы
(следствие собственного набора
ui/, а не самих форм). - Signal Forms — молодой API: для нестандартных сценариев (динамические/вложенные формы, сложная кросс-полевая валидация) готовых примеров меньше, чем у зрелого Reactive, — иногда сверяемся с документацией, а не копируем готовый рецепт.
7. Проверка
- Все формы используют Signal Forms;
ngModel— только для одиночных контролов вне формы. - Тип значения формы выводится без
any. - При отправке невалидной формы пользователь видит ошибки под полями — нет «тихого» отказа.
- Интервалы полей/футера — из токенов (нет «магических» px).
8. Открытые вопросы / отложено
- Альтернативные представления форм (метка слева/сверху) — вводим по необходимости.
- Помощник serverErrors (привязка ошибок API к полям после submit) — появится с первой формой, обращающейся к API.