Модуль 2 · урок 2 з 15
Компонент зсередини
Компонент — це шматок екрана: шаблон, клас із даними й стилі. Уся решта Angular існує, щоб компоненти отримували дані й лишались простими. Тут розберемо, з чого компонент складається, як він спілкується з сусідами й що з цього застаріло.
Мінімальний компонент
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-task-card', // як тег: <app-task-card>
changeDetection: ChangeDetectionStrategy.OnPush, // модуль 3 пояснить чому
templateUrl: './task-card.html',
styleUrl: './task-card.scss',
})
export class TaskCard {
title = 'Порожня задача';
}
Три речі, які варто зауважити одразу:
- Немає
standalone: true. У сучасних версіях компоненти самодостатні за замовчуванням — жоднихNgModuleне потрібно. - Ім'я класу без суфікса
Component. Новий стиль CLI саме такий; у старому коді побачишTaskCardComponent— обидва варіанти живі. OnPushодразу. Це вимога з таблиці ліда, і простіше поставити відразу, ніж переписувати потім.
Дані всередину: input()
import { Component, input } from '@angular/core';
import { Task } from '../../core/models/task';
@Component({ /* … */ })
export class TaskCard {
// обов'язковий вхід: якщо батько його не передасть — помилка ще на етапі компіляції
task = input.required<Task>();
// необов'язковий, зі значенням за замовчуванням
compact = input(false);
// вхід із перетворенням: у шаблоні пишуть рядок, усередині отримуємо число
level = input(0, { transform: (v: string | number) => Number(v) });
}
<!-- у батьківському шаблоні -->
<app-task-card [task]="selectedTask()" [compact]="true" />
input() повертає сигнал: у шаблоні й у коді значення читається як функція —
task(). Це не примха синтаксису: саме тому Angular знає, хто саме залежить від цього
значення, і перемальовує лише потрібне.
@Input() task!: Task; — те саме, тільки поле звичайне, а не сигнал.
Працює й далі, але має дві незручності: значення може бути undefined до першого
присвоєння (звідси й !), і щоб зреагувати на зміну, потрібен
ngOnChanges. Із сигнальним input() достатньо computed().
Події назовні: output()
import { Component, input, output } from '@angular/core';
export class TaskCard {
task = input.required<Task>();
selected = output<Task>(); // подія з корисним навантаженням
dismissed = output<void>(); // подія без даних
onPick(): void {
this.selected.emit(this.task()); // сповістити батька
}
}
<app-task-card [task]="t" (selected)="openTask($event)" (dismissed)="hide()" />
Двостороння прив'язка: model()
Інколи компонент і показує значення, і змінює його — типовий приклад поле пошуку.
Для цього є model(): він працює як input і output одночасно.
export class SearchBox {
value = model(''); // читається як value(), пишеться як value.set(...)
onInput(text: string): void {
this.value.set(text); // батько отримає нове значення автоматично
}
}
<app-search-box [(value)]="query" /> <!-- «банан у коробці» -->
Життєвий цикл
| Хук | Коли спрацьовує | Для чого реально потрібен |
|---|---|---|
constructor | При створенні класу | Отримати залежності через inject() |
ngOnInit | Після перших вхідних даних | Стартове завантаження — хоча зараз це частіше роблять інакше |
ngOnChanges | При зміні класичних @Input() | Реакція на нові дані у старому стилі |
ngOnDestroy | Перед видаленням компонента | Прибирання: таймери, слухачі, підписки (модуль 4) |
ngAfterViewInit | Коли шаблон уже в DOM | Робота з реальними елементами, вимірювання |
У сигнальному стилі ngOnChanges замінює computed(), а
ngOnDestroy для підписок — takeUntilDestroyed().
Лишаються переважно constructor і зрідка ngAfterViewInit.
Якщо в компоненті три-чотири хуки — це майже завжди ознака, що логіку варто винести в сервіс.
Доступ до елементів шаблону
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
export class SearchBox {
input = viewChild<ElementRef<HTMLInputElement>>('field');
constructor() {
afterNextRender(() => {
this.input()?.nativeElement.focus(); // фокус після першого рендеру
});
}
}
<input #field type="search">
Це знадобиться в модулі 7, коли треба буде ставити фокус у модалці. Правило просте: чіпати DOM руками можна, але лише коли інакше не виходить — у 95% випадків достатньо прив'язки в шаблоні.
Стилі компонента
// task-card.scss — ці стилі не витечуть назовні
:host { // сам елемент <app-task-card>
display: block; // за замовчуванням він inline — часта причина «чому не працює margin»
}
:host(.is-selected) { // коли на хості є клас
border-color: var(--accent);
}
.card__title { font-weight: 700; }
Перший рядок тут важливіший, ніж здається: хост-елемент компонента за замовчуванням
inline, тож розміри й вертикальні відступи на ньому не працюють — рівно та історія,
що була в уроці про блокову модель. Механіку ізоляції стилів розберемо в модулі 3.