Создание переиспользуемых интерфейсных библиотек регулярно сталкивается с дилеммой: как спроектировать компонент, одинаково легко отображающий короткий текст, разметку со ссылкой, шаблон и интерактивный блок? В типовом сценарии разработки модального окна, всплывающей подсказки (tooltip), ячейки таблицы или уведомления автор закладывает строковый параметр сообщения. Однако потребителям неизбежно требуется встроить иконку, оформить кнопку или вывести форму.
В Angular эта задача решается несколькими независимыми механизмами с несовместимым API. Разработчикам приходится либо плодить специализированные свойства, либо писать громоздкие шаблонные развилки. Библиотека Polymorpheus (@taiga-ui/polymorpheus), созданная архитекторами дизайн-системы Taiga UI Романом Седовым и Александром Инкиным, объединяет примитивы, функции, декларативные шаблоны и динамические компоненты в полиморфный слой с единым контрактом.
Четыре способа рендеринга и границы каждого инструмента
Чтобы понять предпосылки появления полиморфного рендеринга, сопоставим четыре способа вывода данных в Angular:
- Интерполяция строк. Конструкция
{{ text }}преобразует значение в строку черезtoString()и вставляет ее в текстовый узел документа (DOM). Механизм надежен для простых подписей, но исключает HTML-разметку, стилизацию отдельных слов и обработчики событий. - Вызов функций в шаблоне. Программист передает данные в чистую функцию, возвращающую вычисленный текст или число. Если функция не создает новые ссылки на массивы или объекты и не выполняет тяжелых расчетов, а компонент использует стратегию
ChangeDetectionStrategy.OnPush, вызовы остаются предсказуемыми. - Шаблоны
ng-templateи ссылкиTemplateRef. Механизм позволяет описывать произвольные иерархии элементов, а для динамической вставки служит директиваngTemplateOutlet. Сложность заключается в передаче контекста: соглашение Angular использует ключ$implicitдля значения по умолчанию (синтаксис<ng-template let-item>) и именованные поля для остальных параметров. При строгой проверке типов (strictTemplates) ручная типизация контекстов требует вспомогательных директив. - Динамические компоненты через
ngComponentOutlet. Подход незаменим, когда блоку нужен изолированный жизненный цикл, состояние и доступ к внедрению зависимостей (Dependency Injection, DI). Обратная сторона — многословность: для передачи контекста приходится конструировать дочерний инжектор (Injector).
Анатомия архитектурного тупика в библиотеках компонентов
В компонентах общего назначения — меню, диалогах или таблицах — отсутствие связи между четырьмя подходами порождает тупик. Рассмотрим эволюцию ячейки таблицы:
- Сначала ячейка принимает текст:
@Input() content: string. - Позже требуется форматирование чисел:
@Input() content: string | ((data: Row) => string). - Затем дизайнеры добавляют иконку с тултипом, требуя
TemplateRef<RowContext>. В шаблоне появляется развилка на проверках типов через@ifи@else. - Наконец, требуется интерактивный редактор. Свойство должно принимать классы компонентов (
Type<any>), требуя создания кастомного инжектора и токенов DI.
В итоге компонент обрастает десятками строк обслуживающего кода, не связанного с его предметной задачей, а любая новая точка кастомизации требует дублирования этой логики.
Концепция Polymorpheus: полиморфизм на уровне представления
Polymorpheus решает проблему через единую абстракцию содержимого. Компонент объявляет единственный входной параметр типа PolymorpheusContent<C>, где C описывает интерфейс контекста.
Тип PolymorpheusContent<C> объединяет четыре сущности: примитивные типы (string | number), чистые функции преобразования вида (context: C) => string | number, ссылки TemplateRef<C> и динамические компоненты, обернутые в класс PolymorpheusComponent.
Центральным звеном выступает структурная директива [polymorpheusOutlet]:
<ng-container *polymorpheusOutlet="content as primitive; context: itemContext">
{{ primitive }}
</ng-container>
Директива анализирует аргумент: примитив рендерится в теге по умолчанию, для TemplateRef создается встроенное представление (EmbeddedViewRef) с привязкой к context, для функции вычисляется результат, а для PolymorpheusComponent разворачивается динамический компонент через ViewContainerRef. Обертка позволяет директиве отличить класс-конструктор компонента от функции представления и служит контейнером для пользовательского инжектора (Injector).
Управление контекстом и строгая типизация
При использовании TemplateRef компилятор в режиме strictTemplates не может автоматически вывести тип переменной $implicit. Чтобы устранить проблему, Polymorpheus предоставляет директиву PolymorpheusTemplate, декларирующую ожидаемый тип контекста:
<ng-template polymorpheus [polymorpheusType]="userType" let-user>
<span>{{ user.name }}</span>
</ng-template>
Внутри шаблона переменная user получает точный интерфейсный тип User вместо any, гарантируя строгий контроль компилятора.
Для динамических компонентов передача данных поддержана двумя путями: через внедрение зависимостей (компонент внедряет токен POLYMORPHEUS_CONTEXT или вызывает функцию injectContext<T>()) либо через стандартные свойства @Input, если их имена совпадают с ключами контекста. При обновлении полей контекста директива запускает проверку представлений без разрушения DOM-дерева компонента.
Сравнение механизмов рендеринга данных в Angular
Сопоставим встроенные инструменты вывода данных и полиморфный подход:
| Механизм | Входные данные | Контекст | Нагрузка на change detection | Гибкость в библиотеках |
|---|---|---|---|---|
| Интерполяция строк | string, number | Нет | Минимальная, прямое обновление узла DOM | Крайне низкая, исключает интерактивность |
| Функция в шаблоне | Чистые функции | Аргумент вызова | Зависит от функции и стратегии OnPush | Средняя, оптимальна для форматирования |
| Шаблоны ng-template | TemplateRef | $implicit и поля | Стандартная для встроенных представлений | Высокая, но требует шаблонной обвязки |
| Динамические компоненты | Type<any> | Кастомный инжектор | Полный жизненный цикл компонента и DI | Максимальная, но требует многословного кода |
| Подход Polymorpheus | Единый PolymorpheusContent | Контекст C | Оптимальная, изолирована в директиве | Максимальная при едином API для всех сценариев |
Чеклист проектирования гибких интерфейсных элементов
Внедрение полиморфного рендеринга требует соблюдения архитектурных правил:
- Проектирование интерфейса контекста: формируйте интерфейс контекста
C, выделяя ключевую сущность в поле$implicitдля лаконичного синтаксиса шаблонов, а второстепенные метаданные передавайте в именованных полях. - Отказ от избыточной абстракции: применяйте полиморфный вход там, где действительно нужна вариативность отображения (модальные окна, тосты, сложные ячейки таблиц). Для статических надписей стандартного
@Input() label: stringдостаточно. - Соблюдение чистоты функций: запрещайте мутацию данных, асинхронные вызовы и создание новых объектов внутри функций представления, чтобы исключить лишние циклы рендеринга.
- Изоляция динамических зависимостей: передавая компонент через
new PolymorpheusComponent(MyComponent, customInjector), формируйте дочерний инжектор осознанно, предотвращая утечки глобального состояния. - Сквозная верификация стратегии OnPush: проверяйте, что компоненты-потребители и динамические шаблоны функционируют под управлением
ChangeDetectionStrategy.OnPush.
Зрелость решения и границы применимости
Библиотека Polymorpheus проверена промышленной эксплуатацией в дизайн-системе Taiga UI. Версия 5.0.1 распространяется под лицензией Apache-2.0 и не содержит внешних зависимостей, кроме пакетов @angular/core и @angular/common.
Архитекторам важно учитывать границы инструмента: Polymorpheus не заменяет компонентную модель Angular. В современных версиях фреймворка с развитием реактивных сигналов (Signals) часть задач управления изменениями берет на себя платформа. Тем не менее задача создания согласованного контракта для библиотек компонентов остается актуальной: полиморфный слой сохраняет стабильный публичный интерфейс, избавляя проект от шаблонного клея и обеспечивая баланс между простотой и кастомизируемостью.
