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ísticaAngularReactVue.js
Curva de aprendizajePronunciadaMediaSuave
Tamaño de bundleGrandePequeñoPequeño
TypeScriptNativoExcelenteSoporte nativo
ArquitecturaOpinionadaFlexibleFlexible
EcosistemaCompletoExtensoCompleto
TestingIntegradoManual setupManual setup
Enterprise readyExcelenteBuenoBueno

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.


Usamos cookies para mejorar tu experiencia. ¿Aceptas las cookies de análisis?