Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Окремі компоненти в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Standalone компонент** в Angular (14+) - це компонент з `standalone: true` у декораторі, який управляє своїми залежностями через масив `imports` в `@Component`, без оголошення в `NgModule`. ```typescript @Component({ selector: 'app-card', standalone: true, imports: [NgIf], template: `<p *ngIf="title">{{ title }}</p>` }) export class CardComponent { @Input() title = ''; } ``` **Головне:** інші компоненти йдуть в `imports`, а не в `declarations`. Для запуску standalone-додатку - `bootstrapApplication()`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Standalone компонент** - це компонент, який оголошує свої залежності прямо в `@Component`, без потреби в `NgModule`. Доступний з Angular 14, генерується за замовчуванням з Angular 17. ## Теорія ### TL;DR - Встановлюєш `standalone: true` в `@Component` і перелічуєш усі залежності шаблону в масиві `imports` компонента - До Angular 14 кожен компонент мав жити в `NgModule` - standalone прибирає цей шар - Запуск змінюється: `bootstrapApplication()` замість `platformBrowserDynamic().bootstrapModule()` - Ліниве завантаження (lazy loading) стає коротшим: `loadComponent` завантажує один компонент замість `loadChildren` з модулем - Директиви та пайпи підтримують `standalone: true` так само, як компоненти ### Швидкий приклад ```typescript @Component({ selector: 'app-user-card', standalone: true, imports: [CommonModule, RouterLink], // залежності прямо тут, без NgModule template: ` <div *ngIf="user"> <h2>{{ user.name }}</h2> <a [routerLink]="['/profile', user.id]">Профіль</a> </div> ` }) export class UserCardComponent { @Input() user?: { id: number; name: string }; } ``` `CommonModule` і `RouterLink` імпортуються прямо в компонент. Жодного файлу модуля, жодного масиву `declarations`. Компонент сам знає, що йому потрібно. ### NgModule проти standalone У модульному Angular кожен компонент має модуль, який його оголошує. Той модуль також імпортує все необхідне для компонента (`CommonModule`, `FormsModule` тощо) і реекспортує компонент, щоб інші модулі могли його використовувати. Три файли для простої фічі. Standalone прибирає цей шар. Масив `imports` в `@Component` працює так само, як в `@NgModule`, але охоплює тільки один компонент. Коли компілятор розв'язує прив'язки в шаблоні, він читає цей масив напряму, а не шукає батьківський модуль. ### Коли використовувати standalone - Новий Angular-проект: CLI з Angular 17+ генерує standalone за замовчуванням - Будь-який новий компонент в існуючому додатку: standalone і модульний код вільно поєднуються - Ліниве завантаження одного компонента: `loadComponent` без обгортки у вигляді feature-модуля - Бібліотеки компонентів: менше файлів, чіткіший граф залежностей Мігрувати існуючий проект можна через `ng generate @angular/core:standalone`. ### Запуск додатку без NgModule ```typescript // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; import { routes } from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), ], }); ``` `AppModule` зник. Провайдери переїхали до другого аргументу `bootstrapApplication`. Кожна функція `provide*` замінює те, що раніше було імпортом модуля. Особисто я знайшов такий підхід значно зручнішим для читання на code review, ніж пошук по масиву `imports` модуля. ### Ліниве завантаження одного компонента ```typescript export const routes: Routes = [ { path: 'settings', loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent), }, ]; ``` Раніше для лінивого маршруту потрібен був окремий `NgModule` з модулем маршрутизації всередині. Тепер сам компонент є межею лінивого завантаження. Менший бандл-сплит, менше коду. ### Як компілятор обробляє standalone Коли Angular бачить `standalone: true`, він вважає масив `imports` компонента єдиним джерелом залежностей для шаблону. Кожна директива, пайп або компонент, що використовуються в шаблоні, мають бути в цьому списку або в модулі зі списку. Відсутній імпорт дає помилку під час компіляції (compile error), а не в рантаймі. Це конкретне покращення порівняно зі старою системою модулів, де забутий реекспорт у спільному модулі ламав все тільки під час запуску. ### Типові помилки **Забули `standalone: true`, але прибрали NgModule** ```typescript // Не скомпілюється @Component({ selector: 'app-header', // standalone: true відсутній imports: [CommonModule], template: `<nav>...</nav>` }) export class HeaderComponent {} ``` Angular видає: `Component 'HeaderComponent' is not a part of any NgModule`. Вирішується одним рядком. **`declarations` в standalone-компоненті** ```typescript // Неправильно - declarations не існує в standalone @Component({ standalone: true, declarations: [ChildComponent], // цієї властивості тут немає }) export class ParentComponent {} ``` Standalone-компоненти використовують `imports` для всього. `declarations` - це концепція модуля. **Занадто широкий імпорт** ```typescript // Компілюється, але тягне весь CommonModule imports: [CommonModule] // Точніше - бандлер знає, що можна прибрати imports: [NgIf, NgFor, AsyncPipe] ``` Обидва варіанти коректні. Другий дає tree-shaking більше інформації про те, що можна видалити з фінального бандлу. **Неправильний метод запуску після видалення AppModule** Якщо видалити `AppModule`, але залишити `platformBrowserDynamic().bootstrapModule(AppModule)` в `main.ts`, додаток не запуститься. Потрібно перейти на `bootstrapApplication(AppComponent, { providers: [...] })`. ### Де standalone зустрічається в реальних проектах - Angular 17+ CLI: всі згенеровані компоненти - standalone за замовчуванням - Angular Material 15+: всі компоненти бібліотеки постачаються як standalone - Nx workspace: standalone за замовчуванням для Angular-бібліотек - Офіційна документація Angular і приклад tour-of-heroes: повністю standalone з Angular 17 ### Питання на співбесіді **Q:** Чи можна використовувати standalone-компонент у модульному додатку? **A:** Так. Його додають в масив `imports` `NgModule`, а не в `declarations`. Компонент сам управляє своїми залежностями, модулю потрібно лише його перелічити. **Q:** Чи може немодульний компонент використовувати standalone? **A:** Через модуль - так. Standalone-компонент імпортується у feature-модуль, і той модуль може імпортуватися іншими. Працює, але чистіший шлях - мігрувати батьківський компонент теж. **Q:** Що станеться, якщо додати немодульний компонент в `imports` NgModule? **A:** Angular кине помилку компіляції з вимогою спочатку додати `standalone: true`. Тихої помилки не буде. **Q:** Чи обов'язковий `standalone: true` в Angular 17+? **A:** Ні. Модульний код все ще компілюється і працює. CLI генерує standalone за замовчуванням, але можна вибрати інший шлях. Angular-команда планує зробити standalone основним API у майбутній версії, але дати deprecation для NgModules поки немає. **Q:** В чому різниця між `loadComponent` і `loadChildren` в маршрутизації? **A:** `loadChildren` завантажує модуль або масив standalone-маршрутів. `loadComponent` завантажує один конкретний компонент як точку входу маршруту. Для feature-областей з кількома вкладеними маршрутами використовують `loadChildren` з файлом standalone-маршрутів. ## Приклади ### Реактивна форма в standalone-компоненті ```typescript import { Component, inject } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { NgIf } from '@angular/common'; @Component({ selector: 'app-login-form', standalone: true, imports: [ReactiveFormsModule, NgIf], template: ` <form [formGroup]="form" (ngSubmit)="submit()"> <input formControlName="email" type="email" placeholder="Email" /> <span *ngIf="form.get('email')?.touched && form.get('email')?.invalid"> Введіть коректний email </span> <button type="submit" [disabled]="form.invalid">Увійти</button> </form> ` }) export class LoginFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ email: ['', [Validators.required, Validators.email]], }); submit() { if (this.form.valid) { console.log(this.form.value); } } } ``` `ReactiveFormsModule` імпортується на рівні компонента. Жодного `AppModule`, жодного спільного модуля форм десь вище по дереву. Компонент несе свої залежності з собою. ### Поєднання standalone з існуючим NgModule ```typescript // Старий feature-модуль, який додає новий standalone-компонент @NgModule({ imports: [ CommonModule, UserCardComponent, // standalone - іде в imports, не в declarations ], declarations: [ LegacyDashboardComponent, // ще не мігрований ], exports: [LegacyDashboardComponent], }) export class LegacyDashboardModule {} ``` Standalone-компоненти йдуть в `imports`. Звичайні компоненти залишаються в `declarations`. Ось як виглядає покрокова міграція без переписування всього дерева модулів одразу. ### Standalone-директива в компоненті ```typescript import { Directive, ElementRef, inject, OnInit } from '@angular/core'; @Directive({ selector: '[appAutoFocus]', standalone: true, }) export class AutoFocusDirective implements OnInit { private el = inject(ElementRef); ngOnInit() { this.el.nativeElement.focus(); } } @Component({ selector: 'app-search', standalone: true, imports: [AutoFocusDirective], template: `<input appAutoFocus type="search" placeholder="Пошук..." />` }) export class SearchComponent {} ``` `AutoFocusDirective` теж standalone, тому йде прямо в `imports` компонента. Жодного модуля-посередника. Той самий патерн працює і для standalone-пайпів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.