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

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 = 'Яша';
}

Три части каждого компонента:

  1. Декоратор @Component — метаданные (как зовут, как выглядит)
  2. Класс — логика и данные
  3. Шаблон — что видит пользователь

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>

💡 Лайфхак: Для большинства компонентов используй элементный селектор. Атрибутный пригодится для директив-компонентов.

// Инлайн-шаблон — для маленьких компонентов
@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
})

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 },
];
}

Это один из самых важных паттернов в 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() позволяет передавать данные от родителя к дочернему компоненту:

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() + 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();
}
}

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