Перейти к содержимому

5. Шаблоны и привязка данных

Шаблоны Angular — это не просто HTML. Это суперсила: интерполяция, привязки, директивы, пайпы и новый синтаксис управляющих потоков. Давай разберём каждый элемент! 🚀


Двойные фигурные скобки — самый простой способ вывести данные в шаблон:

@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, цепочки (;), инкремент/декремент (++, --), битовые операторы. Шаблоны должны быть без побочных эффектов!


<!-- ✅ 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 не добавляет 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>

Content projection — это как children в React. Позволяет вставлять произвольный HTML внутрь компонента:

card.component.ts
@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 /> }
}

Попробуйте концепцию в интерактивном редакторе: