Розбираємо standalone-компоненти у Angular

Всім привіт. Я Сергій Моренець, розробник, викладач, спікер і технічний письменник, хочу поділитися з вами досвідом використання standalone-компонентів в Angular-проєктах. Це все ще досить нова фіча, яка постійно розвивається і просувається розробниками цього фреймворку. Тому я хочу розповісти про неї більш детально і описати особливості та проблеми при міграції проєктів на standalone-варіант. Сподіваюся, що ця стаття буде корисна для всіх, хто хоче дізнатися більше про нові фічі в Angular і особливості їхнього використання.

Що таке-standalone компоненти

Standalone-компоненти (також pipes та директиви) — це не нова функціональність у Angular. Вони з’явилися ще в Angular 14 як developer preview фіча і були призначені для створення компонентів, не прив’язаних до Angular-модулів. З того часу розробники Angular вклали багато зусиль, щоб ця фіча стала стабільною. Більш того, починаючи з Angular 19, стандартні компоненти є standalone. Чому ж розробники фреймворку прагнуть запровадити такі компоненти?

З перших версій Angular модулі були природними контейнерами для компонентів, pipes, директив та сервісів. Більш того, Angular був єдиним сучасним вебфреймворком, в якому одиницею використання був не компонент, а модуль. Навіть для створення найпростішого застосунку з одного компонента потрібно було обов’язково створювати для нього модуль. Це ускладнювало дизайн застосунків, SPA (lazy-loading), читабельність коду та багато іншого. Однак в останні роки розробники Angular виступали за поступове уникнення модулів і використання їхніх елементів безпосередньо. Це було по’язано з різними причинами, наприклад, модулі неможливо було включити у процес tree-shaking. Тобто якщо якийсь модуль імпортувався, але не використовувався, він все одно потрапляв у bundle (хоча його елементи не потрапляли). Також це спрощувало розробку та тестування таких компонентів.

Були й інші причини. Angular спочатку створювався як фреймворк для створення застосунків, а не бібліотек компонентів, але поява нових більш перспективних конкурентів призвела до того, що його розробники задумалися над спрощенням свого API в плані роботи з компонентами. Ще в Angular 6 з’явилися Angular Elements (зараз вони називаються Custom Elements), які дозволяли створювати компоненти в Angular, а потім перетворювати їх на native web компоненти, які можна було використовувати будь-де.

Ще одна причина створення таких компонентів — це спроба розробників Angular розпаровувати процес збирання шляхом розбиття її на локальну збірку кожного юніту (наприклад, компонента). Так давно робиться в інших вебпроєктах (Vue, Svelte), але досі неможливо в Angular в силу монолітності всього застосунку. У той же час сучасні засоби складання (esbuild, Vite) підтримують розпаралелювання, тому відхід від модулів має допомогти у цьому вдосконаленні.

З іншого боку, модулі мають таку корисну функцію як encapsulation. Ви можете вказати в самому модулі, які елементи ви експортуєте назовні для зовнішнього використання, які є внутрішніми для самого модуля. Модулі дозволяють зробити layout елементів природним чином — розташувати їх у тих модулях, до яких вони прив’язані. Тоді як standalone-компоненти можуть створити хаос у тому випадку, коли їх сотні і тисячі в одному проєкті.

Єдиний спосіб переконатися в користі або навпаки, в марності нової фічі — це застосувати її на практиці.

Використовуємо standalone-компоненти

Для міграції ми вибрали один з невеличких проєктів, який в даний момент використовує Angular 19. У ньому кілька модулів і використовується lazy-loading для них на основі SPA. Відразу скажу, що я не описуватиму міграцію кожного компонента та елемента, оскільки це зайняло б занадто багато часу, тому виберу спрощений підхід, але розповім про типові завдання, проблеми та шляхи їх вирішення. Існує автоматична міграція, про яку я поговорю трохи пізніше, але вона не конвертує 100% коду, не застрахована від помилок, а ручна міграція дозволить пояснити відмінності standalone-елементів і різні варіанти їх використання.

Почнемо з першого модуля — UserModule:

@NgModule({
    declarations: [SettingsComponent],
    imports: [
        CommonModule,
        RouterModule.forChild(routes),
        MatButtonModule,
        MatSnackBarModule,
        TranslateModule.forChild({
            loader: {
            provide: TranslateLoader,
            useFactory: HttpLoaderFactory,
            deps: [HttpClient]
        }
    })]
})
export class UserModule {
}

...де знаходиться елемент SettingsComponent, в якому атрибут standalone встановлений в false (це робиться автоматично при переході на Angular 19):

@Component({
    selector: ’app-settings’,
    templateUrl: ’./settings.component.html’,
    standalone: false
})
export class SettingsComponent {

Спробуємо перетворити цей компонент на standalone. Почнемо з першого кроку, видаливши атрибут standalone (за умовчанням він true):

@Component({
    selector: ’app-settings’,
    templateUrl: ’./settings.component.html’,
})
export class SettingsComponent {

IDE відразу ж лається на оголошення цього компонента в модулі:

@NgModule({
    declarations: [SettingsComponent],

Component SettingsComponent is standalone, and cannot be declared in an Angular module. Did you mean to import it instead?

Тому видаляємо атрибут declarations, щоб позбавитися цієї помилки. Але справа в тому, що модуль UserModule нами використовувався не тільки як контейнер для компонентів, але і для такої фічі, як lazy-loading modules у SPA-маршрутах:

export const routes: Routes = [
{path: ’settings’, loadChildren: () => import(’./user/user.module’).then(m => m.UserModule)},

Як же тепер бути? Можна використовувати інший атрибут component, але тоді більше не буде lazy-loading:

export const routes: Routes = [
{path: ’settings’, component: SettingsComponent},

На щастя, замість атрибуту loadChildren можна використовувати альтернативу — новий атрибут loadComponent, який дозволяє завантажувати компоненти on-demand:

export const routes: Routes = [
{path: ’settings’, loadComponent: () => import(’./user/settings/settings.component’).then(c => c.SettingsComponent)},

Спробуємо перевірити роботу застосунку, запустивши його. Відразу отримуємо помилку:

[ERROR] NG8004: No pipe found with name ’translate’. [plugin angular-compiler]

src/app/user/settings/settings.component.html:2:113:

2 │ ...abled]="authenticated()">{{ ’settings.login’ | translate}}</button>

Справді, раніше ми використовували модулі для імпорту інших модулів (зокрема і TranslateModule для локалізації). А вже TranslateModule імпортував свої компоненти та pipes (включаючи TranslatePIpe). Тепер ми можемо імпортувати модулі прямо в компонентах (якщо вони standalone), тому перенесемо імпортовані модулі з UserModule в SettingsComponent:

@Component({
    selector: ’app-settings’,
    templateUrl: ’./settings.component.html’,
    imports: [
        CommonModule,
        MatButtonModule,
        MatSnackBarModule,
        TranslateModule.forChild({
            loader: {
            provide: TranslateLoader,
            useFactory: HttpLoaderFactory,
            deps: [HttpClient]
        }
    })]
})
export class SettingsComponent {

Але не для всіх модулів це можливо. IDE лається на те, що саме з TranslateModule цей фокус не пройде:

TS2322: Type ModuleWithProviders<TranslateModule> is not assignable to type readonly any[] | Type$1<any>

Проблема в тому, що функція TranslateModule.forChild не повертає модуль з компонентами, директивами та іншими елементами (наприклад, як MatButtonModule), а налаштовує конфігурацію для DI даного модуля. Тому використовувати її в атрибуті imports неможливо. Потрібно просто проімпортувати сам модуль:

imports: [
    CommonModule,
    MatButtonModule,
    MatSnackBarModule,
    TranslateModule
]

Запустимо застосунок. Виходячи з логів можна помітити, що у нас тепер є окремі chunks не тільки для модулів, але і для нового standalone-компонента:

Lazy chunk files | Names | Raw size
payment.module-KUXIIGHW.js | payment-module | 15.39 kB |
settings.component-USOBUZ37.js | settings-component | 5.80 kB |

При цьому якщо раніше ми обов’язково додавали компоненти до атрибута declarations, то тепер це робити не потрібно і фактично ми лише один раз описуємо використання SettingsComponent (у декларації маршрутів в атрибуті loadComponent).

Спробуємо зібрати застосунок та перевірити, чи змінився розмір bundle. Тепер він складає 1088 кб (а був 1097 кб). Таким чином відмова від модуля зекономила нам 9 кілобайт. Подивимося, наскільки можна оптимізувати наш компонент. Зараз у нас в imports для SettingsComponent вказані модулі, елементи яких ми використовуємо:

imports: [
    CommonModule,
    MatButtonModule,
    MatSnackBarModule,
    TranslateModule
]

Фактично ці модулі будуть імпортуватися в багатьох інших standalone-компонентах. Чи можна уникнути дублювання? Можна створити так званий shared-модуль:

@NgModule({
    declarations: [],
    imports: [
        CommonModule,
        TranslateModule
    ],
    exports: [CommonModule, TranslateModule]
})
export class SharedModule { }

Його єдина функція — це експорт інших модулів. Тому тепер ми можемо зібрати в ньому найбільш використовувані модулі та замінити їх на SharedModule:

imports: [
    MatButtonModule,
    MatSnackBarModule,
    SharedModule
]

На розмірі bundle це ніяк не позначилося. Але такий підхід не дуже вписується в нову ідеологію Angular, тобто ми замість видалення модулів створюємо нові модулі. Тому відмовляємося від SharedModule на користь імпорту модулів безпосередньо. Але ми можемо імпортувати моделі, а можемо і компоненти (директиви та pipes), якщо знаємо їх назви:

imports: [
    MatButton,
    TranslatePipe
]

На розмірі bundle це не позначилося. Але тут у багатьох може виникнути сумнів про ефективність такого підходу. Раніше ми декларували елементи на рівні модулів, а потім імпортували їх з модуля. Тепер ми повинні імпортувати їх у кожному standalone-компоненті, який їх використовує. Як це працюватиме на практиці?

  1. Скільки разів буде створюватись, наприклад, pipes, які ми використовуємо (TranslatePipe)?
  2. Чи включатиметься код standalone pipe в кожен standalone-компонент?

Перевіримо це практично. Створимо нову (поки що не standalone):

@Pipe({
    name: ’quote’,
    standalone: false
})
export class QuotePipe implements PipeTransform {
    text = ’STANDALONE_TEST’
    constructor() {
        console.log(’QuotePipe created’);
    }
    transform(value: string): string {
        return ’""’ + value + ’"’;
    }
}

Щоразу при своєму створенні вона виводитиме повідомлення в консоль. Також я додав нове поле, яке не буде мініфікуватися для того, щоб перевірити згенерований код цієї pipe (воно дозволить шукати код pipe).

Якщо ми не використовуємо standalone-елементи, то pipe створюється один раз і її код включається один раз в bundle.

Тепер переробимо все на standalone-варіант, використовуємо цю pipe в StatusComponent і двічі включимо його в темплейт:

<app-status></app-status>
<app-status></app-status>

Запускаємо застосунок. QuotePipe створюється двічі, але її код включається в bundle один раз. Тепер використовуємо QuotePipe в різних standalone-компонентах, як і раніше QuotePipe створюється для кожного використання, але включається один раз в bundle.

Ускладнимо завдання. Тепер будемо динамічно (loadComponent) завантажувати standalone-компоненти. Все залишається, як і раніше. Таким чином можна констатувати, що використання standalone-елементів призводить до створення нових об’єктів щоразу при їх використанні. Але сам код елементів включається в bundle лише один раз.

Особливості використання таблиць

Переходимо до модуля PaymentModule. Візьмемо один із компонентів PaymentsComponent, темплейт якого складніший і містить таблиці з Angular Material. Якщо ми спробуємо імпортувати кожен елемент окремо, то зіткнемося з проблемою, пов’язаною з директивами. Дуже легко пропустити директиву, а потім отримати таку помилку в runtime:

NG0303: Can’t bind to ’matRowDefColumns’ since it isn’t a known property of ’tr’ (used in the ’_PaymentsComponent’ component template).

Тут не одразу й не скажеш, що ми забули вказати. І лише детальне дослідження показує, що є директива MatRowDef, в якій вхідним параметром буде matRowDefColumns:

@Directive({
    selector: ’[matRowDef]’,
    providers: [{provide: CdkRowDef, useExisting: MatRowDef}],
    inputs: [
        {name: ’columns’, alias: ’matRowDefColumns’},
        {name: ’when’, alias: ’matRowDefWhen’},
    ],
})
export class MatRowDef<T> extends CdkRowDef<T> {}

Наступна помилка ще менш інформативна:

ERROR TypeError: column.headerCell is undefined

І лише після нової ітерації аналізу коду з’ясовується, що бракує імпорту для директиви MatHeaderCellDef. На жаль, не завжди Angular-компілятор здатний відстежувати подібні помилки, і є навіть тикет на це. В результаті список імпортів виходить досить великим:

imports: [MatTableMatColumnDef, MatHeaderCell, MatCell,
MatHeaderRow, MatRow, MatHeaderRowDef, MatCellDef, MatRowDef, MatHeaderCellDef]

Для покращення читабельності та уникнення випадкових помилок простіше замінити його на один модуль, де знаходяться всі ці компоненти та директиви:

imports: [MatTableModule]

А так виглядатиме декларація цього компоненту:

@Component({
    selector: ’app-payments’,
    templateUrl: ’./payments.component.html’,
    imports: [MatTableModule, AsyncPipe, TranslatePipe, DatePipe, NgIf]
})
export class PaymentsComponent implements OnInit {

Як ви бачите, потрібно явно імпортувати всі pipes та директиви, навіть такі як NgIf з Com-monModule. З іншого боку, це ще одна нагода перейти на control flow (@if). Розробники Angular планували автоматично імпортувати всі елементи із CommonModule, але потім відмовилися від цієї ідеї. Збираємо проєкт. Розмір bundle 1088 кб.

Робота з формами

Переходимо до TicketModule. Тут новою проблемою під час міграції стала наявність форм у компоненті CreateTicketComponent:

<form [formGroup]="ticketForm«>

Якщо ми спробуємо імпортувати директиву FormGroupDirective, то отримаємо помилку:

Directive FormGroupDirective is not standalone and cannot be imported directly. It must be imported via an NgModule.

На це є тикет, який поки що завис у повітрі. Тому іншого виходу немає, потрібно імпортувати тільки весь модуль ReactiveFormsModule повністю.

Ось як виглядає декларація CreateTicketComponent:

@Component({
    selector: ’app-create-ticket’,
    templateUrl: ’./create-ticket.component.html’,
    imports: [TranslatePipe, MatSelect, MatFormField, MatOption, MatLabel, MatButton, MatDialogActions,
        MatDialogClose, ReactiveFormsModule, MatDialogTitle, MatDialogContent],
    providers: [PaymentProviderService]
})
export class CreateTicketComponent {

Далі переходимо до компонента CreatedOrderComponent:

@Component({
    selector: ’app-create-order’,
    templateUrl: ’./create-order.component.html’,
    imports: [ReactiveFormsModule, MatButton, MatFormField, MatDatepickerModule, TranslatePipe, MatDialogModule, MatError,                 MatInputModule]
})
export class CreateOrderComponent {

Оскільки ми його створюємо динамічно:

openOrderDialog(routeId: string): void {
const dialogRef = this._dialog.open(CreateOrderComponent);

То його ні декларувати, ні імпортувати ніде не треба. Ось як виглядає декларація TripsComponent:

@Component({
    selector: ’app-trips’,
    templateUrl: ’./trips.component.html’,
    imports: [MatTableModule, MatButton, ReactiveFormsModule, TranslatePipe, AsyncPipe, CitySelectionComponent, NgIf],
    providers: [RouteService]
})
export class TripsComponent implements OnInit {

Зверніть увагу, що ми вперше використовували атрибут providers, вказавши сервіс RouteService, який використовується тільки в цьому компоненті. Також ми вперше проімпортували власний компонент (CitySelectionComponent), який використовується всередині поточного компонента. Раніше він лише оголошувався у поточному модулі (атрибут declarations).

Ну і насамкінець переробляємо головний компонент AppComponent:

@Component({
    selector: ’app-root’,
    templateUrl: ’./app.component.html’,
    imports: [HeaderComponent, RouterOutlet, MatTabsModule, TranslatePipe, RouterLink, RouterLinkActive]
})
export class AppComponent {

Але тепер наш застосунок перестає запускатися, виводячи помилку:

Component AppComponent is standalone and cannot be used in the @NgModule.bootstrap array. Use the bootstrapApplication function for bootstrap instead

Тобто використовувати звичний механізм запуску, заснований на модулях, вже не вдасться:

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));

Конфігурація запуску застосунку

Замість функції platformBrowserDynamic було додано нову функцію bootstrapApplication. Але поки що ми її використати не можемо. У нас є модуль CoreModule, в якому оголошуються багато хто з providers нашого застосунку:

@NgModule({
    imports: [
        CommonModule
    ]
})
export class CoreModule {
    static forRoot(inMemory: boolean): ModuleWithProviders<CoreModule> {
        return {
            ngModule: CoreModule,
            providers: AuthenticationService, 
            {provide: HTTP_INTERCEPTORS, useClass: SecurityInterceptor, multi: true}, OrderService, PaymentService, LocalizationService]
        };
    }
}

І тут дуже наочно видно подвійну природу Angular-модулів. З одного боку це контейнер для UI-елементів, з іншого боку це контейнер/конфігурація для DI-сервісів/провайдерів. UI елементи ми тепер вказуємо безпосередньо в компонентах. А ось providers потрібно тепер вказувати під час bootstrapping програми. Оскільки ми твердо вирішили позбутися всіх модулів, що робити з CoreModule? Просто замінимо його константою coreProviders:

const coreProviders = [AuthenticationService,
    {provide: HTTP_INTERCEPTORS, useClass: SecurityInterceptor, multi: true},
    OrderService, PaymentService, LocalizationService];

Далі у нас у AppModule йде імпорт JwtModule, який потрібен для роботи з JWT-токенами:

JwtModule.forRoot({
    config: {
        tokenGetter: jwtLoader
    }
}),

Бібліотека @auth0/angular-jwt давно не оновлювалася та не підтримує новий moduleless-підхід. Тому в таких випадках для імпорту провайдерів є нова функція importProvidersFrom, яка таки призначена для роботи з legacy-кодом:

importProvidersFrom(JwtModule.forRoot({
    config: {
        tokenGetter: jwtLoader
    }
})),

А ось бібліотека ngx-translate якраз почала підтримувати застосунки без модулів, тому тут є вбудована функція provideTranslateService для імпорту провайдерів:

provideTranslateService({
    loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient],
    }
}),

Модулі можуть в окремих випадках використовуватися для ініціалізації сервісів. Зазвичай сервіси ініціалізуються під час DI в компоненти (або інші елементи). Але є сервіси, які не прив’язані до UI або їх потрібно ініціалізувати під час завантаження застосунку. У нас це сервіс для роботи з HATEOAS:

export class AppModule {
    constructor(hateoasConfig: NgxHateoasClientConfigurationService) {
        hateoasConfig.configure({
            http: {
                rootUrl: `${BASE_API_URL} `
            },
            useTypes: {resources: [Order]}
        });
    }
}

Чим замінити код, якщо модулів більше немає? У Angular 14 додали спеціальну конструкцію для такого випадку:

providers: [
    {
        provide: ENVIRONMENT_INITIALIZER,
        multi: true,
        useValue: () => inject(LocaleService).load()
}

... яка в цьому випадку налаштовує LocaleService. Але потім розробники Angular вирішили відмовитися від такої ідеї, оголосили попередній підхід застарілим (deprecated) і додали спеціальну функцію provideEnvironmentInitializer:

provideEnvironmentInitializer(() => {
    const hateoasConfig = inject(NgxHateoasClientConfigurationService);
    hateoasConfig.configure({
        http: {
            rootUrl: `${BASE_API_URL} `
        },
        useTypes: {resources: [Order]}
    });
})

Всі providers потрібно вказувати в новому типі ApplicationConfig, який у нашому випадку цілком виглядає так (включаючи з константою coreProviders):

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        provideAnimationsAsync(),
        provideHttpClient(withInterceptorsFromDi()),
        ...coreProviders,
        importProvidersFrom(JwtModule.forRoot({
            config: {
                tokenGetter: jwtLoader
            }
        })),
        provideTranslateService({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient],
            }
        }),
        provideEnvironmentInitializer(() => {
            const hateoasConfig = inject(NgxHateoasClientConfigurationService);
            hateoasConfig.configure({
                http: {
                    rootUrl: `${BASE_API_URL} `
                },
                useTypes: {resources: [Order]}
            });
        })
    ]
};
export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}

А ось так виглядає завантаження застосунку:

bootstrapApplication(AppComponent, appConfig)
    .catch(err => console.error(err));

Збираємо застосунок, жодних помилок не зафіксовано. Вимірюємо продуктивність:

  • Час production збирання — 5.1 сек.
  • Розмір bundle — 1077 кб
  • Час локального запуску — 2.0 сек.

Таким чином ми скоротили bundle на 20 кб, час збирання на 10%, час локального запуску на 15%, хоча покращення продуктивності ніколи не прив’язувалося до такої фічі, як standalone-компоненти.

Ось тепер можна видалити всі модулі.

Обмеження та налаштування

У новій концепції розробки компонентів не все так гладко і насправді тут все ще є низка обмежень:

  • Неможливо вказати кілька компонентів для процесу bootstrapping.
  • Якщо ви вже маєте готові standalone-компоненти, які ви відкомпілювали у версіях <Angular 19, то цілком можливо, що ви не зможете їх використовувати в Angular 19.

Крім того, для standalone-компонентів була додана нова опція unusedStandaloneImports в налаштуваннях діагностики:

{
    «angularCompilerOptions»: {
        «extendedDiagnostics»: {
            «checks»: {
                «unusedStandaloneImports»: «suppress»
            }
        }
    }
}

Вона дозволяє визначити ті компоненти, які ви імпортуєте, але не використовуєте у темплейтах. На жаль, але вона не завжди працює правильно.

І ще одна проблема, яка специфічна для нової функціональності — тепер ви не можете використовувати standalone-компоненти, якщо вони циклічно посилаються один на одного. Такої проблеми не було з модулями, але, на щастя, є workaround — функція forwardRef:

imports: [
    forwardRef(() => MyStandaloneComponent))
]

Автоматична міграція

Поговоримо про автоматичну міграцію на standalone-елементи. Як я й казав, вона не може конвертувати 100% вашого коду (особливо це стосується директив), але може стати помічником у міграції, якщо у вас у проєкті сотні компонентів та ручна міграція практично неможлива. Також можливі випадки, коли вона просто не працюватиме.

Отже, для автоматичної міграції є команда ng generate @angular/core:standalone. Коли ви її запускаєте, то вона вам пропонує вибрати тип (стадію міграції):

? Choose the type of migration (Use arrow keys)
> Convert all components, directives and pipes to standalone
Remove unnecessary NgModule classes
Bootstrap the application using standalone APIs

Тобто спочатку ви конвертуєте ваші елементи, потім видаляєте модулі і в кінці змінюєте тип завантаження застосунку. У документації дається порада не робити це з усім проєктом відразу, а розбити на безліч етапів, після кожного з яких ви перевіряєте працездатність застосунку. Це теоретично можна зробити, вибравши шлях, яким слід розпочати міграцію (за умовчанням це весь проєкт):

? Which path in your project should be migrated? ./

На жаль, але мені так і не вдалося вибрати якусь частину проєкту, я завжди отримував помилку:

Could not find any files to migrate under the path src/app/about/. Cannot run the standalone migration.

Тому в цьому випадку я мав можливість лише конвертувати весь проєкт. Але без гарного покриття коду тестами ви це робитимете на свій страх і ризик.

Висновки

Чи варто переводити всі свої компоненти в категорію standalone? Насамперед це залежить від того, чи будуть у майбутньому підтримуватися звичайні компоненти і модулі. Ще у 2023 році всі елементи з Angular Material/CDK були конвертовані у standalone.

Більше того, є деякі фічі, де можна використовувати тільки standalone-компоненти:

  • deferrable views.
  • не можна використовувати не-standalone директиви як host directives у компонентах.

В офіційній документації по Angular ніде не говориться, що модулі deprecated, тут використовуються більш завуальовані тези: модулі опціональні, рекомендується перейти на standalone-компоненти. Припинення підтримки модулів зараз неможливе, оскільки є величезна кількість open-source бібліотек і проєктів, які широко використовують модулі і навряд чи хтось їх моментально переписуватиме.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось8
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Головна killer-фіча

standalone

— це можливість легко збирати різні в’юхи з самодостатніх UI-компонентів у будь-якому місці додатку. Коли все побудовано на standalone-компонентах, вам не потрібно тягнути модулі, думати про forRoot, прокидувати інпути а в середені все обмазувати *ngIf.

Наприклад, на сторінці А картка має виглядати одним чином, а на сторінці Б — інакше. Ви просто збираєте з різних UI-компонентів те, що потрібно саме в цьому контексті.

Саме для цього їх і запровадили.

До чого Azure лого на пікчі в статті про ангуляр?

Це питання не до мене. Preview-картинки до статті вибирають редактори на DOU

Ми випадково помилились під час створення обкладинки, вже виправили. Дякую за уважність.

Щодо імпорту MatTableModule.
Я не проводив експериментів з розміром бандла, але прийшов до того ж висновку щодо зручності і створив константу-масив і імпортував вже її. Аналогічний підхід використав для всіх імпортів, пов’язаних з MatAccordion, MatFormField

Хоча я з цим працюю кожен день, але відчуття наче в ангулярі все дуже складно. Той же vue3 виглядає набагато простіше. З іншого боку з коробки дуже багато налаштувань. До речі unusedImports погано працюють з імпортованими директивами та модулями

У Angular дуже багато чого змінюється в останні роки, і змінюється на краще.
Це і сигнали, і standalone елементи, і control block flow, і ось скоро зроблять zoneless додатки.

Начебто zoneless вже є? Чи це unstable?

Дякую за статтю, дуже корисно!

Підписатись на коментарі