⚠️ 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 propiedadformGroup
elNgModule
proporciona esa directiva a través de la directivaReactiveFormsModule
.
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 unNgModule
);@Injectable({provideIn: "..."})
(tanto en las aplicaciones basadas en NgModule como en las "independientes");providers
en la opciónbootstrapApplication
(en aplicaciones totalmente "autónomas");providers
fied en una configuraciónRoute
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.