angular
April 17

Использование standalone-компонентов в Angular

В Angular 14 был представлен альтернативный способ написания приложений — standalone (автономные) компоненты, директивы и пайпы. Такие компоненты могут использоваться независимо от NgModule.

Для наглядности попробуем создать свое тестовое приложение:

npm init @angular ng17

Следующим шагом является удаление файла app.module.ts и замена функции bootstrapModule() в main.ts на bootstrapApplication():

import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

bootstrapApplication(AppComponent)

Функция bootstrapApplication()может принимать список провайдеров, которые потребуются для корневому компоненту, а так же всем дочерним:

import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { HttpClientModule } from '@angular/common/http'

bootstrapApplication(AppComponent, {
  providers: [importProvidersFrom(HttpClientModule)]
}).catch(err => console.error(err));

Функция importProviderstFrom извлекает провайдеры из модуля.

Далее доработаем AppComponent, чтобы он стал автономным. Просто добавим флаг standalone со значением true:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  standalone: true,
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}

Сделав компонент автономным, у разработчика появляется возможность использовать новое свойство imports (аналогично NgModule).

Одни автономные компоненты могут импортировать другие автономные компоненты, директивы, пайпы, а так же NgModules.

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

npx ng g directive foo --standalone
import { Directive } from '@angular/core';

@Directive({
  selector: '[appFoo]',
  standalone: true
})
export class FooDirective {}
import { CommonModule } from '@angular/common';
import { FooDirective } from './foo.directive';

@Component({
  selector: 'app-root',
  template: `
     <div appFoo *ngIf="bar">Foo</div>
  `,
  standalone: true,
  imports: [FooDirective, CommonModule]
})
export class AppComponent {}

Далее добавим маршрутизацию к приложению. Возможно вы захотите добавить её в AppComponent:

const routes: Routes = [{
  path: 'todos',
  component: TodosPageComponent
}]

@Component({
  selector: 'app-root',
  template: `
     <a routerLink="/todos">Todos</a>
     <router-outlet></router-outlet>
  `,
  standalone: true,
  imports: [RouterModule.forRoot(routes)],
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}

К сожалению, это невозможно, потому что Angular не позволяет использовать ModuleWithProvider в автономном компоненте. Для этого можно использовать новую функцию importProvidersFrom в провайдерах компонента:

const routes: Routes = [{
  path: 'todos',
  component: TodosPageComponent
}]

@Component({
  selector: 'app-root',
  template: `
     <a routerLink="/todos">Todos</a>
     <router-outlet></router-outlet>
  `,
  standalone: true,
  providers: importProvidersFrom(RouterModule.forRoot(routes)),
  imports: [FooDirective, CommonModule],
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}

Использование навигации в приложении будет работать; однако маршрутизатор пропустит первый переход. Инициализация маршрутизатора должна выполняться в процессе запуска:

bootstrapApplication(AppComponent, {
  providers: [importProvidersFrom(RouterModule.forRoot(routes))]
}).catch(err => console.error(err));

Далее реализуем ленивую загрузку:

import { Routes } from '@angular/router';

export const todosRoutes: Routes = [
  {
    path: 'todos',
    title: 'Todos Page',
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./todos-page.component').then((m) => m.TodosPageComponent),
        children: [
          {
            path: ':id',
            loadComponent: () =>
              import('./todo-page/todo-page.component').then(
                (m) => m.TodoPageComponent
              ),
          },
        ],
      },
    ],
  },
];

Вместо использования loadChildren и передачи NgModule используется свойство loadComponent. Также можно объявить провайдеры для этого маршрута и его дочерних элементов, используя новое свойство providers:

import { Routes } from '@angular/router';

export const todosRoutes: Routes = [
  {
    path: 'todos',
    title: 'Todos Page',
    providers: [
      {
        provide: 'Angular',
        useValue: 'v14',
      },
    ],
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./todos-page.component').then((m) => m.TodosPageComponent),
        children: [
          {
            path: ':id',
            loadComponent: () =>
              import('./todo-page/todo-page.component').then(
                (m) => m.TodoPageComponent
              ),
          },
        ],
      },
    ],
  },
];

... или передать массив маршрутов в loadChildren:

export const ROUTES: Route[] = [
  { path: 'child', component: ChildCmp},
]
{
  path: 'parent',
  loadChildren: () => import('./children').then(m => m.ROUTES),
}