Компоненти Angular з нуля: селектори, шаблони, вхідні дані, вихідні дані
Практичний огляд сучасних самостійних компонентів Angular: структура файлів, декоратори, селектори, шаблони, обмежені стилі та потік даних між батьківськими та дочірніми компонентами за допомогою input() та output().
Як тільки проект Angular створений та почав працювати, наступне питання — як сам екран будується. Відповідь — компоненти: невеликі, самодостатні одиниці, кожна з яких відповідає за частину інтерфейсу. У цьому посібнику розглядається, з чого складається компонент, як самостійні компоненти змінюють ситуацію порівняно зі старішим кодом, заснованим на NgModule, та як батьківський та дочірній компоненти обмінюються даними та подіями за допомогою сучасних API input() та output(). До кінця ви зможете створювати компоненти, під’єднувати їх до сторінки та передавати інформацію в обох напрямках.
Якщо вам все ще потрібен проект для експериментів, посібник з створення сучасного проекту Angular 22 охоплює налаштування та структуру папок, які припускається ця стаття.
Чому Angular розділяє інтерфейс на компоненти
Angular Application
│
├── Navbar Component
├── Sidebar Component
├── Product Component
├── Login Component
└── Footer Component
Компонент зазвичай об’єднує чотири елементи: клас TypeScript, який керує станом та поведінкою, шаблон HTML, що описує структуру, стилі для зовнішнього вигляду та метадані, які пояснюють Angular, як з ним працювати. Оскільки кожен елемент має чітку функцію, можна аналізувати навігаційну панель, не думаючи про футер, використовувати однакові картки продуктів у кількох місцях та тестувати окремі частини інтерфейсу ізольовано. Форма входу, панель профілю чи віджет дашборду — усе це природні приклади таких компонентів.
Стандартним варіантом є самостійні компоненти
Багато навчальних матеріалів з Angular, особливо старіші, організовують компоненти всередині NgModule, який оголошує компоненти та вказує, від чого вони залежать. Сучасний Angular відійшов від такої моделі. Тепер компоненти за замовчуванням є автономними, тож компонент може існувати сам по собі без реєстрації в модулі. Мінімальний компонент виглядає так:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
export class WelcomeComponent {
}
Ви можете очікувати побачити прапорець, який позначає компонент як автономний, як у наступному рядку, але це більше не є обов’язковим:
standalone: true
Його відсутність має такий самий ефект, оскільки стандартною поведінкою є робота компонента окремо. Коли самостійному компоненту потрібен інший компонент, директива чи пайп, він вказує цю залежність у масиві imports своєї конфігурації @Component. Перевагою є локальність: можна відкрити один файл та побачити, від чого саме залежить цей компонент, замість того щоб шукати інформацію в определенні модуля в іншій частині проекту.
Як компонент зберігається на диску
Типовий компонент знаходиться у власній папці разом із трьома файлами:
welcome/
│
├── welcome.ts
├── welcome.html
└── welcome.css
Файл .ts містить логіку, файл .html — маркап, а файл .css — стилі. Ось що може міститися в кожному з них для простого компонента привітання. Спочатку йде клас, який експортує властивість title для шаблону:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
export class WelcomeComponent {
title = 'Welcome to Angular 22';
}
Шаблон читає цю властивість за допомогою подвійних фігурних дужок:
<h1>{{ title }}</h1>
<p>
This is my first Angular component.
</p>
А стилівський файл застосовується лише до елементів у цьому шаблоні:
h1 {
color: #dd0031;
}
p {
font-size: 18px;
}
Декоратор @Component() є з’єднувальним елементом. Він вказує Angular на шаблон та стилівський файл, щоб ці три файли функціонували як одна єдність, як показано на наступному скетчі:
welcome.ts
│
├── Logic
│
├── welcome.html
│ ↓
│ UI
│
└── welcome.css
↓
Style
Керівництво з стилю Angular рекомендує тримати тісно пов’язані файли в одному каталозі, саме це робить така структура. Коли компонент розростається, ви точно знаєте, де знаходяться його маркап, стилі та логіка.
Що декоратор @Component() повідомляє Angular
Декоратор додає метадані до класу. Саме ці метадані допомагають Angular зрозуміти, для чого призначений клас та як його відображати:
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
Тут більшу частину роботи виконують три властивості:
selectorвизначає ім’я тегу, яке ви використовуєте для розміщення компонента в інших шаблонах.templateUrlвказує на HTML-файл із маркуванням компонента.styleUrlвказує на таблицю стилів компонента.
Існують ще додаткові опції, такі як imports для включення інших компонентів чи функцій Angular, а також вбудовані альтернативи templateUrl та styleUrl, про які йтиметься далі в цій статті. Немає потреби вивчати їх усі заздалегідь; вони стануть зрозумілими під час створення більшої кількості компонентів.
Селектори перетворюють клас на власний елемент
Селектор — це ідентифікатор, який Angular шукає в шаблонах. У цій конфігурації він встановлений як app-welcome:
@Component({
selector: 'app-welcome',
...
})
Отже, саме значення селектора є просто:
app-welcome
У будь-якому місці, де ви хочете, щоб компонент з’явився, ви пишете його назву так, ніби це HTML-тег:
<app-welcome></app-welcome>
Коли Angular зустрічає <app-welcome> під час рендерингу шаблону, він створює екземпляр відповідного компонента та рендерує його шаблон всередині цього елемента. Елемент, який відповідає селектору, називається господарським елементом компонента, і він залишається в DOM, що має значення пізніше, коли ви хочете налаштувати стиль або додати поведінку до всього компонента.
Angular CLI генерує селектори з префіксом, налаштованим для вашого додатку, зазвичай app-, хоча при створенні компонента ви можете обрати інший селектор. Префікс має не лише декоративне значення: імена кастомних елементів повинні містити дефіс, а префікс, специфічний для проекту, запобігає зіткненню ваших компонентів із нативними елементами чи тегами з бібліотек третіх сторін.
Шаблони поєднують маркап з даними компонента
Шаблон визначає те, що насправді бачить користувач. У найпростішому випадку це звичайний HTML:
<h1>Welcome!</h1>
<p>We are learning Angular components.</p>
Те, що робить шаблони Angular потужнішими за статичний HTML, — це можливість читати дані та викликати функції, визначені у класі компонента. Припустимо, що клас оголошує властивість:
name = 'Mubbassir';
Тоді шаблон може відобразити її за допомогою інтерполяції:
<h1>Hello {{ name }}!</h1>
Angular підтримує синхронність між отриманим результатом відображення та даними: коли змінюється прив’язане значення, сторінка оновлюється без необхідності вручну маніпулювати DOM. Саме цей зв’язок між класом TypeScript та його шаблоном є основою для всього іншого в Angular — від обробки подій до форм.
Стилі компонентів за замовчуванням залишаються обмеженими
Кожен компонент може мати власні стилі:
.card {
padding: 20px;
border-radius: 10px;
}
h2 {
color: #dd0031;
}
За замовчуванням Angular застосовує емульовану інкапсуляцію перегляду до цих правил. Під час збирання проекту він переписує селектори та додає генеровані атрибути до елементів компонента, тож правило на кшталт h2 { ... } відповідає лише елементам h2 у шаблоні цього компонента, а не кожному заголовку в додатку. У додатку з сотнями компонентів такий захист запобігає тому, щоб зміна стилю в одному місці потайки погіршила вигляд іншого.
Інкапсуляція не виключає можливості спільного стилізування. Правила, які мають діяти скрізь — такі як типографіка, параметри скидання налаштувань чи інструменти для формування макету — слід розміщувати у глобальному файлі стилів додатку, тоді як файли компонентів містять стилі, специфічні для цього компонента.
Створення компонента за допомогою Angular CLI
Створення файлів вручну також можливе, але CLI робить це швидше та з послідовною номенклатурою. З кореня проекту виконайте:
ng generate component user-card
або скорочену версію:
ng g c user-card
Генератор створює папку приблизно такої форми (непотрібний символ = після назви файлу специфікації — це опечатка у прикладі, а не частина назви):
user-card/
├── user-card.ts
├── user-card.html
├── user-card.css
└── user-card.spec.ts=
Новіший стиль найменування файлів є коротшим за стару конвенцію user-card.component.ts. Створення тестового файлу .spec.ts залежить від конфігурації тестування проекту. Останні версії CLI, які дотримуються оновленого посібника зі стилю, також схильні генерувати назви класів без суфікса Component (наприклад, UserCard); у наведених нижче прикладах зберігається суфікс, і будь-яке з найменувань підходить, за умови що імпорти збігаються.
Як батьківські та дочірні компоненти спілкуються між собою
Реальні екрани є деревами компонентів, і їм потрібно обмінюватися інформацією. Уявімо батьківський компонент, який знає про користувача, та дочірній компонент, завданням якого є відображення цього користувача. Батьківський компонент потребує способу передачі даних вниз, а дочірній — способу повідомлення про те, що щось сталося, наприклад, при натисканні кнопки.
Angular обробляє напрямок вниз за допомогою input, а напрямок вгору — за допомогою output:
Parent Component
│
│ Data
│
▼
Child Component
│
│ Event
│
▼
Parent Component
Для нового коду Angular рекомендує використовувати функції input() та output(), засновані на сигналах. API, що базуються на декораторах @Input() та @Output(), все ще повністю підтримуються, тому ви все ще знайдете їх у існуючих кодових базах.
Передача даних вниз за допомогою input()
Почніть з класу дочірнього компонента:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.html',
styleUrl: './user-card.css'
})
export class UserCardComponent {
name = input.required<string>();
}
Важливою є рядок з оголошенням input:
name = input.required<string>();
Це означає, що UserCardComponent очікує рядок name від того, хто його використовує. Використання input.required() робить це очікування суворим: якщо батьківський елемент забуде прив’язати name, Angular повідомляє про помилку замість того, щоб мовчки відобразити порожнє значення. Для необов’язкових полів замість цього використовується простий input(), який дозволяє вказати значення за замовчуванням.
Оскільки name є сигналом, а не звичайною властивістю, його читають шляхом виклику. У класі потрібно написати:
this.name()
а в шаблоні:
{{ name() }}
Забути додати дужки — це поширена початкова помилка; без них шаблон відображає саму функцію сигналу, а не її значення. Шаблон дочірнього елемента використовує це поле ось так:
<div class="card">
<h2>{{ name() }}</h2>
<p>Welcome to Angular!</p>
</div>
У батьківському елементі значення прив’язується до поля під час розміщення дочірнього елемента:
<app-user-card [name]="userName" />
Якщо батьківський клас містить:
userName = 'Yuvaraj';
дочірній клас отримує цей рядок та відображає його:
Yuvaraj
Скобки роблять це прив’язкою властивості:
[name]="userName"
Вони наказують Angular оцінити userName як вираз у батьківському класі та передати результат у поле name дочірнього класу. Без скобок Angular б передав буквальний текст userName. Оскільки це поле є сигналом, дочірній клас автоматично оновлюється щоразу, коли змінюється значення у батьківському класі.
Надсилання подій вгору за допомогою output()
Тепер додамо до дочірнього класу кнопку, щоб батьківський клас міг дізнатися, коли обраний користувач. Дочірній клас оголошує output та метод, який через нього передає дані:
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.html',
styleUrl: './user-card.css'
})
export class UserCardComponent {
name = input.required<string>();
selected = output<string>();
selectUser() {
this.selected.emit(this.name());
}
}
Оголошення output визначає власну подію, яка передає рядок:
selected = output<string>();
Виклик emit ініціює цю подію та передає поточну назву як її дані:
this.selected.emit(this.name());
Шаблон дитини викликає selectUser() після натискання кнопки. Зверніть увагу на синтаксис (click), який є механізмом прив’язки подій у Angular:
<div class="card">
<h2>{{ name() }}</h2>
<button (click)="selectUser()">
Select User
</button>
</div>
Батьківський елемент прослуховує цю спеціальну подію за допомогою того ж синтаксису з дужками, але цього разу з назвою отриманих даних:
<app-user-card
[name]="userName"
(selected)="userSelected($event)"
/>
Круглі дужки означають „прослуховувати цю подію“, а $event містить будь-яке значення, яке надіслала дитина — у цьому випадку ім’я користувача. Батьківський елемент обробляє це значення у звичайному методі:
userSelected(name: string) {
console.log('Selected user:', name);
}
Такий розподіл обов’язків забезпечує можливість повторного використання дитини. Вона не знає та не цікавиться тим, що робить батьківський елемент із обраним значенням; вона просто повідомляє про те, що така дія відбулась. Батьківський елемент вирішує, чи потрібно це задокументувати, здійснити переход чи оновити власний стан.
Розміщення компонента на сторінці
Останній приклад поєднує всі елементи воєдино. Ця версія компонента для привітання використовує вбудовані template та styles у декораторі замість окремих файлів, що зручно для дуже малих компонентів:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome-card">
<h2>Welcome Component</h2>
<p>This component was created using the <strong>app-welcome</strong> selector.</p>
</div>
`,
styles: `
.welcome-card {
padding: 25px;
margin-top: 20px;
border-radius: 12px;
background: #f5f5f5;
text-align: center;
font-family: Arial, sans-serif;
}
h2 {
color: #dd0031;
}
p {
font-size: 18px;
}
`
})
export class WelcomeComponent {}
Кореневий компонент імпортує WelcomeComponent у своєму масиві imports, саме це дозволяє використовувати селектор app-welcome у його шаблоні. Його відсутність є одною з найпоширеніших причин помилки „is not a known element“ у самостійних додатках:
import { Component } from '@angular/core';
import { WelcomeComponent } from './welcome';
@Component({
selector: 'app-root',
imports: [WelcomeComponent],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {}
Нарешті, кореневий шаблон розміщує компонент за допомогою його селектора:
<div class="container">
<h1>Angular Components</h1>
<p>
Below is our custom Angular component:
</p>
<app-welcome></app-welcome>
</div>
Інлайн-шаблони зберігають усе в одному файлі, але стають незручними, коли обсяг маркапу перевищує кілька рядків. Розумне правило — починати з інлайн-шаблонів для дрібних елементів візуалізації та переходити до окремих файлів .html та .css, як тільки шаблон потребує справжньої структури.
Основні висновки
- Компонент поєднує клас TypeScript, шаблон та необов’язкові стилі, які з’єднані за допомогою декоратора
@Component(). - У сучасному Angular стандартом є використання самостійних компонентів; залежності оголошуються у масиві
importsкожного компонента, а не вNgModule. - Селектор — це користувацький тег, який використовується для розміщення компонента, а відповідний елемент стає його хостом.
- Шаблони прив’язуються до даних класу, і Angular оновлює відображення, коли ці дані змінюються.
input() (або input.required()) для передачі даних вниз та output() для надсилання подій вгору; читайте дані, викликаючи їх як сигнали.Якщо дивитися з цієї точки зору, додаток Angular — це дерево компонентів, кожен з яких відповідає за свою частину інтерфейсу та взаємодіє через чітко визначені вхідні та вихідні дані. Саме ця структура дозволяє зберігати зрозумілість великих додатків Angular під час їх розширення.