Компоненты Angular с нуля: селекторы, шаблоны, входные данные, выходные данные
Практическое руководство по современным автономным компонентам Angular: структура файлов, декораторы, селекторы, шаблоны, ограниченные стили, а также передача данных между родительским и дочерним компонентами с использованием функций input() и output().
Как только проект Angular создан и запущен, следующий вопрос заключается в том, как формируется сам экран. Ответ — компоненты: небольшие, автономные единицы, каждая из которых отвечает за определенную часть интерфейса. В этом руководстве рассматривается, из чего состоит компонент, как стандартные компоненты отличаются от более старого кода, основанного на NgModule, и как родительский и дочерний компоненты обмениваются данными и событиями с использованием современных API input() и output(). К концу вы сможете создавать компоненты, встраивать их в страницу и передавать информацию в обоих направлениях.
Если вам все еще нужен проект для экспериментов, пошаговая инструкция по настройке современного проекта Angular 22 описывает настройку и структуру папок, которые предполагаются в этой статье.
Почему Angular разделяет интерфейс на компоненты
Представьте обычную веб-страницу: навигационную панель сверху, боковую панель, контент продукта посередине и футер снизу. Всю эту разметку, стили и поведение можно было бы поместить в один огромный файл, но тогда он быстро стал бы сложным для чтения и ещё сложнее — для изменений. 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 может использоваться в его шаблоне. Отсутствие этого импорта — одна из наиболее частых причин появления ошибки «элемент неизвестен» в автономных приложениях:
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 по мере их роста.