4. Компоненты
Angular Компоненты — строительные блоки приложения 🧱
Заголовок раздела «Angular Компоненты — строительные блоки приложения 🧱»Представь, что ты строишь из Lego. Каждый кубик — это компонент. Маленький, самостоятельный, с понятным интерфейсом. Соединяешь кубики вместе — получаешь приложение. Вот и вся суть Angular компонентов! 🎉
Что такое компонент?
Заголовок раздела «Что такое компонент?»Компонент в Angular — это класс TypeScript + шаблон HTML + стили CSS, упакованные вместе. Это основная единица UI в любом Angular приложении.
import { Component } from '@angular/core';
@Component({ selector: 'app-hello', template: `<h1>Привет, {{ name }}! 👋</h1>`, styles: [`h1 { color: #dd0031; }`]})export class HelloComponent { name = 'Яша';}Три части каждого компонента:
- Декоратор
@Component— метаданные (как зовут, как выглядит) - Класс — логика и данные
- Шаблон — что видит пользователь
Декоратор @Component — разбираем по частям 🔍
Заголовок раздела «Декоратор @Component — разбираем по частям 🔍»selector — имя компонента
Заголовок раздела «selector — имя компонента»selector — это CSS-селектор, который говорит Angular, где вставить компонент в DOM.
// Элементный селектор (самый популярный)@Component({ selector: 'app-button' })// Использование: <app-button></app-button>
// Атрибутный селектор@Component({ selector: '[appHighlight]' })// Использование: <div appHighlight></div>
// Классовый селектор@Component({ selector: '.app-card' })// Использование: <div class="app-card"></div>💡 Лайфхак: Для большинства компонентов используй элементный селектор. Атрибутный пригодится для директив-компонентов.
template vs templateUrl
Заголовок раздела «template vs templateUrl»// Инлайн-шаблон — для маленьких компонентов@Component({ selector: 'app-chip', template: ` <span class="chip">{{ label }}</span> `})export class ChipComponent { label = 'Angular';}
// Внешний файл — для сложных шаблонов@Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss']})export class DashboardComponent {}changeDetection — когда Angular проверяет изменения
Заголовок раздела «changeDetection — когда Angular проверяет изменения»import { Component, ChangeDetectionStrategy } from '@angular/core';
// Default — Angular проверяет ВСЁ дерево при любом событии@Component({ selector: 'app-heavy', template: `...`, changeDetection: ChangeDetectionStrategy.Default})
// OnPush — Angular проверяет ТОЛЬКО при новых @Input() или вручную// 🚀 Значительно ускоряет большие приложения!@Component({ selector: 'app-fast', template: `...`, changeDetection: ChangeDetectionStrategy.OnPush})ViewEncapsulation — как изолировать стили 🎨
Заголовок раздела «ViewEncapsulation — как изолировать стили 🎨»import { Component, ViewEncapsulation } from '@angular/core';
// Emulated (по умолчанию) — Angular добавляет уникальные атрибуты// <p _ngcontent-abc-c123>текст</p>@Component({ selector: 'app-card', template: `<p>Карточка</p>`, styles: [`p { color: blue; }`], encapsulation: ViewEncapsulation.Emulated // стили не утекают наружу ✅})
// None — глобальные стили (осторожно! могут сломать другие компоненты)@Component({ encapsulation: ViewEncapsulation.None // стили видны везде ⚠️})
// ShadowDom — настоящая изоляция через Shadow DOM браузера@Component({ encapsulation: ViewEncapsulation.ShadowDom // максимальная изоляция 🛡️})Дерево компонентов 🌳
Заголовок раздела «Дерево компонентов 🌳»Angular приложение — это дерево компонентов. Наверху — AppComponent, далее ветвится на фичевые и шаренные компоненты:
AppComponent (корень)├── HeaderComponent│ └── NavComponent├── MainComponent│ ├── ProductListComponent│ │ └── ProductCardComponent (×N)│ └── SidebarComponent└── FooterComponent// app.component.ts — корневой компонент@Component({ selector: 'app-root', template: ` <app-header></app-header> <main> <app-product-list [products]="products"></app-product-list> </main> <app-footer></app-footer> `})export class AppComponent { products = [ { id: 1, name: 'Ноутбук', price: 50000 }, { id: 2, name: 'Мышь', price: 2500 }, ];}Smart vs Dumb компоненты 🧠 vs 🎨
Заголовок раздела «Smart vs Dumb компоненты 🧠 vs 🎨»Это один из самых важных паттернов в Angular:
| Smart (Container) | Dumb (Presentational) | |
|---|---|---|
| Знает о данных | ✅ Да | ❌ Нет |
| Получает данные | Из сервисов/стора | Только через @Input() |
| Отправляет события | В сервисы/стор | Только через @Output() |
| Переиспользуемость | Низкая | Высокая |
// 🧠 Smart компонент — знает о данных@Component({ selector: 'app-product-page', template: ` <app-product-list [products]="products$ | async" (productSelected)="onProductSelected($event)"> </app-product-list> `})export class ProductPageComponent { products$ = this.productService.getProducts();
constructor(private productService: ProductService) {}
onProductSelected(product: Product) { this.router.navigate(['/product', product.id]); }}
// 🎨 Dumb компонент — просто отображает@Component({ selector: 'app-product-list', template: ` @for (product of products; track product.id) { <app-product-card [product]="product" (click)="productSelected.emit(product)"> </app-product-card> } `, changeDetection: ChangeDetectionStrategy.OnPush // всегда OnPush для Dumb!})export class ProductListComponent { @Input() products: Product[] = []; @Output() productSelected = new EventEmitter<Product>();}@Input() — данные текут вниз ⬇️
Заголовок раздела «@Input() — данные текут вниз ⬇️»@Input() позволяет передавать данные от родителя к дочернему компоненту:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
interface Product { id: number; name: string; price: number; inStock: boolean;}
@Component({ selector: 'app-product-card', template: ` <div class="card" [class.out-of-stock]="!product.inStock"> <h3>{{ product.name }}</h3> <p class="price">{{ product.price | currency:'RUB' }}</p> <span *ngIf="!product.inStock">Нет в наличии</span> </div> `})export class ProductCardComponent implements OnChanges { @Input({ required: true }) product!: Product; @Input('size') cardSize: 'sm' | 'md' | 'lg' = 'md'; // Input с алиасом
ngOnChanges(changes: SimpleChanges) { if (changes['product']) { console.log('Продукт изменился:', changes['product'].currentValue); } }}Использование в родителе:
<!-- Передаём объект через property binding --><app-product-card [product]="selectedProduct" [size]="'lg'"></app-product-card>@Output() — события летят вверх ⬆️
Заголовок раздела «@Output() — события летят вверх ⬆️»@Output() + EventEmitter позволяет дочернему компоненту сообщать о событиях родителю:
import { Component, Output, EventEmitter } from '@angular/core';
@Component({ selector: 'app-product-card', template: ` <div class="card"> <h3>{{ product.name }}</h3> <button (click)="addToCart()">В корзину 🛒</button> <button (click)="toggleFavorite()"> {{ isFavorite ? '❤️' : '🤍' }} </button> </div> `})export class ProductCardComponent { @Input({ required: true }) product!: Product; @Output() addedToCart = new EventEmitter<Product>(); @Output() favoriteToggled = new EventEmitter<{ product: Product; isFavorite: boolean }>();
isFavorite = false;
addToCart() { this.addedToCart.emit(this.product); // передаём данные вверх }}Обработка в родителе:
<app-product-card [product]="product" (addedToCart)="onAddToCart($event)" (favoriteToggled)="onFavoriteToggled($event)"></app-product-card>Хуки жизненного цикла ♻️
Заголовок раздела «Хуки жизненного цикла ♻️»Компонент рождается, живёт и умирает. Angular даёт хуки для каждого этапа:
import { Component, OnInit, OnDestroy, OnChanges, AfterViewInit, SimpleChanges, Input, ViewChild, ElementRef } from '@angular/core';import { Subject } from 'rxjs';import { takeUntil } from 'rxjs/operators';
@Component({ selector: 'app-lifecycle', template: `<canvas #myCanvas></canvas>` })export class LifecycleComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() data: any; @ViewChild('myCanvas') canvas!: ElementRef<HTMLCanvasElement>; private destroy$ = new Subject<void>();
constructor(private service: SomeService) {} // 1️⃣ DI — не делаем запросы!
ngOnChanges(changes: SimpleChanges) { // 2️⃣ до ngOnInit, при каждом @Input console.log('Inputs изменились:', changes); }
ngOnInit() { // 3️⃣ здесь делаем HTTP запросы this.service.getData() .pipe(takeUntil(this.destroy$)) .subscribe(data => console.log(data)); }
ngAfterViewInit() { // 4️⃣ DOM готов — можно @ViewChild const ctx = this.canvas.nativeElement.getContext('2d'); }
ngOnDestroy() { // 5️⃣ очищаем подписки! this.destroy$.next(); this.destroy$.complete(); }}Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: