ADR-UI-002. Префикс, именование и конвенции
Чтобы исключить коллизии селекторов с другими Angular-приложениями и обеспечить предсказуемый стиль во всём проекте, нужен свой префикс и единый набор базовых конвенций. Правила опираются на актуальный стиль Angular 22 и учитывают переносимость собственного набора компонентов.
1. Контекст и постановка задачи
Нужен свой префикс (чтобы селекторы и шаблонные идентификаторы не сталкивались с другими Angular-приложениями и заимствованным кодом) и единый набор конвенций — чтобы стиль был предсказуем, а будущая автоматизация (skills/codegen) опиралась на стабильные правила.
Учитываем переносимость: собственный набор компонентов (ui/) должен выноситься в другой
проект без переписывания. Отсюда — префикс держим там, где он реально нужен (DOM-селекторы), а
имена TypeScript-классов оставляем нейтральными.
2. Драйверы решения
- Отсутствие коллизий селекторов и шаблонных идентификаторов с другими приложениями.
- Единообразие ревью — стиль предсказуем, мелочи не обсуждаются в каждом PR.
- Опора на нативные средства Angular 22 — signals, native control flow,
hostв декораторе, OnPush по умолчанию. - Переносимость набора — вынос
ui/в другой проект меняет минимум (префикс селекторов), не трогая имена классов. - Автоматизация — генераторы компонентов/сервисов опираются на явные правила.
3. Рассмотренные варианты
- A. Без префикса — короче, но провоцирует коллизии селекторов с библиотеками и заимствованным кодом.
- B. Префикс
icdв селекторах, классы без префикса — короткое имя проекта как DOM-идентификатор; TypeScript-классы нейтральны (изоляция — модульной системой TS). - C. Префикс и в классах (
IcdButton…) — избыточно для переносимого набора: при выносе пришлось бы переименовывать все классы.
4. Решение
Выбран Вариант B. Префикс icd — только в селекторах компонентов/директив и именах пайпов
(шаблонные идентификаторы). Имена TypeScript-классов — чистый PascalCase без префикса и без
суффикса (стиль Angular 22). При выносе ui/ в библиотеку меняется только префикс селекторов
(в eslint.config.js и шаблонах) — классы переносятся нетронутыми.
4.1. Именование (стиль Angular 22 — без суффиксов)
| Сущность | Класс | Селектор / имя | Файл |
|---|---|---|---|
| Компонент | Button |
icd-button (element, kebab-case) |
button.ts (+ .html, .scss) |
| Директива | Highlight |
[icdHighlight] (attribute, camelCase) |
highlight.ts |
| Pipe | DateYmd |
icdDateYmd (в шаблоне) |
date-ymd.ts |
| Сервис | AuthStore |
— | auth-store.ts |
| Реестр маршрутов | BaseRoutes |
— | <feature>.routes.ts |
| Папка фичи | — | — | modules/<feature-kebab>/ |
Суффиксы Component / Directive / Pipe / Service не используются (актуальный стиль
Angular). Файлы именуются по сущности (button.ts, не button.component.ts). Если имя сервиса
коллизирует с типом/моделью — различаем смысловым суффиксом роли (Store, Api, Client),
а не техническим Service.
4.2. Разрешение коллизий имён (UI ↔ API)
Имена UI-классов не префиксуются, поэтому теоретически возможно совпадение с типом api-модели
(например, UI-контейнер Card и модель Card). На практике риск низкий: набор ui/ — это
generic-виджеты (Button, Table, Input), а api-модели ICD — доменные (InformationCard,
InputPacket, FundItem), пересечений почти нет. Разрешение:
- По месту — алиас импорта:
import { Card as CardModel } from '@api/…'(стандартный механизм модулей TypeScript). - Превентивно — генерируемые api-модели несут суффикс (
Dto/Model) либо импортируются через namespace (import * as Api from '@api/…'); тогдаCardDto/Api.Cardне конфликтуют с UI-классами в принципе.
Префикс на имена UI-классов не вводим (переносимость набора важнее); коллизии закрываются на стороне api-моделей. Детали контракта api-моделей — в отдельном ADR.
4.3. Базовые конвенции компонентов
- Standalone (default в Angular 22 —
standalone: trueне указываем). - Change detection — OnPush по умолчанию с Angular 22, явно указывать не нужно; проект
zoneless.
Defaultуказываем только там, где он осознанно требуется. inject()вместо constructor-injection.input()/output()функции вместо декораторов@Input/@Output.computed()для производного состояния.host-объект в декораторе вместо@HostBinding/@HostListener.- Native control flow
@if/@for/@switch; не*ngIf/*ngFor/*ngSwitch. class/stylebindings вместоngClass/ngStyle.NgOptimizedImageдля статических изображений.- Внешние шаблоны/стили — путь относительно
.ts-файла компонента.
4.4. Структура класса
Порядок членов класса — строгий, двухуровневый:
- По роли (внешняя ось): поля → конструктор → методы. Все поля — вверху класса, до конструктора; методы — после него.
- По области видимости (внутренняя ось, внутри полей и внутри методов):
public→protected→private.
Итоговая раскладка сверху вниз:
| № | Группа |
|---|---|
| 1 | public-поля |
| 2 | protected-поля |
| 3 | private-поля |
| 4 | конструктор |
| 5 | public-методы |
| 6 | protected-методы |
| 7 | private-методы |
Уточнения: static-члены идут перед экземплярными в своей группе; геттеры/сеттеры трактуются как
методы соответствующей видимости. Для Angular-компонентов это ложится естественно: input() /
output() (public-поля) → производное состояние computed() (protected-поля) → внедрённые
зависимости inject() (private-поля) → методы.
4.5. Комментарии
- JSDoc (
/** … */) к методам с непустым именем намерения. - Не комментировать тривиальные геттеры/сеттеры и «что делает код» — комментарий нужен, когда не очевидно почему.
4.6. TypeScript
strict: true; предпочитать вывод типов, когда он очевиден.- Избегать
any; при неопределённости —unknown. - Не кастовать
as Type/<Type>, если тип выводится; каст<Type>{ … }— только для осознанно частичного заполнения.
5. Положительные следствия
- Префикс в селекторах исключает коллизии в DOM; классы остаются переносимыми без переименования.
- Стиль предсказуем — ревью о сути, а не о форматировании; генераторы опираются на явные правила.
- Опора на нативный Angular 22 (OnPush по умолчанию, signals, control flow) — меньше кода и шаблонного шума.
6. Отрицательные следствия и компромиссы
- Префикс
icd-в каждом селекторе — небольшой визуальный шум в шаблонах (узнаваемость важнее). - При выносе набора в библиотеку префикс селекторов всё же надо заменить — механическая работа, но локализованная (конфиг линтера + шаблоны).
- Отказ от суффиксов требует дисциплины при коллизиях имён сервис/модель (решается ролью-суффиксом).
7. Проверка
- Линтер
@angular-eslintнастроен (eslint.config.js): правилаcomponent-selectorиdirective-selectorтребуют префиксicd.ng lintпроходит на текущем каркасе. - Любой новый компонент / директива / pipe соответствует таблице именования; спорные мелочи (порядок членов класса) не обсуждаются — есть однозначное правило.