Standalone components en Angular

En el artículo de hoy estaremos hablando un poco de la Introducción a los componentes autónomos (Standalone Components) y cual es el impacto, todas esas novedades que están en este momento en gran auge.

· 8 min de lectura
Standalone components en Angular

⚠️ ATENCIÓN señores, antes de comenzar a leer este post es importante que te pongas cómodo ya que en este blog cuando hablamos de angular no podemos hacerlo brevemente, aquí explicamos a detalles, pero te aseguro que una vez termines la lectura tus conocimientos estarán reforzados, así que sin más anuncios vamos a darle. 💪

Los debates en torno a los Standalone Components llevan varios meses en la comunidad. Igor Minar, uno de los desarrolladores clave detrás de Angular, dijo que había querido tratar con  NgModule desde la primera versión beta de Angular.

Esto fue en 2016. Así que fue todo un acontecimiento cuando Pawel Kozlowski publicó la RFC oficial para Standalone Components en GitHub.

En la versión 14 y superiores, los componentes independientes proporcionan una forma simplificada de crear aplicaciones Angular. Los componentes independientes, las directivas y los pipes tienen como objetivo agilizar la experiencia de creación reduciendo la necesidad de  NgModule .

Las aplicaciones existentes pueden adoptar el nuevo estilo autónomo de forma opcional e incremental sin necesidad de realizar cambios.

¿Qué son los componentes autónomos?

El elemento clave en Angular es el componente. Cada componente pertenece a un NgModule que le proporciona las dependencias. Las declaraciones de propiedades del decorador de un NgModule crean esta relación.

Por ejemplo, si el componente requiere la propiedad formGroup el NgModule proporciona esa directiva a través de la directiva ReactiveFormsModule.

Creación de componentes independientes (Standalone components)

El standalone flag y la importación de componentes
Ahora los componentes, las directivas y los pipes pueden marcarse como standalone: true Las clases de Angular marcadas como independientes no necesitan ser declaradas en un NgModule (el compilador de Angular informará de un error si lo intentas).

Los componentes autónomos especifican sus dependencias directamente en lugar de obtenerlas a través de NgModule.  Por ejemplo, si PhotoGalleryComponent es un Standalone Components, puedes importar directamente otro componente independiente ImageGridComponent:

@Component({
  standalone: true,
  selector: 'photo-gallery',
  imports: [ImageGridComponent],
  template: `
    ... <image-grid [images]="imageList"></image-grid>
  `,
})
export class PhotoGalleryComponent {
  // component logic
}

imports también puede utilizarse para referenciar directivas y pipes independientes. De este modo, se pueden escribir componentes independientes sin necesidad de crear un NgModule

Uso de NgModules existentes en un componente independiente

Al escribir un componente independiente, es posible que quieras utilizar otros componentes, directivas o pipes en la plantilla del componente. Algunas de esas dependencias pueden no estar marcadas como independientes, sino declaradas y exportadas por un NgModule  En este caso, puedes importar el  NgModule directamente en el componente autónomo:

@Component({
  standalone: true,
  selector: 'photo-gallery',
  // an existing module is imported directly into a standalone component
  imports: [MatButtonModule],
  template: `
    ...
    <button mat-button>Next Page</button>
  `,
})
export class PhotoGalleryComponent {
  // logic
}

Puede utilizar componentes autónomos con NgModule  basado en bibliotecas o dependencias en su plantilla. Los componentes independientes pueden aprovechar al máximo el ecosistema existente de bibliotecas de Angular.

Si llegados a este punto tu quieres esta explicación en un video, te dejo el de un amigo excepcional como lo es Nicolas Arrieta.

Uso de componentes independientes en aplicaciones basadas en NgModule

Los componentes independientes también pueden importarse a contextos existentes basados en NgModule . Esto permite que las aplicaciones existentes (que usan NgModule hoy en día) adopten gradualmente el nuevo estilo de componente independiente.

Puedes importar un componente independiente (o una directiva, o un pipe) igual que lo haría con un NgModule- using

@NgModule({
  declarations: [AlbumComponent],
  exports: [AlbumComponent], 
  imports: [PhotoGalleryComponent],
})
export class AlbumModule {}

Arrancar una aplicación con un componente independiente

Una aplicación Angular puede ser arrancada sin ningún tipo de  NgModule utilizando un componente independiente como componente raíz de la aplicación. Para ello se utiliza la función bootstrapApplication API:

// in the main.ts file
import {bootstrapApplication} from '@angular/platform-browser';
import {PhotoAppComponent} from './app/photo.app.component';

bootstrapApplication(PhotoAppComponent);

Configuración de la inyección de dependencias

Al arrancar una aplicación, a menudo quieres configurar la inyección de dependencias de Angular y proporcionar valores de configuración o servicios para su uso en toda la aplicación. Puedes pasarlos como proveedores a  bootstrapApplication

bootstrapApplication(PhotoAppComponent, {
  providers: [
    {provide: BACKEND_URL, useValue: 'https://photoapp.looknongmodules.com/api'},
    // ...
  ]
});

La operación de arranque autónomo se basa en la configuración explícita de una lista de Provider  para la inyección de dependencias. Sin embargo, las bibliotecas existentes pueden depender de NgModule para configurar DI. Por ejemplo, el enrutador de Angular utiliza el RouterModule.forRoot()

Para configurar el enrutamiento en una aplicación Puedes utilizar estos NgModule en  bootstrapApplication  a través de importProvidersFrom utilidad:

bootstrapApplication(PhotoAppComponent, {
  providers: [
    {provide: BACKEND_URL, useValue: 'https://photoapp.looknongmodules.com/api'},
    importProvidersFrom(
      RouterModule.forRoot([/* app routes */]),
    ),
    // ...
  ]
});

Enrutamiento y lazy-loading

Las APIs del router fueron actualizadas y simplificadas para aprovechar los componentes independientes: un NgModule  ya no es necesario en muchos escenarios comunes de carga lenta.

Carga perezosa de un componente independiente

Cualquier ruta puede cargar perezosamente su componente independiente enrutado utilizando loadComponent:

export const ROUTES: Route[] = [
  {path: 'admin', loadComponent: () => import('./admin/panel.component').then(mod => mod.AdminPanelComponent)},
  // ...
];

Esto funciona siempre que el componente cargado sea independiente.

Carga perezosa de muchas rutas a la vez

El loadChildren ahora admite la carga de un nuevo conjunto de archivos hijo Route sin necesidad de escribir un lazy loaded NgModule  que importa RouterModule.forChild  para declarar las rutas.

Esto funciona cuando cada ruta cargada de esta manera está utilizando un componente independiente.
// In the main application:
export const ROUTES: Route[] = [
  {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
  // ...
];

// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [
  {path: 'home', component: AdminHomeComponent},
  {path: 'users', component: AdminUsersComponent},
  // ...
];

Prestación de servicios a un subconjunto de rutas

La API de carga lenta para  NgModule  (loadChildren) crea un nuevo inyector de "módulo" cuando carga los hijos cargados perezosamente de una ruta.

Esta característica suele ser útil para proporcionar servicios sólo a un subconjunto de rutas en la aplicación.

Por ejemplo, si todas las rutas bajo r /admin se han delimitado mediante un loadChildren límite, entonces los servicios de administración sólo podrían proporcionarse a esas rutas. Para ello era necesario utilizar el loadChildren API, incluso si la carga perezosa de las rutas en cuestión era innecesaria.

El router permite ahora especificar explícitamente otros providers en un Route que permite este mismo alcance sin la necesidad de la carga perezosa o  NgModule  

Por ejemplo, los servicios de alcance dentro de una estructura de ruta /admin tendrían el siguiente aspecto:

export const ROUTES: Route[] = [
  {
    path: 'admin',
    providers: [
      AdminService,
      {provide: ADMIN_API_KEY, useValue: '12345'},
    ],
    children: [
      path: 'users', component: AdminUsersComponent,
      path: 'teams', component: AdminTeamsComponent,
    ],
  },
  // ... other application routes that don't
  //     have access to ADMIN_API_KEY or AdminService.
];

También es posible combinar providers con loadChildren  de configuración de enrutamiento adicional, para lograr el mismo efecto de la carga perezosa de un NgModule  con rutas adicionales y proveedores a nivel de ruta.

Este ejemplo configura los mismos proveedores/rutas hijas que el anterior, pero detrás de un límite de carga perezosa:

// Main application:
export const ROUTES: Route[] = {
  // Lazy-load the admin routes.
  {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
  // ... rest of the routes
}

// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [{
  path: '',
  pathMatch: 'prefix',
  providers: [
    AdminService,
    {provide: ADMIN_API_KEY, useValue: 12345},
  ],
  children: [
    {path: 'users', component: AdminUsersCmp},
    {path: 'teams', component: AdminTeamsCmp},
  ],
}];

Obsérvese el uso de una ruta vacía hacia el host providers que se comparten entre todas las rutas hijas.

Temas avanzados

Esta sección entra en más detalles que son relevantes sólo para patrones de uso más avanzados. Puedes omitir esta sección si es la primera vez que aprendes sobre componentes independientes, directivas y pipes. Sin embargo; yo te recomiendo que le des un vistazo.

Componentes independientes para autores de bibliotecas

Los componentes independientes, las directivas y los pipes pueden exportarse desde NgModule  que los importan:

@NgModule({
  imports: [ImageCarouselComponent, ImageSlideComponent],
  exports: [ImageCarouselComponent, ImageSlideComponent],
})
export class CarouselModule {}

Este patrón es útil para las bibliotecas de Angular que publican un conjunto de directivas que cooperan. En el ejemplo anterior, tanto la directiva ImageCarouselComponent  y ImageSlideComponent deben estar presentes en una plantilla para construir un "widget de carrusel" lógico.

Como alternativa a la publicación de un NgModule  los autores de las bibliotecas pueden querer exportar un conjunto de directivas cooperantes:

export CAROUSEL_DIRECTIVES = [ImageCarouselComponent, ImageSlideComponent] as const;

Esta matriz podría ser importada por las aplicaciones que utilizan  NgModule  y se añade a la @NgModule.imports.

Ten en cuenta la presencia de la función de TypeScript as const ya que proporciona al compilador de Angular información adicional necesaria para una correcta compilación y es una práctica recomendada (ya que hace que el array exportado sea inmutable desde el punto de vista de TypeScript).

Inyección de dependencia y jerarquía de inyectores

Las aplicaciones de Angular pueden configurar la inyección de dependencias especificando un conjunto de proveedores disponibles. En una aplicación típica, hay dos tipos de inyectores diferentes:

  • Inyector de módulos con proveedores configurados en @NgModule.providers o @Injectable({providedIn: "..."}). Estos proveedores para toda la aplicación son visibles para todos los componentes, así como para otros servicios configurados en un inyector de módulos.
  • Node injectors configurados en @Directive.providers / @Component.providers o @Component.viewProviders. Estos proveedores son visibles sólo para un componente determinado y todos sus hijos.

Inyectores ambientales

Haciendo  NgModule   opcional requerirá nuevas formas de configurar los inyectores de "módulos" con proveedores de toda la aplicación (for example, HttpClient) En la aplicación independiente (uno creado con bootstrapApplication).

Los proveedores de "módulos" pueden configurarse durante el proceso de arranque, en el providers opción:

bootstrapApplication(PhotoAppComponent, {
  providers: [
    {provide: BACKEND_URL, useValue: 'https://photoapp.looknongmodules.com/api'},
    {provide: PhotosService, useClass: PhotosService},
    // ...
  ]
});

La nueva API de bootstrap nos devuelve la posibilidad de configurar "inyectores de módulos" sin necesidad de utilizar NgModules. En este sentido, la parte "módulo" del nombre ya no es relevante y hemos decidido introducir un nuevo término: "inyectores de entorno".

Los inyectores de entorno pueden configurarse mediante una de las siguientes opciones:

  • @NgModule.providers (en aplicaciones que arrancan a través de un NgModule );
  • @Injectable({provideIn: "..."})(tanto en las aplicaciones basadas en NgModule como en las "independientes");
  • providers en la opción bootstrapApplication (en aplicaciones totalmente "autónomas");
  • providers fied en una configuración Route

Angular v14 introduce un nuevo tipo TypeScript EnvironmentInjector para representar esta nueva denominación. El acompañamiento createEnvironmentInjector

La API permite crear inyectores de entorno mediante programación:

import {createEnvironmentInjector} from '@angular/core';

const parentInjector = … // existing environment injector
const childInjector = createEnvironmentInjector([{provide: PhotosService, useClass: CustomPhotosService}], parentInjector);

Los inyectores de entorno tienen una capacidad adicional: pueden ejecutar la lógica de inicialización cuando se crea un inyector de entorno (similar al  NgModule  constructores que se ejecutan cuando se crea un inyector de módulos):

import {createEnvironmentInjector, ENVIRONMENT_INITIALIZER} from '@angular/core';

createEnvironmentInjector([
{provide: PhotosService, useClass: CustomPhotosService},
{provide: ENVIRONMENT_INITIALIZER, useValue: () => {
        console.log("This function runs when this EnvironmentInjector gets created");
}}
]);

Inyectores autónomos

En realidad, la jerarquía de inyectores de dependencia es algo más elaborada en las aplicaciones que utilizan componentes independientes. Consideremos el siguiente ejemplo:

// an existing "datepicker" component with an NgModule
@Component({
        selector: 'datepicker',
        template: '...',
})
class DatePickerComponent {
  constructor(private calendar: CalendarService) {}
}

@NgModule({
        declarations: [DatePickerComponent],
        exports: [DatePickerComponent]
        providers: [CalendarService],
})
class DatePickerModule {
}

@Component({
        selector: 'date-modal',
        template: '<datepicker></datepicker>',
        standalone: true,
        imports: [DatePickerModule]
})
class DateModalComponent {
}

En el ejemplo anterior, el componente DateModalComponent es independiente puede ser consumido directamente y no tiene ningún NgModule que deba ser importado para poder utilizarlo. Sin embargo, DateModalComponent tiene una dependencia, el DatePickerComponent, que se importa a través de su NgModule (el DatePickerModule).

Este NgModule puede declarar proveedores (en este caso: CalendarService) que son necesarios para la DatePickerComponent para que funcione correctamente.

Cuando Angular crea un componente autónomo, necesita saber que el inyector actual tiene todos los servicios necesarios para las dependencias del componente autónomo, incluyendo las basadas en NgModules.

Para garantizar esto, en algunos casos Angular creará un nuevo "inyector autónomo" como hijo del inyector de entorno actual. En la actualidad, esto ocurre para todos los componentes autónomos arrancados: será un hijo del inyector de entorno raíz.

La misma regla se aplica a los inyectores creados dinámicamente (por ejemplo, por el enrutador o el ViewContainerRef API) componentes independientes.

Se crea un inyector independiente para garantizar que los proveedores importados por un componente independiente estén "aislados" del resto de la aplicación.

Esto nos permite pensar en los componentes autónomos como piezas verdaderamente autónomas que no pueden "filtrar" sus detalles de implementación al resto de la aplicación.

Fuente

Otras referencias

Plataforma de cursos gratis sobre programación

Artículos Relacionados

Domain-Driven Design (DDD)
· 4 min de lectura
RxJS en Angular
· 7 min de lectura
¿Cómo crear un microservicio NestJS Redis?
· 4 min de lectura