10 — Встроенные директивы на пальцах

Цель: пройтись по всем готовым директивам, которые даёт Angular «из коробки», понять что они делают и как связаны с механикой view/карманов. Точные пути в исходнике — в соседнем vault'е (notes/v21/).

Сначала — два слова про «новый» и «старый» способ

В Angular 21 у управления шаблоном есть два поколения:

НОВОЕ (control flow, встроено в компилятор — это НЕ директивы):
  @if / @else if / @else
  @for (... ; track ...) { } @empty { }
  @switch / @case / @default

СТАРОЕ (директивы из @angular/common, теперь @deprecated):
  *ngIf      *ngFor      *ngSwitch / *ngSwitchCase / *ngSwitchDefault

Важно: @if/@for/@switch — это синтаксис компилятора, а не директивы. Их не надо импортировать, они компилируются в специальные инструкции (быстрее и проще типизируются). Старые *ngIf/*ngFor — это настоящие классы-директивы, которые под капотом делают то же самое (createEmbeddedView / remove), но через ViewContainerRef руками. Подробно механизм — в 08-portaly.

Практика: в новом коде пиши @if/@for/@switch. Старые *ng* ещё работают, но их планируют убрать. Дальше в главе — что под капотом у старых, чтобы понимать легаси-код.

Группа 1 — структурные (меняют структуру DOM)

«Структурная» = добавляет/убирает куски DOM. Узнаёшь по * (микросинтаксис). Все работают через те самые три шага из 08-portaly: карман → копия view → вставить.

*ngIf — показать/спрятать

<div *ngIf="open">...</div>
<div *ngIf="user as u; else loading">{{u.name}}</div>
<ng-template #loading>Загрузка...</ng-template>
  • truecreateEmbeddedView тела; falseclear (убрать).
  • ngIfThen / ngIfElse — какой шаблон показывать в каждой ветке.
  • Новый аналог: @if (open) { } @else { }.

*ngFor — список

<li *ngFor="let n of items; track n; let i = index">{{i}}: {{n}}</li>
  • Внутри — IterableDiffer: на каждом цикле смотрит, что в массиве добавилось/удалилось/переехало, и делает только нужные правки (createEmbeddedView / move / remove) — список целиком не перерисовывается.
  • trackBy (старый) / track (новый) — как опознавать элементы, чтобы не пересоздавать DOM зря.
  • Локальные переменные: index, count, first, last, even, odd.
  • Новый аналог: @for (n of items; track n) { } @empty { }.

*ngSwitch — выбор одной ветки

<div [ngSwitch]="status">
  <p *ngSwitchCase="'ok'">Готово</p>
  <p *ngSwitchCase="'err'">Ошибка</p>
  <p *ngSwitchDefault>Ждём</p>
</div>
  • Сравнивает значение (===), показывает совпавший case (или default).
  • Новый аналог: @switch (status) { @case ('ok') {} @default {} }.

Группа 2 — outlet'ы (вставить готовое в это место)

Это не «спрячь/покажи», а «возьми вот это и вставь сюда». Тонкие обёртки над движком из 08-portaly.

ngTemplateOutlet — вставь этот <ng-template>

<ng-template #row let-name="name"><b>{{name}}</b></ng-template>
<div *ngTemplateOutlet="row; context: { name: 'Аня' }"></div>
  • Под капотом — viewContainerRef.createEmbeddedView(tpl, context).
  • Хитрость: контекст передаётся через Proxy — поменял поле контекста, view не пересоздаётся, просто обновляется. Сменил сам шаблон — старый убирается, новый вставляется.
  • Зачем: переиспользовать кусок разметки, передавать «слоты»-шаблоны в дочерний компонент.

ngComponentOutlet — создай компонент динамически

<ng-container *ngComponentOutlet="WidgetComponent; inputs: { title: 'Привет' }" />
  • Под капотом — viewContainerRef.createComponent(Comp, {...}).
  • Умеет: передавать inputs (на каждом цикле синхронизирует через componentRef.setInput()), свой injector, environmentInjector, ngModule, content-проекцию.
  • Зачем: когда тип компонента известен только в рантайме (плагины, виджеты, динамические формы) — декларативная замена ручного createComponent.

Группа 3 — атрибутные (меняют сам элемент, не структуру)

Не добавляют/убирают DOM, а правят атрибуты существующего элемента. Структуры не трогают, карманы не создают.

ngClass — классы по условию

<div [ngClass]="{ active: isActive, error: hasError }"></div>
<div [ngClass]="['box', theme]"></div>
  • Принимает строку / массив / Set / объект { class: boolean }.
  • На каждом ngDoCheck сверяет состояние каждого класса и через Renderer2 добавляет/убирает только то, что изменилось.
  • Часто проще: нативный [class.active]="isActive" — для одного класса не нужен ngClass.

ngStyle — inline-стили

<div [ngStyle]="{ 'color': c, 'top.px': y }"></div>
  • Принимает объект { styleName: value }, понимает единицы в ключе (top.px, font-size.em).
  • Через KeyValueDiffer отслеживает изменения и правит стили через Renderer2.
  • Часто проще: нативный [style.color]="c" / [style.top.px]="y".

Бонус — i18n (это pipe'ы, не директивы)

  • i18nPlural — число → текст по правилам множественного числа CLDR ({ '=0': 'нет', '=1': 'один', other: '# штук' }).
  • i18nSelect — выбор строки по значению ({ male: 'он', female: 'она', other: 'оно' }).

А где CDK Portal / Overlay / Dialog?

Их нет в этом исходнике (@angular/cdk — отдельная библиотека, не часть ядра). В 08-portaly описано, что CDK Portal — лишь полиморфная обёртка над всё теми же createEmbeddedView / createComponent / переносом DOM. Никакого нового механизма.

Карта: кто через что работает

СТРУКТУРНЫЕ (структура DOM)        ← движок: ViewContainerRef + createEmbeddedView
  *ngIf / @if          → создать/убрать тело
  *ngFor / @for        → IterableDiffer → createEmbeddedView/move/remove
  *ngSwitch / @switch  → показать совпавший case

OUTLET'Ы (вставить готовое)        ← тот же движок, тонкая обёртка
  ngTemplateOutlet     → createEmbeddedView(tpl, ctx)
  ngComponentOutlet    → createComponent(Comp, {inputs,...})

АТРИБУТНЫЕ (правят элемент)        ← НЕ создают view, правят через Renderer2
  ngClass              → add/removeClass
  ngStyle              → set/removeStyle

Итог

  • Сегодня по умолчанию: @if / @for / @switch (control flow компилятора), а не *ngIf / *ngFor / *ngSwitch (старые директивы, deprecated).
  • Структурные директивы и @for/@if — это обёртки над «карман + копия view» из 08-portaly.
  • ngTemplateOutlet / ngComponentOutlet — «вставь готовый шаблон/компонент сюда».
  • ngClass / ngStyle — единственные, кто не создаёт view, а просто правит атрибуты элемента.
  • DI, который раздают компоненты этим детям, ищется по дереву узлов — см. 09-di-injectory.

Теперь у тебя полная картина: код → компиляция → сборка → старт → работа → view/LView → селекторы → порталы → DI → встроенные директивы. Открой prosto.canvas — там весь путь одним полотном.