Angular
¿Qué es Angular?
Angular es un framework completo de TypeScript desarrollado por Google para construir aplicaciones web y móviles robustas. A diferencia de librerías como React, Angular es una plataforma completa que incluye todo lo necesario para desarrollar aplicaciones empresariales de gran escala.
¿Para qué sirve Angular?
Angular es ideal para:
- Aplicaciones empresariales complejas y de gran escala.
- Dashboards y paneles de administración avanzados.
- Progressive Web Apps (PWAs) robustas.
- Aplicaciones con arquitecturas complejas y muchos módulos.
- Proyectos que requieren TypeScript desde el inicio.
- Single Page Applications (SPAs) con múltiples equipos de desarrollo.
¿Cómo funciona?
Angular utiliza una arquitectura basada en componentes con inyección de dependencias y decoradores de TypeScript. Imagina Angular como una fábrica industrial completa con todas las herramientas, procesos y protocolos necesarios para construir productos complejos de manera consistente y escalable.
Ejemplo: Componente básico en Angular
Un componente Angular combina TypeScript, HTML y CSS con decoradores:
// user-profile.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { UserService } from '../services/user.service';
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
isActive: boolean;
}
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
@Input() userId!: number;
@Output() userUpdated = new EventEmitter<User>();
user: User | null = null;
isLoading = true;
isEditing = false;
editForm = {
name: '',
email: '',
role: 'user' as const
};
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUser();
}
async loadUser(): Promise<void> {
try {
this.isLoading = true;
this.user = await this.userService.getUserById(this.userId);
if (this.user) {
this.editForm = {
name: this.user.name,
email: this.user.email,
role: this.user.role
};
}
} catch (error) {
console.error('Error loading user:', error);
} finally {
this.isLoading = false;
}
}
startEditing(): void {
this.isEditing = true;
}
cancelEditing(): void {
this.isEditing = false;
if (this.user) {
this.editForm = {
name: this.user.name,
email: this.user.email,
role: this.user.role
};
}
}
async saveUser(): Promise<void> {
if (!this.user) return;
try {
const updatedUser = await this.userService.updateUser(this.user.id, this.editForm);
this.user = updatedUser;
this.isEditing = false;
this.userUpdated.emit(updatedUser);
} catch (error) {
console.error('Error updating user:', error);
}
}
async toggleUserStatus(): Promise<void> {
if (!this.user) return;
try {
const updatedUser = await this.userService.toggleUserStatus(this.user.id);
this.user = updatedUser;
this.userUpdated.emit(updatedUser);
} catch (error) {
console.error('Error toggling user status:', error);
}
}
get isAdmin(): boolean {
return this.user?.role === 'admin';
}
get statusClass(): string {
if (!this.user) return '';
return this.user.isActive ? 'status-active' : 'status-inactive';
}
}
<!-- user-profile.component.html -->
<div class="user-profile" *ngIf="!isLoading; else loadingTemplate">
<div class="user-header">
<h2>{{ user?.name }}</h2>
<span class="user-role" [ngClass]="user?.role">{{ user?.role | uppercase }}</span>
<span class="user-status" [ngClass]="statusClass">
{{ user?.isActive ? 'Activo' : 'Inactivo' }}
</span>
</div>
<!-- Modo vista -->
<div *ngIf="!isEditing" class="view-mode">
<div class="user-info">
<p><strong>Email:</strong> {{ user?.email }}</p>
<p><strong>ID:</strong> {{ user?.id }}</p>
</div>
<div class="actions">
<button
class="btn btn-primary"
(click)="startEditing()"
[disabled]="!isAdmin">
Editar
</button>
<button
class="btn"
[ngClass]="user?.isActive ? 'btn-warning' : 'btn-success'"
(click)="toggleUserStatus()"
[disabled]="!isAdmin">
{{ user?.isActive ? 'Desactivar' : 'Activar' }}
</button>
</div>
</div>
<!-- Modo edición -->
<div *ngIf="isEditing" class="edit-mode">
<form #userForm="ngForm" (ngSubmit)="saveUser()">
<div class="form-group">
<label for="name">Nombre:</label>
<input
type="text"
id="name"
name="name"
[(ngModel)]="editForm.name"
required
#nameInput="ngModel"
class="form-control"
[class.is-invalid]="nameInput.invalid && nameInput.touched">
<div *ngIf="nameInput.invalid && nameInput.touched" class="invalid-feedback">
El nombre es requerido
</div>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
type="email"
id="email"
name="email"
[(ngModel)]="editForm.email"
required
email
#emailInput="ngModel"
class="form-control"
[class.is-invalid]="emailInput.invalid && emailInput.touched">
<div *ngIf="emailInput.invalid && emailInput.touched" class="invalid-feedback">
<span *ngIf="emailInput.errors?.['required']">El email es requerido</span>
<span *ngIf="emailInput.errors?.['email']">El email no es válido</span>
</div>
</div>
<div class="form-group">
<label for="role">Rol:</label>
<select
id="role"
name="role"
[(ngModel)]="editForm.role"
class="form-control">
<option value="user">Usuario</option>
<option value="admin">Administrador</option>
<option value="guest">Invitado</option>
</select>
</div>
<div class="form-actions">
<button
type="submit"
class="btn btn-success"
[disabled]="userForm.invalid">
Guardar
</button>
<button
type="button"
class="btn btn-secondary"
(click)="cancelEditing()">
Cancelar
</button>
</div>
</form>
</div>
</div>
<ng-template #loadingTemplate>
<div class="loading">
<div class="spinner"></div>
<p>Cargando usuario...</p>
</div>
</ng-template>
/* user-profile.component.scss */
.user-profile {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.user-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
h2 {
margin: 0;
color: #333;
}
.user-role {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
&.admin { background: #dc3545; color: white; }
&.user { background: #007bff; color: white; }
&.guest { background: #6c757d; color: white; }
}
.user-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
&.status-active { background: #28a745; color: white; }
&.status-inactive { background: #ffc107; color: #000; }
}
}
.form-group {
margin-bottom: 1rem;
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
&.is-invalid {
border-color: #dc3545;
}
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
}
.actions, .form-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.btn-primary { background: #007bff; color: white; }
&.btn-success { background: #28a745; color: white; }
&.btn-warning { background: #ffc107; color: #000; }
&.btn-secondary { background: #6c757d; color: white; }
}
}
.loading {
text-align: center;
padding: 2rem;
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Servicios y Inyección de Dependencias
Angular utiliza un sistema robusto de inyección de dependencias:
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root' // Singleton global
})
export class UserService {
private readonly apiUrl = 'https://api.ejemplo.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}
getUserById(id: number): Promise<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
)
.toPromise();
}
updateUser(id: number, userData: Partial<User>): Promise<User> {
return this.http.put<User>(`${this.apiUrl}/${id}`, userData)
.pipe(
catchError(this.handleError)
)
.toPromise();
}
toggleUserStatus(id: number): Promise<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}/toggle-status`, {})
.pipe(
catchError(this.handleError)
)
.toPromise();
}
private handleError(error: HttpErrorResponse) {
let errorMessage = 'Ocurrió un error desconocido';
if (error.error instanceof ErrorEvent) {
// Error del cliente
errorMessage = `Error: ${error.error.message}`;
} else {
// Error del servidor
errorMessage = `Código de error: ${error.status}, mensaje: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
Módulos y Organización
Angular organiza la aplicación en módulos:
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserProfileComponent } from './components/user-profile/user-profile.component';
import { UserListComponent } from './components/user-list/user-list.component';
@NgModule({
declarations: [
AppComponent,
UserProfileComponent,
UserListComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Routing avanzado
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UserListComponent } from './components/user-list/user-list.component';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
import { AuthGuard } from './guards/auth.guard';
import { AdminGuard } from './guards/admin.guard';
const routes: Routes = [
{ path: '', redirectTo: '/users', pathMatch: 'full' },
{
path: 'users',
component: UserListComponent,
canActivate: [AuthGuard]
},
{
path: 'users/:id',
component: UserDetailComponent,
canActivate: [AuthGuard]
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AdminGuard]
},
{ path: '**', redirectTo: '/users' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Formularios reactivos
Angular ofrece formularios reactivos poderosos:
// reactive-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Nombre:</label>
<input formControlName="name" class="form-control">
<div *ngIf="userForm.get('name')?.invalid && userForm.get('name')?.touched">
El nombre es requerido
</div>
</div>
<div class="form-group">
<label>Email:</label>
<input formControlName="email" type="email" class="form-control">
<div *ngIf="userForm.get('email')?.invalid && userForm.get('email')?.touched">
<span *ngIf="userForm.get('email')?.errors?.['required']">Email requerido</span>
<span *ngIf="userForm.get('email')?.errors?.['email']">Email inválido</span>
</div>
</div>
<div formGroupName="address">
<h3>Dirección</h3>
<input formControlName="street" placeholder="Calle">
<input formControlName="city" placeholder="Ciudad">
</div>
<div class="skills-section">
<h3>Habilidades</h3>
<div formArrayName="skills">
<div *ngFor="let skill of skillsArray.controls; let i = index">
<input [formControlName]="i" placeholder="Habilidad">
<button type="button" (click)="removeSkill(i)">Eliminar</button>
</div>
</div>
<button type="button" (click)="addSkill()">Agregar Habilidad</button>
</div>
<button type="submit" [disabled]="userForm.invalid">Guardar</button>
</form>
`
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: [''],
city: ['', Validators.required]
}),
skills: this.fb.array([])
});
}
get skillsArray(): FormArray {
return this.userForm.get('skills') as FormArray;
}
addSkill(): void {
this.skillsArray.push(this.fb.control(''));
}
removeSkill(index: number): void {
this.skillsArray.removeAt(index);
}
onSubmit(): void {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}
Guards y Seguridad
// auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(): boolean {
if (this.authService.isAuthenticated()) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}
Pipes personalizados
// currency-format.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'currencyFormat'
})
export class CurrencyFormatPipe implements PipeTransform {
transform(value: number, currency: string = 'USD'): string {
if (value == null) return '';
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: currency
}).format(value);
}
}
// Uso en template:
// {{ price | currencyFormat:'EUR' }}
Características principales
- TypeScript nativo: Tipado fuerte y herramientas de desarrollo avanzadas.
- Arquitectura modular: Organización escalable con módulos y lazy loading.
- Inyección de dependencias: Sistema robusto para gestión de servicios.
- CLI poderoso: Angular CLI para scaffolding y build optimizado.
- Formularios reactivos: Validación y gestión de formularios complejos.
- Testing integrado: Jasmine y Karma incluidos por defecto.
Angular CLI
# Crear nueva aplicación
ng new mi-app
# Generar componentes, servicios, etc.
ng generate component user-profile
ng generate service user
ng generate guard auth
ng generate pipe currency-format
# Desarrollo
ng serve
# Build para producción
ng build --prod
# Testing
ng test
ng e2e
# Análisis del bundle
ng build --stats-json
npx webpack-bundle-analyzer dist/mi-app/stats.json
¿Cuándo usar Angular?
- Aplicaciones empresariales grandes con múltiples equipos.
- Proyectos que requieren TypeScript desde el inicio.
- Aplicaciones complejas con muchos módulos y funcionalidades.
- Organizaciones que valoran convenciones y estructura.
- Proyectos a largo plazo que necesitan mantenibilidad.
- Aplicaciones que requieren testing robusto.
Comparación con otros frameworks
| Característica | Angular | React | Vue.js |
|---|---|---|---|
| Curva de aprendizaje | Pronunciada | Media | Suave |
| Tamaño de bundle | Grande | Pequeño | Pequeño |
| TypeScript | Nativo | Excelente | Soporte nativo |
| Arquitectura | Opinionada | Flexible | Flexible |
| Ecosistema | Completo | Extenso | Completo |
| Testing | Integrado | Manual setup | Manual setup |
| Enterprise ready | Excelente | Bueno | Bueno |
Ventajas de Angular
✅ Framework completo: Todo incluido, desde routing hasta testing. ✅ TypeScript nativo: Tipado fuerte y mejor experiencia de desarrollo. ✅ Arquitectura escalable: Módulos, servicios e inyección de dependencias. ✅ CLI poderoso: Herramientas de desarrollo excepcionales. ✅ Respaldo de Google: Estabilidad y evolución continua. ✅ Enterprise ready: Ideal para aplicaciones corporativas grandes.
Desventajas a considerar
❌ Curva de aprendizaje: Más complejo que otros frameworks. ❌ Bundle size: Aplicaciones más pesadas inicialmente. ❌ Over-engineering: Puede ser excesivo para proyectos simples. ❌ Actualizaciones: Migraciones entre versiones pueden ser complejas.
Conclusión
Angular es la elección ideal para aplicaciones empresariales complejas que requieren estructura, escalabilidad y mantenibilidad a largo plazo. Su enfoque opinionado y herramientas integradas lo convierten en una plataforma completa para equipos que valoran las convenciones y la arquitectura robusta.
Con TypeScript como ciudadano de primera clase y un ecosistema maduro, Angular te permite construir aplicaciones sofisticadas con confianza, especialmente en entornos corporativos donde la estabilidad y las mejores prácticas son prioritarias.