Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Дайджесты новостей
Изометрическая сцена цифровой мастерской фронтенд-архитектора: модульные блоки и полиморфные интерфейсные элементы собираются в сложную динамическую систему компонентов Angular с неоновыми потоками контекста и сигналов

Архитектура динамического UI в Angular: почему Polymorpheus стал базой для сложных компонентов

Создание переиспользуемых интерфейсных библиотек регулярно сталкивается с дилеммой: как спроектировать компонент, одинаково легко отображающий короткий текст, разметку со ссылкой, шаблон и интерактивный блок? В типовом сценарии разработки модального окна, всплывающей подсказки (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).

Анатомия архитектурного тупика в библиотеках компонентов

В компонентах общего назначения — меню, диалогах или таблицах — отсутствие связи между четырьмя подходами порождает тупик. Рассмотрим эволюцию ячейки таблицы:

  1. Сначала ячейка принимает текст: @Input() content: string.
  2. Позже требуется форматирование чисел: @Input() content: string | ((data: Row) => string).
  3. Затем дизайнеры добавляют иконку с тултипом, требуя TemplateRef<RowContext>. В шаблоне появляется развилка на проверках типов через @if и @else.
  4. Наконец, требуется интерактивный редактор. Свойство должно принимать классы компонентов (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-templateTemplateRef$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) часть задач управления изменениями берет на себя платформа. Тем не менее задача создания согласованного контракта для библиотек компонентов остается актуальной: полиморфный слой сохраняет стабильный публичный интерфейс, избавляя проект от шаблонного клея и обеспечивая баланс между простотой и кастомизируемостью.