TUTORIAL ANGULAR 17: CONSTRUYE APLICACIONES WEB MODERNAS
Introducción a Angular 17
Angular es uno de los frameworks más robustos para el desarrollo de aplicaciones web frontend, compitiendo con alternativas como React y Vue.js. La versión más reciente, Angular 17, lanzada en noviembre de 2023, introduce mejoras significativas en rendimiento, herramientas de desarrollo y experiencia del desarrollador. Entre sus características destacadas están una nueva sintaxis para controladores de flujo, la API de señales para reactividad y una CLI optimizada. Este tutorial te guiará paso a paso para construir una aplicación Angular desde cero, consumiendo una API REST simulada, aplicando Angular Material para el diseño y desplegando el resultado en Firebase con el comando ng deploy.
A lo largo de este artículo, aprenderás a configurar el entorno, crear componentes, implementar enrutamiento, consumir APIs REST con HttpClient, manejar errores, implementar paginación y desplegar la aplicación. Este tutorial está diseñado para desarrolladores con conocimientos básicos de TypeScript y familiaridad con Node.js, ofreciendo ejemplos prácticos y código funcional para reforzar cada concepto.
Prerrequisitos
Para seguir este tutorial, necesitarás:
- Conocimientos básicos de TypeScript.
- Node.js 18.0+ y npm 9.0+ instalados. Puedes descargarlos desde el sitio oficial de Node.js o usar un administrador de versiones como NVM.
- Un editor de código como Visual Studio Code.
- Acceso a una cuenta de Firebase para el despliegue.
Instalación de Angular CLI 17
El primer paso es instalar la última versión de Angular CLI, la herramienta oficial para inicializar y gestionar proyectos Angular. Abre una terminal y ejecuta:
npm install -g @angular/cli
Esto instalará Angular CLI globalmente. Al momento de escribir este tutorial, la versión instalada es 17.3.0. Verifica la instalación con:
ng version
Creación del Proyecto Angular
Con Angular CLI instalado, crea un nuevo proyecto ejecutando:
cd ~
ng new angular-example
La CLI te preguntará si deseas incluir enrutamiento (selecciona Sí) y qué formato de hoja de estilos prefieres (elige CSS). Esto generará una estructura de proyecto con los archivos necesarios, instalará dependencias de npm y configurará el enrutamiento automáticamente.
Para iniciar el servidor de desarrollo, navega al directorio del proyecto y ejecuta:
cd angular-example
ng serve
La aplicación estará disponible en http://localhost:4200. Abre tu navegador para verificar que se muestra la página inicial de Angular. Mantén el servidor corriendo y abre una nueva terminal para los siguientes pasos.
Configuración de HttpClient
Angular HttpClient permite realizar solicitudes HTTP para consumir APIs REST. Para habilitarlo, importa el módulo HttpClientModule en el archivo principal del módulo de la aplicación. Abre src/app/app.module.ts y actualízalo:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Con esto, HttpClient está listo para usarse en los servicios de la aplicación.
Creación de Componentes
Las aplicaciones Angular se construyen con componentes que representan partes de la interfaz de usuario. Crea dos componentes: uno para la página principal (home) y otro para la página de información (about).
Ejecuta en la terminal:
ng generate component home
Esto generará los siguientes archivos:
src/app/home/
├── home.component.css
├── home.component.html
├── home.component.spec.ts
├── home.component.ts
Luego, crea el componente about:
ng generate component about
Edita src/app/about/about.component.html para agregar contenido estático:
<p style="padding: 15px;">Esta es la página de información de la aplicación.</p>
Configuración del Enrutamiento
El enrutamiento permite navegar entre diferentes vistas de la aplicación. Edita src/app/app-routing.module.ts para definir las rutas:
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { AboutComponent } from "./about/about.component";
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: HomeComponent },
{ path: "about", component: AboutComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Este código define tres rutas: una redirección desde la raíz a home, y rutas explícitas para los componentes home y about. La redirección asegura que los usuarios lleguen a la página principal al cargar la aplicación.
Integración de Angular Material
Angular Material proporciona componentes de diseño predefinidos para crear interfaces modernas. Instálalo ejecutando:
ng add @angular/material
Selecciona el tema Indigo/Pink y acepta las opciones predeterminadas para gestos y animaciones. Luego, importa los módulos necesarios en src/app/app.module.ts:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { AppRoutingModule } from "./app-routing.module";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatToolbarModule } from "@angular/material/toolbar";
import { MatIconModule } from "@angular/material/icon";
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { AppComponent } from "./app.component";
import { HomeComponent } from "./home/home.component";
import { AboutComponent } from "./about/about.component";
@NgModule({
declarations: [AppComponent, HomeComponent, AboutComponent],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatCardModule,
MatProgressSpinnerModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Actualiza src/app/app.component.html para incluir una barra de navegación:
<mat-toolbar color="primary">
<h1>Tienda Angular</h1>
<button mat-button routerLink="/">Inicio</button>
<button mat-button routerLink="/about">Acerca</button>
</mat-toolbar>
<router-outlet></router-outlet>
Esto crea una barra superior con botones de navegación.
Simulación de una API REST
Para simular una API REST, utiliza json-server, una herramienta que genera un servidor a partir de un archivo JSON. Instálalo en el proyecto:
npm install --save json-server
Crea una carpeta server en la raíz del proyecto:
mkdir server
cd server
Crea un archivo database.json con el siguiente contenido:
{
"products": []
}
Para generar datos falsos, instala faker.js:
cd ..
npm install faker --save
Crea un archivo server/generate.js:
const faker = require("faker");
const database = { products: [] };
for (let i = 1; i <= 300; i++) {
database.products.push({
id: i,
name: faker.commerce.productName(),
description: faker.lorem.sentences(),
price: faker.commerce.price(),
imageUrl: "https://source.unsplash.com/1600x900/?product",
quantity: faker.random.number(),
});
}
console.log(JSON.stringify(database));
Actualiza package.json para incluir scripts de generación y servidor:
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"generate": "node ./server/generate.js > ./server/database.json",
"server": "json-server --watch ./server/database.json"
}
Genera los datos y ejecuta el servidor:
npm run generate
npm run server
El servidor estará disponible en http://localhost:3000, ofreciendo endpoints como GET /products, GET /products/<id>, POST /products, entre otros.
Consumo de la API REST con HttpClient
Crea un servicio para encapsular las solicitudes HTTP. Genera el servicio con:
ng generate service api
Edita src/app/api.service.ts:
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
@Injectable({
providedIn: "root",
})
export class ApiService {
private SERVER_URL = "http://localhost:3000/products";
constructor(private httpClient: HttpClient) {}
public get() {
return this.httpClient.get(this.SERVER_URL);
}
}
Inyecta el servicio en src/app/home/home.component.ts:
import { Component, OnInit } from "@angular/core";
import { ApiService } from "../api.service";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.css"],
})
export class HomeComponent implements OnInit {
products: any[] = [];
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService.get().subscribe((data: any[]) => {
console.log(data);
this.products = data;
});
}
}
Actualiza src/app/home/home.component.html para mostrar los productos:
<div style="padding: 13px;">
<mat-spinner *ngIf="products.length === 0"></mat-spinner>
<mat-card *ngFor="let product of products" style="margin-top:10px;">
<mat-card-header>
<mat-card-title>{{product.name}}</mat-card-title>
<mat-card-subtitle
>{{product.price}} $ / {{product.quantity}}</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>{{product.description}}</p>
<img style="height:100%; width:100%;" src="{{product.imageUrl}}" />
</mat-card-content>
<mat-card-actions>
<button mat-button>Comprar producto</button>
</mat-card-actions>
</mat-card>
</div>
Manejo de Errores HTTP
Para manejar errores en las solicitudes HTTP, actualiza src/app/api.service.ts con operadores RxJS:
import { Injectable } from "@angular/core";
import {
HttpClient,
HttpErrorResponse,
HttpParams,
} from "@angular/common/http";
import { throwError } from "rxjs";
import { catchError, retry } from "rxjs/operators";
@Injectable({
providedIn: "root",
})
export class ApiService {
private SERVER_URL = "http://localhost:3000/products";
constructor(private httpClient: HttpClient) {}
handleError(error: HttpErrorResponse) {
let errorMessage = "Error desconocido";
if (error.error instanceof ErrorEvent) {
errorMessage = `Error: ${error.error.message}`;
} else {
errorMessage = `Código: ${error.status}, Mensaje: ${error.message}`;
}
window.alert(errorMessage);
return throwError(() => new Error(errorMessage));
}
public sendGetRequest() {
return this.httpClient
.get(this.SERVER_URL, {
params: new HttpParams({ fromString: "_page=1&_limit=20" }),
})
.pipe(retry(3), catchError(this.handleError));
}
}
Este código implementa reintentos automáticos y muestra mensajes de error al usuario.
Implementación de Paginación
Para agregar paginación, analiza el encabezado Link de las respuestas HTTP. Actualiza src/app/api.service.ts:
import { Injectable } from "@angular/core";
import {
HttpClient,
HttpErrorResponse,
HttpParams,
HttpResponse,
} from "@angular/common/http";
import { throwError, Observable } from "rxjs";
import { catchError, retry, tap } from "rxjs/operators";
@Injectable({
providedIn: "root",
})
export class ApiService {
private SERVER_URL = "http://localhost:3000/products";
public first: string = "";
public prev: string = "";
public next: string = "";
public last: string = "";
constructor(private httpClient: HttpClient) {}
handleError(error: HttpErrorResponse) {
let errorMessage = "Error desconocido";
if (error.error instanceof ErrorEvent) {
errorMessage = `Error: ${error.error.message}`;
} else {
errorMessage = `Código: ${error.status}, Mensaje: ${error.message}`;
}
window.alert(errorMessage);
return throwError(() => new Error(errorMessage));
}
parseLinkHeader(header: string) {
if (!header || header.length === 0) {
return;
}
let parts = header.split(",");
const links: { [key: string]: string } = {};
parts.forEach((p) => {
let section = p.split(";");
let url = section[0].replace(/<(.*)>/, "$1").trim();
let name = section[1].replace(/rel="(.*)"/, "$1").trim();
links[name] = url;
});
this.first = links["first"];
this.prev = links["prev"];
this.next = links["next"];
this.last = links["last"];
}
public sendGetRequest(): Observable<HttpResponse<any>> {
return this.httpClient
.get(this.SERVER_URL, {
params: new HttpParams({ fromString: "_page=1&_limit=20" }),
observe: "response",
})
.pipe(
retry(3),
catchError(this.handleError),
tap((res) => this.parseLinkHeader(res.headers.get("Link")))
);
}
public sendGetRequestToUrl(url: string): Observable<HttpResponse<any>> {
return this.httpClient.get(url, { observe: "response" }).pipe(
retry(3),
catchError(this.handleError),
tap((res) => this.parseLinkHeader(res.headers.get("Link")))
);
}
}
Actualiza src/app/home/home.component.ts para manejar la paginación:
import { Component, OnInit, OnDestroy } from "@angular/core";
import { HttpResponse } from "@angular/common/http";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { ApiService } from "../api.service";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.css"],
})
export class HomeComponent implements OnInit, OnDestroy {
products: any[] = [];
private destroy$ = new Subject<void>();
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService
.sendGetRequest()
.pipe(takeUntil(this.destroy$))
.subscribe((res: HttpResponse<any>) => {
console.log(res);
this.products = res.body;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
firstPage() {
this.products = [];
this.apiService
.sendGetRequestToUrl(this.apiService.first)
.pipe(takeUntil(this.destroy$))
.subscribe((res: HttpResponse<any>) => {
this.products = res.body;
});
}
previousPage() {
if (this.apiService.prev) {
this.products = [];
this.apiService
.sendGetRequestToUrl(this.apiService.prev)
.pipe(takeUntil(this.destroy$))
.subscribe((res: HttpResponse<any>) => {
this.products = res.body;
});
}
}
nextPage() {
if (this.apiService.next) {
this.products = [];
this.apiService
.sendGetRequestToUrl(this.apiService.next)
.pipe(takeUntil(this.destroy$))
.subscribe((res: HttpResponse<any>) => {
this.products = res.body;
});
}
}
lastPage() {
this.products = [];
this.apiService
.sendGetRequestToUrl(this.apiService.last)
.pipe(takeUntil(this.destroy$))
.subscribe((res: HttpResponse<any>) => {
this.products = res.body;
});
}
}
Modifica src/app/home/home.component.html para incluir botones de paginación:
<div style="padding: 13px;">
<mat-spinner *ngIf="products.length === 0"></mat-spinner>
<mat-card *ngFor="let product of products" style="margin-top:10px;">
<mat-card-header>
<mat-card-title>#{{product.id}} {{product.name}}</mat-card-title>
<mat-card-subtitle
>{{product.price}} $ / {{product.quantity}}</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>{{product.description}}</p>
<img style="height:100%; width:100%;" src="{{product.imageUrl}}" />
</mat-card-content>
<mat-card-actions>
<button mat-button>Comprar producto</button>
</mat-card-actions>
</mat-card>
</div>
<div>
<button (click)="firstPage()" mat-button>Primera</button>
<button (click)="previousPage()" mat-button>Anterior</button>
<button (click)="nextPage()" mat-button>Siguiente</button>
<button (click)="lastPage()" mat-button>Última</button>
</div>
Despliegue en Firebase
Para desplegar la aplicación en Firebase, instala la integración de Angular Fire:
ng add @angular/fire
Sigue las instrucciones para autenticarte con Firebase y selecciona un proyecto existente. Esto generará archivos de configuración (firebase.json y .firebaserc) y actualizará angular.json.
Despliega la aplicación con:
ng deploy
Este comando compila la aplicación en modo producción y la sube a Firebase Hosting. Asegúrate de tener un proyecto de Firebase configurado previamente.
Conclusiones
Este tutorial te ha guiado a través del proceso de construcción de una aplicación Angular 17 desde cero, integrando herramientas modernas como Angular CLI, HttpClient, y Angular Material. Has aprendido a simular una API REST con json-server, consumir datos con HttpClient, implementar paginación, manejar errores y desplegar la aplicación en Firebase. Estas habilidades son esenciales para desarrollar aplicaciones web robustas y escalables, aprovechando las capacidades de Angular para crear interfaces de usuario dinámicas y responsivas. Explora más funciones de Angular, como las señales para reactividad o las nuevas características de la CLI, para seguir mejorando tus proyectos.