taller-ionic/cap2.md
2025-04-24 15:57:53 -04:00

20 KiB

Taller: Desarrollo con Angular e Ionic

Capítulo 2: Componentes y Estructura de una Aplicación Angular/Ionic

1. Estructura de una Aplicación Angular/Ionic

Al crear un nuevo proyecto de Ionic con Angular mediante el comando ionic start, se genera una estructura de carpetas organizada:

mi-app/
├── src/                   # Código fuente principal
│   ├── app/               # Lógica y componentes de la aplicación
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── app-routing.module.ts
│   ├── assets/            # Recursos estáticos (imágenes, fuentes, etc.)
│   ├── environments/      # Configuraciones por entorno (dev, prod)
│   ├── theme/             # Variables globales de estilo (colores, etc.)
│   ├── global.scss        # Estilos globales
│   ├── index.html         # Archivo HTML raíz
│   ├── main.ts            # Punto de entrada de la aplicación
├── angular.json           # Configuración de Angular
├── capacitor.config.ts    # Configuración de Capacitor
├── package.json           # Dependencias y scripts
├── tsconfig.json          # Configuración de TypeScript

Archivos Clave:

  • app.module.ts: Módulo principal que configura la aplicación
  • app-routing.module.ts: Define las rutas de navegación
  • app.component.ts: Componente raíz que contiene toda la aplicación
  • index.html: Archivo HTML base donde se monta la aplicación
  • main.ts: Punto de entrada que arranca la aplicación Angular

2. Fundamentos de los Componentes Angular

Anatomía de un Componente

Cada componente Angular consta de:

  1. Clase TypeScript: Contiene la lógica y datos
  2. Plantilla HTML: Define la estructura visual
  3. Estilos CSS: Define la apariencia (opcional)
  4. Metadatos: Configuración mediante decorador @Component
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-producto',          // Cómo se referencia en HTML
  templateUrl: './producto.component.html',  // Plantilla HTML
  styleUrls: ['./producto.component.scss']   // Estilos
})
export class ProductoComponent implements OnInit {
  nombre: string = 'Smartphone';
  precio: number = 599.99;
  disponible: boolean = true;
  
  constructor() { }
  
  ngOnInit() {
    // Inicialización del componente
  }
  
  aplicarDescuento(porcentaje: number): void {
    this.precio = this.precio * (1 - porcentaje/100);
  }
}

Ciclo de Vida de los Componentes

Los componentes tienen un ciclo de vida gestionado por Angular:

  1. ngOnChanges: Cuando cambian las propiedades de entrada (@Input)
  2. ngOnInit: Después de la primera inicialización
  3. ngAfterViewInit: Cuando la vista se ha inicializado
  4. ngOnDestroy: Justo antes de que Angular destruya el componente
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DatosService } from './datos.service';

@Component({
  selector: 'app-ejemplo',
  template: '<div>{{datos}}</div>'
})
export class EjemploComponent implements OnInit, OnDestroy {
  datos: any[] = [];
  private suscripcion!: Subscription;
  
  constructor(private datosService: DatosService) { }
  
  ngOnInit() {
    // Perfecto para inicializar datos, suscripciones, etc.
    this.suscripcion = this.datosService.obtenerDatos()
      .subscribe(datos => {
        this.datos = datos;
      });
  }
  
  ngOnDestroy() {
    // Limpieza antes de destruir el componente
    if (this.suscripcion) {
      this.suscripcion.unsubscribe();
    }
  }
}

3. Vinculación de Datos (Data Binding)

Angular ofrece cuatro formas de vinculación de datos:

Interpolación {{ }}

Muestra valores de propiedades en la plantilla:

<h1>{{ titulo }}</h1>
<p>Precio: {{ precio | currency }}</p>
<div>Estado: {{ disponible ? 'En stock' : 'Agotado' }}</div>

Property Binding [ ]

Vincula propiedades HTML con valores del componente:

<img [src]="imagenUrl">
<button [disabled]="!formularioValido">Enviar</button>
<div [ngClass]="{'destacado': esDestacado, 'oculto': !visible}">
  Contenido con clases dinámicas
</div>

Event Binding ( )

Responde a eventos del usuario:

<button (click)="agregarAlCarrito()">Agregar al carrito</button>
<input (input)="actualizarBusqueda($event)">
<form (submit)="enviarFormulario()">
  <!-- Campos del formulario -->
</form>

Two-Way Binding [( )]

Combina property binding y event binding para actualizar datos en ambas direcciones:

<input [(ngModel)]="nombreUsuario">

<!-- Equivalente a: -->
<input [value]="nombreUsuario" (input)="nombreUsuario = $event.target.value">

Nota: Para usar ngModel, debes importar FormsModule en tu módulo Angular.

4. Directivas en Angular

Las directivas son clases que extienden HTML con nueva funcionalidad.

Directivas Estructurales

Modifican el DOM añadiendo o quitando elementos:

<!-- *ngIf: Condicional -->
<div *ngIf="producto.disponible">
  El producto está disponible
</div>

<!-- *ngFor: Repetición -->
<ul>
  <li *ngFor="let producto of productos; let i = index">
    {{i+1}}. {{producto.nombre}} - {{producto.precio | currency}}
  </li>
</ul>

<!-- *ngSwitch: Condicional múltiple -->
<div [ngSwitch]="rol">
  <div *ngSwitchCase="'admin'">Panel de administrador</div>
  <div *ngSwitchCase="'editor'">Panel de editor</div>
  <div *ngSwitchDefault>Panel de usuario</div>
</div>

Directivas de Atributo

Modifican la apariencia o comportamiento de elementos existentes:

<!-- ngClass: Clases dinámicas -->
<div [ngClass]="{'activo': estaActivo, 'destacado': esDestacado}">
  Elemento con clases dinámicas
</div>

<!-- ngStyle: Estilos dinámicos -->
<div [ngStyle]="{'color': colorTexto, 'font-size.px': tamanoFuente}">
  Texto con estilo dinámico
</div>

<!-- ngModel: Two-way binding (requiere FormsModule) -->
<input [(ngModel)]="nombre" placeholder="Escribe tu nombre">

5. Componentes UI de Ionic - Visión General

Ionic proporciona una amplia gama de componentes UI pre-diseñados que siguen las guías de diseño de iOS y Android. A continuación, se muestra una visión general de las categorías principales:

Categorías de Componentes

  1. Navegación

    • ion-header, ion-toolbar, ion-buttons
    • ion-tabs
    • ion-menu
    • ion-back-button
  2. Contenido y Presentación

    • ion-card y componentes relacionados
    • ion-list, ion-item
    • ion-grid, ion-row, ion-col
    • ion-avatar, ion-thumbnail
  3. Formularios e Inputs

    • ion-input, ion-textarea
    • ion-select, ion-radio, ion-checkbox
    • ion-toggle, ion-range
    • ion-datetime
  4. Feedback al Usuario

    • ion-loading
    • ion-toast
    • ion-alert
    • ion-action-sheet

Ejemplo Básico de Uso

<!-- Ejemplo de una página con algunos componentes básicos -->
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Mi Aplicación</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card>
    <ion-card-header>
      <ion-card-title>Bienvenido</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      Contenido de ejemplo para mostrar un card básico.
    </ion-card-content>
  </ion-card>
  
  <ion-list>
    <ion-item>
      <ion-label>Elemento 1</ion-label>
    </ion-item>
    <ion-item>
      <ion-label>Elemento 2</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

Controladores de Componentes

Para componentes interactivos como alertas, modales o toasts, se utilizan controladores:

import { Component } from '@angular/core';
import { AlertController } from '@ionic/angular';

@Component({
  selector: 'app-ejemplo',
  templateUrl: './ejemplo.page.html',
})
export class EjemploPage {
  constructor(private alertController: AlertController) {}
  
  async mostrarAlerta() {
    const alert = await this.alertController.create({
      header: 'Información',
      message: 'Esta es una alerta de ejemplo',
      buttons: ['OK']
    });
    
    await alert.present();
  }
}

Nota: Para más detalles sobre cada componente, consulta la documentación oficial de Ionic, donde encontrarás ejemplos completos de uso y todas las propiedades disponibles.

6. Servicios y Dependency Injection

Los servicios en Angular son clases que encapsulan lógica de negocio y compartir estado entre componentes. La inyección de dependencias facilita su uso.

Creación de un Servicio

ionic generate service services/productos

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Producto {
  id: number;
  nombre: string;
  precio: number;
  imagen: string;
  descripcion: string;
}

@Injectable({
  providedIn: 'root'  // Disponible en toda la aplicación
})
export class ProductosService {
  private apiUrl = 'https://mi-api.com/productos';
  
  constructor(private http: HttpClient) { }
  
  obtenerProductos(): Observable<Producto[]> {
    return this.http.get<Producto[]>(this.apiUrl);
  }
  
  obtenerProductoPorId(id: number): Observable<Producto> {
    return this.http.get<Producto>(`${this.apiUrl}/${id}`);
  }
  
  buscarProductos(termino: string): Observable<Producto[]> {
    return this.http.get<Producto[]>(this.apiUrl).pipe(
      map(productos => productos.filter(p => 
        p.nombre.toLowerCase().includes(termino.toLowerCase())
      ))
    );
  }
  
  agregarProducto(producto: Producto): Observable<Producto> {
    return this.http.post<Producto>(this.apiUrl, producto);
  }
  
  actualizarProducto(producto: Producto): Observable<Producto> {
    return this.http.put<Producto>(`${this.apiUrl}/${producto.id}`, producto);
  }
  
  eliminarProducto(id: number): Observable<any> {
    return this.http.delete(`${this.apiUrl}/${id}`);
  }
}

Uso del Servicio en un Componente

import { Component, OnInit } from '@angular/core';
import { ProductosService, Producto } from '../../services/productos.service';
import { LoadingController } from '@ionic/angular';

@Component({
  selector: 'app-lista-productos',
  templateUrl: './lista-productos.page.html',
  styleUrls: ['./lista-productos.page.scss'],
})
export class ListaProductosPage implements OnInit {
  productos: Producto[] = [];
  cargando = false;
  error = false;
  
  constructor(
    private productosService: ProductosService,
    private loadingController: LoadingController
  ) { }
  
  async ngOnInit() {
    await this.cargarProductos();
  }
  
  async cargarProductos() {
    this.cargando = true;
    
    const loading = await this.loadingController.create({
      message: 'Cargando productos...'
    });
    await loading.present();
    
    this.productosService.obtenerProductos().subscribe({
      next: (data) => {
        this.productos = data;
        this.error = false;
      },
      error: (error) => {
        console.error('Error al cargar productos', error);
        this.error = true;
      },
      complete: () => {
        this.cargando = false;
        loading.dismiss();
      }
    });
  }
  
  async refrescarProductos(event: any) {
    this.productosService.obtenerProductos().subscribe({
      next: (data) => {
        this.productos = data;
        this.error = false;
        event.target.complete();
      },
      error: (error) => {
        console.error('Error al refrescar productos', error);
        this.error = true;
        event.target.complete();
      }
    });
  }
  
  buscarProductos(event: any) {
    const termino = event.detail.value;
    
    if (termino && termino.trim() !== '') {
      this.productosService.buscarProductos(termino).subscribe(productos => {
        this.productos = productos;
      });
    } else {
      this.cargarProductos();
    }
  }
}

7. Navegación y Enrutamiento

Angular Router permite definir rutas para navegar entre páginas:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'inicio',
    pathMatch: 'full'
  },
  {
    path: 'inicio',
    loadChildren: () => import('./pages/inicio/inicio.module').then(m => m.InicioPageModule)
  },
  {
    path: 'productos',
    loadChildren: () => import('./pages/productos/productos.module').then(m => m.ProductosPageModule)
  },
  {
    path: 'producto/:id',
    loadChildren: () => import('./pages/detalle-producto/detalle-producto.module').then(m => m.DetalleProductoPageModule)
  },
  {
    path: 'carrito',
    loadChildren: () => import('./pages/carrito/carrito.module').then(m => m.CarritoPageModule)
  },
  {
    path: 'perfil',
    loadChildren: () => import('./pages/perfil/perfil.module').then(m => m.PerfilPageModule)
  },
  {
    path: '**',
    loadChildren: () => import('./pages/not-found/not-found.module').then(m => m.NotFoundPageModule)
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Navegación Declarativa

<!-- Enlace simple -->
<ion-button routerLink="/productos">Ver productos</ion-button>

<!-- Con parámetros -->
<ion-item [routerLink]="['/producto', producto.id]">
  {{ producto.nombre }}
</ion-item>

<!-- Con parámetros de consulta -->
<a [routerLink]="['/productos']" [queryParams]="{categoria: 'electronica'}">
  Ver electrónica
</a>

Navegación Programática

import { Component } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';

@Component({
  selector: 'app-navegacion',
  templateUrl: './navegacion.page.html',
})
export class NavegacionPage {
  constructor(private router: Router) { }
  
  irAProductos() {
    this.router.navigate(['/productos']);
  }
  
  verProducto(id: number) {
    this.router.navigate(['/producto', id]);
  }
  
  filtrarProductos() {
    const navigationExtras: NavigationExtras = {
      queryParams: {
        categoria: 'electronica',
        orden: 'precio'
      }
    };
    this.router.navigate(['/productos'], navigationExtras);
  }
}

Recibir Parámetros

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductosService, Producto } from '../../services/productos.service';

@Component({
  selector: 'app-detalle-producto',
  templateUrl: './detalle-producto.page.html',
})
export class DetalleProductoPage implements OnInit {
  producto: Producto | undefined;
  
  constructor(
    private route: ActivatedRoute,
    private productosService: ProductosService
  ) { }
  
  ngOnInit() {
    // Parámetros de ruta
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      if (id) {
        this.cargarProducto(id);
      }
    });
    
    // Parámetros de consulta
    this.route.queryParamMap.subscribe(params => {
      const vista = params.get('vista');
      console.log('Vista:', vista);
    });
  }
  
  cargarProducto(id: number) {
    this.productosService.obtenerProductoPorId(id).subscribe(producto => {
      this.producto = producto;
    });
  }
}

8. Integración con Capacitor

Para utilizar capacidades nativas con Capacitor, necesitas instalar y configurar plugins específicos.

Ejemplo: Cámara

npm install @capacitor/camera
npx cap sync

import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';

@Component({
  selector: 'app-camara',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Cámara</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content class="ion-padding">
      <ion-button expand="block" (click)="tomarFoto()">
        <ion-icon name="camera" slot="start"></ion-icon>
        Tomar Foto
      </ion-button>
      
      <div *ngIf="photoUrl" class="ion-margin-top">
        <img [src]="photoUrl" alt="Foto tomada">
      </div>
    </ion-content>
  `
})
export class CamaraPage {
  photoUrl: string | undefined;
  
  constructor() { }
  
  async tomarFoto() {
    try {
      const photo = await Camera.getPhoto({4. Las directivas extienden HTML con funcionalidades dinámicas
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera
      });
      
      // La propiedad webPath es la URL que se puede usar para mostrar la foto
      this.photoUrl = photo.webPath;
    } catch (error) {
      console.error('Error al tomar la foto', error);
    }
  }
}

Ejemplo: Geolocalización

npm install @capacitor/geolocation
npx cap sync

import { Component } from '@angular/core';
import { Geolocation, Position } from '@capacitor/geolocation';

@Component({
  selector: 'app-geolocalizacion',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Ubicación</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content class="ion-padding">
      <ion-button expand="block" (click)="obtenerUbicacion()">
        <ion-icon name="location" slot="start"></ion-icon>
        Obtener Ubicación
      </ion-button>
      
      <ion-card *ngIf="coordenadas">
        <ion-card-header>
          <ion-card-title>Tu ubicación actual</ion-card-title>
        </ion-card-header>
        <ion-card-content>
          <p><strong>Latitud:</strong> {{ coordenadas.latitude }}</p>
          <p><strong>Longitud:</strong> {{ coordenadas.longitude }}</p>
          <p><strong>Precisión:</strong> {{ coordenadas.accuracy }} metros</p>
        </ion-card-content>
      </ion-card>
    </ion-content>
  `
})
export class GeolocalizacionPage {
  coordenadas: {
    latitude: number;
    longitude: number;
    accuracy: number;
  } | undefined;
  
  constructor() { }
  
  async obtenerUbicacion() {
    try {
      // Solicitar permisos primero
      const permisos = await Geolocation.checkPermissions();
      
      if (permisos.location !== 'granted') {
        const solicitado = await Geolocation.requestPermissions();
        if (solicitado.location !== 'granted') {
          throw new Error('Permiso de ubicación denegado');
        }
      }
      
      const position = await Geolocation.getCurrentPosition({
        enableHighAccuracy: true
      });
      
      this.coordenadas = {
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        accuracy: position.coords.accuracy
      };
    } catch (error) {
      console.error('Error al obtener ubicación', error);
    }
  }
}

Resumen

En este capítulo hemos explorado a fondo los componentes y estructura de una aplicación Angular/Ionic:

  1. La estructura de carpetas sigue una organización lógica que separa código, estilos y recursos
  2. Los componentes Angular encapsulan HTML, CSS y lógica en unidades coherentes
  3. El enlace de datos permite la comunicación bidireccional entre vistas y lógica
  4. Las directivas extienden HTML con funcionalidades dinámicas
  5. Ionic proporciona componentes UI que se adaptan a iOS y Android automáticamente
  6. Los servicios permiten compartir lógica y estado entre componentes
  7. El enrutamiento facilita la navegación entre diferentes vistas
  8. Capacitor extiende las aplicaciones con acceso a funcionalidades nativas

Con estos fundamentos, estamos listos para combinar todos estos conceptos en una aplicación completa en el siguiente capítulo.

Recursos Adicionales