5. Шаблоны и привязка данных
Angular Template Syntax — язык шаблонов 📝
Заголовок раздела «Angular Template Syntax — язык шаблонов 📝»Шаблоны Angular — это не просто HTML. Это суперсила: интерполяция, привязки, директивы, пайпы и новый синтаксис управляющих потоков. Давай разберём каждый элемент! 🚀
Интерполяция {{ expression }} — данные в шаблон
Заголовок раздела «Интерполяция {{ expression }} — данные в шаблон»Двойные фигурные скобки — самый простой способ вывести данные в шаблон:
@Component({ template: ` <h1>{{ title }}</h1> <p>2 + 2 = {{ 2 + 2 }}</p> <p>{{ 'Привет'.toUpperCase() }}</p> <p>Сегодня: {{ today | date:'dd.MM.yyyy' }}</p> <p>{{ user.name }} из {{ user.city }}</p> `})export class AppComponent { title = 'Angular приложение'; today = new Date(); user = { name: 'Яша', city: 'Москва' };}⚠️ Запрещено в интерполяции: присваивание (
=), операторыnew, цепочки (;), инкремент/декремент (++,--), битовые операторы. Шаблоны должны быть без побочных эффектов!
Выражения vs Утверждения в шаблонах
Заголовок раздела «Выражения vs Утверждения в шаблонах»<!-- ✅ Template Expression — вычисляет значение -->[value]="firstName + ' ' + lastName"[disabled]="!isFormValid"[class.active]="currentTab === 'home'"
<!-- ✅ Template Statement — реагирует на событие -->(click)="deleteItem(item)"(submit)="onFormSubmit($event)"(keyup.enter)="search()"Оператор безопасной навигации ?. — защита от null 🛡️
Заголовок раздела «Оператор безопасной навигации ?. — защита от null 🛡️»Один из самых полезных операторов! Защищает от Cannot read property of undefined:
@Component({ template: ` <p>{{ user?.address?.street }}</p> <span>{{ order?.items?.[0]?.name }}</span> <p>{{ user?.displayName || 'Гость' }}</p> `})export class UserComponent { user: User | null = null;}Оператор ненулевого утверждения !. — “я знаю что не null”
Заголовок раздела «Оператор ненулевого утверждения !. — “я знаю что не null”»@Component({ template: ` <!-- Говорим TypeScript: "доверяй мне, тут точно не null" --> <p>{{ user!.name }}</p>
<!-- Используется когда уверены в значении, но TypeScript жалуется из-за типов --> <app-user-card [user]="currentUser!"></app-user-card> `})💡 Используй
!.осторожно — он отключает проверку TypeScript. Лучше использовать?.с fallback значением.
Template Reference Variables #name — ссылки на элементы 🔖
Заголовок раздела «Template Reference Variables #name — ссылки на элементы 🔖»Переменная шаблона даёт прямой доступ к DOM элементу или компоненту:
<!-- #phone — ссылка на DOM элемент <input> --><input #phone type="tel" placeholder="+7 (___) ___-__-__" /><button (click)="callNumber(phone.value)">Позвонить</button>
<!-- #myForm — ссылка на директиву NgForm --><form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)"> <input name="email" ngModel required email /> <button [disabled]="!myForm.valid">Отправить</button></form>
<!-- #datePicker — ссылка на компонент --><app-date-picker #datePicker></app-date-picker><button (click)="datePicker.open()">Открыть календарь</button>
<!-- Использование в том же шаблоне, сразу --><video #player width="400"> <source src="movie.mp4" type="video/mp4" /></video><button (click)="player.play()">▶ Играть</button><button (click)="player.pause()">⏸ Пауза</button>В TypeScript доступ через @ViewChild:
export class AppComponent { @ViewChild('phone') phoneInput!: ElementRef<HTMLInputElement>;
ngAfterViewInit() { this.phoneInput.nativeElement.focus(); }}<ng-template> — переиспользуемые фрагменты шаблона
Заголовок раздела «<ng-template> — переиспользуемые фрагменты шаблона»ng-template не рендерится сам по себе — это заготовка, которую Angular использует по ссылке:
<!-- Определяем шаблон загрузки --><ng-template #loadingTpl> <div class="spinner"> <span>⏳ Загрузка...</span> </div></ng-template>
<!-- Используем шаблон в разных местах -->@if (isLoading) { <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>}
<!-- ng-template для @if else -->@if (user) { <app-user-profile [user]="user" />} @else { <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>}
<!-- ng-template с параметрами (context) --><ng-template #userCard let-user let-index="index"> <div class="card"> <span class="badge">{{ index + 1 }}</span> <h3>{{ user.name }}</h3> </div></ng-template><ng-container> — невидимый контейнер 📦
Заголовок раздела «<ng-container> — невидимый контейнер 📦»ng-container не добавляет DOM элементы — идеален для группировки без лишних тегов:
<!-- ✅ ng-container не создаёт DOM элемент — идеален для таблиц и flex/grid --><ng-container *ngIf="isAdmin"> <td>Имя</td> <td>Действия</td></ng-container>
<!-- Несколько директив на одном элементе через ng-container --><ng-container *ngIf="items.length > 0"> <ng-container *ngFor="let item of items; let i = index"> @if (item.isVisible) { <app-item [item]="item" [index]="i" /> } </ng-container></ng-container><ng-content> — проекция контента ✨
Заголовок раздела «<ng-content> — проекция контента ✨»Content projection — это как children в React. Позволяет вставлять произвольный HTML внутрь компонента:
@Component({ selector: 'app-card', template: ` <div class="card"> <div class="card-header"> <ng-content select="[card-title]"></ng-content> </div> <div class="card-body"> <ng-content></ng-content> <!-- дефолтный слот --> </div> <div class="card-footer"> <ng-content select="[card-actions]"></ng-content> </div> </div> `})export class CardComponent {}<!-- Использование карточки с проекцией --><app-card> <h2 card-title>Заголовок карточки</h2>
<!-- Это попадает в дефолтный ng-content --> <p>Основной контент карточки с любым HTML.</p> <p>Можно передать <strong>что угодно!</strong></p>
<div card-actions> <button>Отмена</button> <button class="primary">Сохранить</button> </div></app-card>Пайпы | — трансформация данных в шаблоне 🔧
Заголовок раздела «Пайпы | — трансформация данных в шаблоне 🔧»Пайп берёт значение и возвращает трансформированное:
<!-- Встроенные пайпы Angular --><p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD --><p>{{ user.name | titlecase }}</p> <!-- John Doe --><p>{{ 4999.99 | currency:'RUB':'symbol' }}</p> <!-- ₽4,999.99 --><p>{{ today | date:'dd MMMM yyyy' }}</p> <!-- 24 января 2024 --><pre>{{ user | json }}</pre> <!-- {"name":"Яша",...} --><p>{{ longText | slice:0:100 }}...</p> <!-- первые 100 символов -->
<!-- async пайп — автоматически подписывается и отписывается! --><div *ngIf="users$ | async as users"> @for (user of users; track user.id) { <p>{{ user.name }}</p> }</div>
<!-- Цепочка пайпов --><p>{{ description | slice:0:200 | uppercase }}</p>Создание своего пайпа:
@Pipe({ name: 'rubles', standalone: true })export class RublesPipe implements PipeTransform { transform(value: number): string { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(value); }}// Использование: {{ 4999 | rubles }} → "4 999 ₽"Новый синтаксис управляющих потоков (Angular 17+) 🆕
Заголовок раздела «Новый синтаксис управляющих потоков (Angular 17+) 🆕»Angular 17 представил встроенный синтаксис вместо структурных директив:
<!-- @if — вместо *ngIf -->@if (user.isLoggedIn) { <app-dashboard [user]="user" />} @else if (user.isPending) { <p>Ожидает подтверждения...</p>} @else { <app-login />}
<!-- @for — вместо *ngFor, ОБЯЗАТЕЛЕН track! -->@for (product of products; track product.id) { <app-product-card [product]="product" />} @empty { <p>Товары не найдены 😔</p>}
<!-- @for с дополнительными переменными -->@for (item of items; track item.id; let i = $index, last = $last) { <li [class.last]="last">{{ i + 1 }}. {{ item.name }}</li>}
<!-- @switch — вместо ngSwitch -->@switch (user.role) { @case ('admin') { <app-admin-panel /> } @case ('editor') { <app-editor-toolbar /> } @default { <app-viewer-ui /> }}Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: