Angular IDPStrong Junior
🔥 0

Модуль 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()" />
БАТЬКО · TASK-LIST тримає список і поточний вибір openTask(task) { … } ДИТИНА · TASK-CARD тільки показує те, що дали selected.emit(task) [task] — дані (selected) — подія Дитина не знає ні про батька, ні про сервіси, ні про роут. Тому її легко перевикористати і легко протестувати: подав input — перевірив, що вилетів потрібний output. Це і є «presentation-компонент», якого лід чекає в модулі 3.
Однонаправлений потік: дані вниз, події вгору. Дитина лишається дурною й тому придатною до перевикористання.

Двостороння прив'язка: 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.