# Taller: Desarrollo con Angular e Ionic
## Capítulo 1: Introducción a Angular e Ionic
## 1. ¿Qué es Angular?
Angular es un framework de desarrollo front-end mantenido por Google, diseñado para crear aplicaciones web de una sola página (SPA) y aplicaciones móviles. Algunas características clave:
- **Basado en componentes**: Toda la interfaz se construye mediante componentes reutilizables
- **TypeScript**: Utiliza TypeScript como lenguaje principal, añadiendo tipos estáticos a JavaScript
- **Completo**: Ofrece soluciones integradas para enrutamiento, formularios, HTTP, animaciones, etc.
- **Modular**: Su arquitectura permite dividir la aplicación en módulos funcionales
- **Reactivo**: Facilita la programación reactiva mediante RxJS
Angular utiliza un sistema de "detección de cambios" para mantener sincronizada la interfaz de usuario con el estado de la aplicación, lo que permite crear interfaces dinámicas y reactivas.
## 2. Angular CLI: La Herramienta de Línea de Comandos
El Angular CLI (Command Line Interface) es una herramienta oficial que simplifica enormemente el desarrollo con Angular:
```bash
# Instalación global
npm install -g @angular/cli
# Crear nuevo proyecto
ng new mi-proyecto
# Generar componentes, servicios, etc.
ng generate component mi-componente
ng generate service mi-servicio
# Iniciar servidor de desarrollo
ng serve
# Construir para producción
ng build --prod
```
Beneficios del CLI:
- **Scaffolding**: Generación de código con estructura y configuración correctas
- **Herramientas de desarrollo**: Servidor local con recarga en vivo (live reload)
- **Optimización**: Empaquetado y minificación para producción
- **Testing**: Configuración automática para pruebas unitarias e integración
- **Actualización**: Facilita la actualización entre versiones de Angular
## 3. Angular vs Desarrollo Web Tradicional
Angular se asemeja al desarrollo de páginas web normales, pero con una estructura y enfoque diferentes:
| Desarrollo Web Tradicional | Desarrollo con Angular |
|---------------------------|------------------------|
| Páginas HTML separadas | Aplicación de una sola página (SPA) |
| jQuery para manipular DOM | Vinculación de datos bidireccional |
| JavaScript vanilla | TypeScript con tipos estáticos |
| Recarga completa entre páginas | Navegación sin recarga (enrutamiento SPA) |
| Mezcla de lógica y presentación | Separación modelo-vista-controlador |
| Scripts y estilos globales | Encapsulación de componentes |
A pesar de las diferencias, los conocimientos de HTML, CSS y JavaScript son totalmente aplicables en Angular, ya que seguimos trabajando con estos lenguajes fundamentales pero de manera estructurada.
## 4. Componentes Básicos de Angular
#### Componentes
El elemento fundamental de las aplicaciones Angular:
```typescript
import { Component } from '@angular/core';
@Component({
selector: 'app-contador',
template: `
Contador: {{ contador }}
`,
styles: [`
div { text-align: center; }
button { margin: 0 5px; }
`]
})
export class ContadorComponent {
contador = 0;
incrementar() {
this.contador++;
}
decrementar() {
this.contador--;
}
}
```
#### Módulos
Organizan la aplicación en bloques funcionales:
```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ContadorComponent } from './contador/contador.component';
@NgModule({
declarations: [
AppComponent,
ContadorComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
```
#### Servicios
Encapsulan la lógica de negocio y son inyectables en componentes:
```typescript
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DatosService {
private datos = ['Primer elemento', 'Segundo elemento'];
getDatos() {
return this.datos;
}
agregarDato(dato: string) {
this.datos.push(dato);
}
}
```
#### Directivas
Modifican el comportamiento del DOM:
```html
{{ item }}
Este contenido es condicional
Texto con estilo dinámico
```
#### Pipes
Transforman datos para su visualización:
```html
{{ fecha | date:'dd/MM/yyyy' }}
{{ precio | currency:'EUR' }}
{{ nombre | uppercase }}
{{ textoLargo | slice:0:50 }}...
```
## 5. ¿Qué es Ionic?
Ionic es un framework de desarrollo de aplicaciones móviles híbridas que se construye sobre Angular:
- **Multiplataforma**: Una única base de código para iOS, Android y web
- **Componentes nativos**: UI con apariencia y comportamiento nativo
- **Capacitor/Cordova**: Acceso a APIs nativas del dispositivo
- **Rendimiento optimizado**: Aplicaciones rápidas y responsivas
## 6. Componentes UI de Ionic
Ionic ofrece una amplia biblioteca de componentes que siguen las directrices de diseño de iOS y Android:
#### Navegación y Estructura
```html
InicioBuscarPerfilMenúInicioPerfilConfiguración
```
> **Nota:** Para ver ejemplos interactivos de estos componentes, visite la documentación oficial:
> - [Ionic Tabs](https://ionicframework.com/docs/api/tabs)
> - [Ionic Menu](https://ionicframework.com/docs/api/menu)
#### Componentes Básicos
```html
Botón EstándarBotón OutlineBotón BlockSubtítuloTítulo Principal
Contenido detallado de la tarjeta que puede incluir
texto, imágenes y otros elementos.
Usuarios
Juan Pérez
Desarrollador
María García
Diseñadora
```
> **Nota:** Puede visualizar estos componentes en la documentación oficial:
> - [Ionic Buttons](https://ionicframework.com/docs/api/button)
> - [Ionic Cards](https://ionicframework.com/docs/api/card)
> - [Ionic Lists](https://ionicframework.com/docs/api/list)
#### Formularios y Entrada
```html
NombreEmailCategoríaDeportesMúsicaTecnologíaNotificacionesAcepto términos
```
> **Nota:** Consulte la documentación oficial para ver ejemplos interactivos:
> - [Ionic Input](https://ionicframework.com/docs/api/input)
> - [Ionic Select](https://ionicframework.com/docs/api/select)
> - [Ionic Toggle](https://ionicframework.com/docs/api/toggle)
> - [Ionic Checkbox](https://ionicframework.com/docs/api/checkbox)
#### Feedback y Alertas
```typescript
import { AlertController, ToastController, LoadingController } from '@ionic/angular';
constructor(
private alertCtrl: AlertController,
private toastCtrl: ToastController,
private loadingCtrl: LoadingController
) {}
async mostrarAlerta() {
const alert = await this.alertCtrl.create({
header: 'Alerta',
subHeader: 'Información importante',
message: '¿Estás seguro de realizar esta acción?',
buttons: ['Cancelar', 'Aceptar']
});
await alert.present();
}
async mostrarToast() {
const toast = await this.toastCtrl.create({
message: 'Operación completada con éxito',
duration: 2000,
position: 'bottom',
color: 'success'
});
toast.present();
}
async mostrarCargando() {
const loading = await this.loadingCtrl.create({
message: 'Cargando datos...',
duration: 2000
});
await loading.present();
}
```
> **Nota:** Para más información sobre alertas y elementos interactivos, consulte:
> - [Ionic Alert](https://ionicframework.com/docs/api/alert)
> - [Ionic Toast](https://ionicframework.com/docs/api/toast)
> - [Ionic Loading](https://ionicframework.com/docs/api/loading)
## 7. Capacitor: El Puente Nativo
Capacitor es el framework que permite a Ionic acceder a las capacidades nativas del dispositivo:
- Cámara y galería de fotos
- Geolocalización
- Almacenamiento persistente
- Notificaciones push
- Sensores del dispositivo
- Archivos y sistema de archivos
Ejemplo básico de uso de Capacitor:
```typescript
import { Camera, CameraResultType } from '@capacitor/camera';
async function tomarFoto() {
const imagen = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri
});
// Usar la imagen (imagen.webPath)
const imagenUrl = imagen.webPath;
}
```
## 8. Ciclo de Desarrollo con Ionic y Angular
El proceso típico de desarrollo con Ionic y Angular incluye:
1. **Creación del proyecto**:
```bash
# Crear un nuevo proyecto (por defecto usa Angular)
ionic start mi-proyecto-ionic
# Especificar el framework (angular, react, vue)
ionic start mi-proyecto-ionic --type=angular
```
2. **Desarrollo en navegador web**:
```bash
# Iniciar servidor de desarrollo con recarga en vivo
ionic serve
```
3. **Pruebas en dispositivo real**:
```bash
# Añadir plataforma Android
ionic capacitor add android
# Ejecutar en dispositivo con recarga en vivo
ionic capacitor run android --livereload
```
4. **Compilación para producción**:
```bash
# Construir la aplicación optimizada
ionic build --prod
# Copiar los archivos a las plataformas nativas
npx cap copy
# Abrir el proyecto en Android Studio para ajustes finales
npx cap open android
```
## Resumen
Angular e Ionic forman una poderosa combinación para desarrollar aplicaciones móviles multiplataforma:
- Angular proporciona la estructura, organización y lógica
- Ionic aporta componentes UI con aspecto nativo
- Capacitor permite acceder a características nativas del dispositivo
- El desarrollo es similar al web tradicional, pero más estructurado y optimizado
En los siguientes capítulos, exploraremos más a fondo cada uno de estos conceptos y construiremos paso a paso una aplicación completa de reservas para un gimnasio, además de la creación básica de un proyecto vacío.
## Recursos y Documentación
- [Documentación oficial de Angular](https://angular.io/docs)
- [Documentación de Ionic Framework](https://ionicframework.com/docs)
- [Capacitor Docs](https://capacitorjs.com/docs)
- [Angular CLI](https://cli.angular.io/)
- [TypeScript](https://www.typescriptlang.org/docs/)
# 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
```typescript
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
```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DatosService } from './datos.service';
@Component({
selector: 'app-ejemplo',
template: '
{{datos}}
'
})
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:
```html
{{ titulo }}
Precio: {{ precio | currency }}
Estado: {{ disponible ? 'En stock' : 'Agotado' }}
```
### Property Binding [ ]
Vincula propiedades HTML con valores del componente:
```html
Contenido con clases dinámicas
```
### Event Binding ( )
Responde a eventos del usuario:
```html
```
### Two-Way Binding [( )]
Combina property binding y event binding para actualizar datos en ambas direcciones:
```html
```
> 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:
```html
```
### Directivas de Atributo
Modifican la apariencia o comportamiento de elementos existentes:
```html
Elemento con clases dinámicas
Texto con estilo dinámico
```
## 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
```html
Mi AplicaciónBienvenido
Contenido de ejemplo para mostrar un card básico.
Elemento 1Elemento 2
```
### Controladores de Componentes
Para componentes interactivos como alertas, modales o toasts, se utilizan controladores:
```typescript
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](https://ionicframework.com/docs/components), 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
```bash
ionic generate service services/productos
```
```typescript
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 {
return this.http.get(this.apiUrl);
}
obtenerProductoPorId(id: number): Observable {
return this.http.get(`${this.apiUrl}/${id}`);
}
buscarProductos(termino: string): Observable {
return this.http.get(this.apiUrl).pipe(
map(productos => productos.filter(p =>
p.nombre.toLowerCase().includes(termino.toLowerCase())
))
);
}
agregarProducto(producto: Producto): Observable {
return this.http.post(this.apiUrl, producto);
}
actualizarProducto(producto: Producto): Observable {
return this.http.put(`${this.apiUrl}/${producto.id}`, producto);
}
eliminarProducto(id: number): Observable {
return this.http.delete(`${this.apiUrl}/${id}`);
}
}
```
### Uso del Servicio en un Componente
```typescript
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:
```typescript
// 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
```html
Ver productos
{{ producto.nombre }}
Ver electrónica
```
### Navegación Programática
```typescript
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
```typescript
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
```bash
npm install @capacitor/camera
npx cap sync
```
```typescript
import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
@Component({
selector: 'app-camara',
template: `
Cámara
Tomar Foto
`
})
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
```bash
npm install @capacitor/geolocation
npx cap sync
```
```typescript
import { Component } from '@angular/core';
import { Geolocation, Position } from '@capacitor/geolocation';
@Component({
selector: 'app-geolocalizacion',
template: `
Ubicación
Obtener Ubicación
Tu ubicación actual
Latitud: {{ coordenadas.latitude }}
Longitud: {{ coordenadas.longitude }}
Precisión: {{ coordenadas.accuracy }} metros
`
})
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
- [Guía de Componentes Angular](https://angular.io/guide/component-overview)
- [Ciclo de Vida en Angular](https://angular.io/guide/lifecycle-hooks)
- [Directivas en Angular](https://angular.io/guide/attribute-directives)
- [Componentes UI de Ionic](https://ionicframework.com/docs/components)
- [Servicios e Inyección de Dependencias](https://angular.io/guide/dependency-injection)
- [Router de Angular](https://angular.io/guide/router)
- [Documentación de Capacitor](https://capacitorjs.com/docs/apis)
# Taller: Desarrollo con Angular e Ionic
## Capítulo 3: Estructura de la Aplicación de Reservas para Gimnasio
En este capítulo, detallaremos la estructura y planificación de nuestra aplicación de reservas para gimnasio. Esto nos servirá como guía para el desarrollo que realizaremos en los siguientes capítulos.
## 1. Definición de Requisitos
Nuestra aplicación de reservas para gimnasio debe cumplir con los siguientes requisitos:
### Funcionalidades Principales
- **Ver clases disponibles**: Listado de clases con información sobre horarios, instructores y plazas
- **Reservar clases**: Posibilidad de reservar una clase si hay plazas disponibles
- **Gestionar reservas**: Ver, modificar y cancelar reservas existentes
- **Perfil de usuario**: Información básica del usuario y preferencias
- **Notificaciones**: Recordatorios de clases reservadas
### Tipos de Usuario
- **Clientes**: Pueden ver clases, hacer reservas y gestionar su perfil
- **Administradores**: Pueden gestionar clases, instructores y ver estadísticas (para una versión futura)
## 2. Planificación de la Estructura
### Entidades Principales
1. **Clases (GymClass)**
- ID
- Nombre
- Descripción
- Instructor
- Horario (Inicio/Fin)
- Capacidad máxima
- Reservas actuales
2. **Reservas (Booking)**
- ID
- ID de Usuario
- ID de Clase
- Fecha
- Estado (Confirmada, Cancelada, Pendiente)
3. **Usuario (User)**
- ID
- Nombre
- Email
- Foto de perfil
- Preferencias
### Páginas Principales
1. **Inicio/Login**
- Autenticación de usuarios
- Pantalla de bienvenida
2. **Lista de Clases**
- Mostrar todas las clases disponibles
- Filtros por día, tipo, instructor
3. **Detalle de Clase**
- Información completa de la clase
- Botón para reservar
- Lista de participantes (opcional)
4. **Mis Reservas**
- Lista de reservas del usuario
- Opciones para cancelar/modificar
5. **Perfil**
- Datos del usuario
- Preferencias
- Estadísticas (opcional)
## 3. Diseño de la Arquitectura
### Estructura de Carpetas
```
gym-reservation-app/
├── src/
│ ├── app/
│ │ ├── core/ # Servicios core, guards, interceptores
│ │ │ ├── services/
│ │ │ ├── guards/
│ │ │ └── interceptors/
│ │ ├── shared/ # Componentes compartidos
│ │ │ ├── components/
│ │ │ ├── directives/
│ │ │ └── pipes/
│ │ ├── models/ # Interfaces y modelos
│ │ ├── pages/ # Páginas principales
│ │ │ ├── auth/
│ │ │ ├── classes/
│ │ │ ├── class-detail/
│ │ │ ├── bookings/
│ │ │ └── profile/
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ └── app-routing.module.ts
│ ├── assets/
│ │ ├── icon/
│ │ └── images/
│ ├── environments/
│ └── theme/
└── capacitor.config.ts
```
### Arquitectura y Patrones
Utilizaremos una arquitectura basada en:
1. **Componentes**: Encapsulan partes de la UI y su lógica asociada
2. **Servicios**: Manejan la lógica de negocio y el acceso a datos
3. **Modelos**: Definen las estructuras de datos
4. **Enrutamiento**: Gestiona la navegación entre páginas
5. **Patrón Observable**: Para gestionar flujos de datos asincrónicos
## 4. Diseño de la Base de Datos
Para esta versión inicial, utilizaremos almacenamiento local mediante Capacitor/Storage. En una versión futura, podríamos migrar a Firebase o una API REST.
**Estructura del almacenamiento local**:
```json
{
"classes": [
{
"id": "1",
"name": "Yoga",
"description": "Clase de yoga para todos los niveles",
"instructor": "María López",
"startTime": "2025-04-22T08:00:00",
"endTime": "2025-04-22T09:00:00",
"maxCapacity": 15,
"currentBookings": 8
},
// Más clases...
],
"bookings": [
{
"id": "1",
"userId": "user123",
"classId": "1",
"className": "Yoga",
"date": "2025-04-22T08:00:00",
"status": "confirmed"
},
// Más reservas...
],
"users": [
{
"id": "user123",
"name": "Usuario Demo",
"email": "usuario@ejemplo.com",
"profilePic": "avatar.jpg",
"preferences": {
"notifications": true,
"favoriteClasses": ["1", "3"]
}
},
// Más usuarios...
]
}
```
## 5. Flujos de Navegación
### Flujo Principal
1. **Inicio**: El usuario abre la aplicación
2. **Autenticación**: El usuario inicia sesión (simulado en esta versión)
3. **Tabs**: Navegación principal con tabs
- Tab 1: Clases disponibles
- Tab 2: Mis reservas
- Tab 3: Perfil
### Flujo de Reserva
1. Usuario navega a "Clases disponibles"
2. Usuario selecciona una clase específica
3. En la pantalla de detalle, pulsa "Reservar Clase"
4. Se muestra confirmación y se redirige a "Mis Reservas"
### Flujo de Cancelación
1. Usuario navega a "Mis Reservas"
2. Usuario desliza una reserva hacia la izquierda
3. Pulsa en "Cancelar"
4. Se muestra confirmación y se actualiza la lista
## 6. Integración con Capacitor
Para mejorar la experiencia de usuario y aprovechar las capacidades nativas, integraremos los siguientes plugins de Capacitor:
1. **@capacitor/local-notifications**
- Enviar recordatorios de clases
- Notificaciones de confirmación de reservas
2. **@capacitor/camera**
- Permitir a los usuarios actualizar su foto de perfil
- Opción de tomar foto o seleccionar de la galería
3. **@capacitor/preferences (antes Storage)**
- Almacenar datos de usuario
- Guardar preferencias
- Persistir información de clases y reservas
4. **@capacitor/device** (opcional)
- Obtener información del dispositivo para personalizar la experiencia
## 7. Plan de Implementación
Para desarrollar la aplicación seguiremos estos pasos:
1. **Configuración del proyecto**
- Crear proyecto Ionic con Angular
- Instalar dependencias necesarias
2. **Implementación de los modelos de datos**
- Definir interfaces TypeScript para las entidades
3. **Servicios Core**
- Implementar servicios para gestionar clases
- Implementar servicios para gestionar reservas
- Implementar servicio de autenticación (mock)
4. **Componentes Shared**
- Crear componentes reutilizables (tarjetas, listas, etc.)
5. **Páginas principales**
- Implementar página de listado de clases
- Implementar página de detalle de clase
- Implementar página de mis reservas
- Implementar página de perfil
6. **Navegación y enrutamiento**
- Configurar las rutas
- Implementar navegación entre páginas
7. **Capacitor y funcionalidades nativas**
- Integrar notificaciones
- Integrar cámara
- Implementar almacenamiento persistente
8. **Pruebas y ajustes**
- Probar funcionalidades
- Ajustar estilos y mejorar UX
## 8. Consideraciones para Versiones Futuras
En futuras versiones se podrían incluir:
1. **Autenticación real**
- Integración con Firebase Auth o similares
- Roles de usuario (cliente/admin)
2. **Backend real**
- API REST o Firebase para datos en tiempo real
- Sincronización entre dispositivos
3. **Funcionalidades avanzadas**
- Pagos para clases premium
- Calendarios personalizados
- Estadísticas de asistencia
- Chat con instructores
4. **Mejoras UX/UI**
- Temas personalizables
- Animaciones
- Modo oscuro/claro
## Conclusión
En este capítulo hemos definido la estructura de nuestra aplicación de reservas para gimnasio, estableciendo una base sólida para el desarrollo. Con esta planificación, podemos proceder a la implementación paso a paso, siguiendo las mejores prácticas de Angular e Ionic.
En el siguiente capítulo, comenzaremos con la implementación práctica, creando el proyecto base y desarrollando los modelos y servicios esenciales.
# Capítulo 4: Desarrollo Práctico de la Aplicación de Reservas
En este capítulo comenzaremos el desarrollo práctico de nuestra aplicación de reservas para gimnasio utilizando Ionic y Angular. Seguiremos paso a paso la implementación desde cero.
## 1. Creación y Configuración del Proyecto
### Crear el proyecto
Lo primero es crear el proyecto mediante el CLI de Ionic:
> Es necesario recordar de que se tiene que tener node instalado, en este caso yo personalmente estoy usando la versión LTS de node (22.14)
```bash
# Asegúrate de tener instalado Ionic CLI y Angular CLI
npm install -g @ionic/cli @angular/cli
# Crear el proyecto con el template de tabs
ionic start taller tabs --type=angular
# Entrar al directorio del proyecto
cd taller
```
![\[img\]https://i.ibb.co/Xx1kxbMn/instalar-cli.png\[/img\]](https://i.ibb.co/Xx1kxbMn/instalar-cli.png)

> Últimamente Angular impulsa mucho el uso de los conocidos como `componentes independientes`, personalmente no me gustan mucho, así que para efectos de la demostración lo haré con la versión clásica con módulos
### Estructura inicial del proyecto
Veamos la estructura de carpetas generada:
```
taller/
├── src/
│ ├── app/
│ │ ├── tab1/ # Tab 1 generado automáticamente
│ │ ├── tab2/ # Tab 2 generado automáticamente
│ │ ├── tab3/ # Tab 3 generado automáticamente
│ │ ├── tabs/ # Controlador de tabs
│ │ ├── app-routing.module.ts
│ │ ├── app.component.ts
│ │ └── app.module.ts
│ ├── assets/
│ ├── environments/
│ ├── theme/
│ ├── global.scss
│ ├── index.html
│ └── main.ts
├── angular.json
├── capacitor.config.ts
├── package.json
└── ...
```
### Ejecutar el proyecto
Iniciemos la aplicación para verificar que todo funciona correctamente:
```bash
ionic serve
```
> Literalmente usa exactamente el mismo servidor de desarrollo que una típica aplicación de Angular, aunque lo único que cambia en este punto es el puerto por defecto, ahora es el 8100 en vez del 4200, de igual manera se puede cambiar sin problemas


Deberías ver la aplicación por defecto en tu navegador, con tres tabs básicos.
## 2. Organización del Proyecto
Vamos a reorganizar la estructura para adaptarla a nuestras necesidades:
```bash
# Crear directorios para nuestra estructura, puedes crearlos manualmente,
# o en mi caso que estoy usando una distribución linux los haré con estos comandos
mkdir -p src/app/pages
mkdir -p src/app/services
mkdir -p src/app/models
mkdir -p src/app/shared/components
```
## 3. Definición de Modelos
Creemos los modelos de datos que necesitaremos:
```bash
# Crear archivos para los modelos
# lo mismo de arriba, puedes crearlos manualmente, en mi caso usaré estos comandos
touch src/app/models/gym-class.model.ts
touch src/app/models/booking.model.ts
touch src/app/models/user.model.ts
```
Ahora implementemos las interfaces para cada modelo:
**gym-class.model.ts**:
```typescript
export interface GymClass {
id: string;
name: string;
description: string;
instructor: string;
startTime: Date;
endTime: Date;
maxCapacity: number;
currentBookings: number;
category?: string;
imageUrl?: string;
}
```
**booking.model.ts**:
```typescript
export interface Booking {
id: string;
userId: string;
classId: string;
className: string;
date: Date;
status: 'confirmed' | 'cancelled' | 'pending';
}
```
**user.model.ts**:
```typescript
export interface User {
id: string;
name: string;
email: string;
profilePic?: string;
profilePicUrl?: string; // Para compatibilidad con el backend
notificationsEnabled?: boolean; // Para compatibilidad con el backend
preferences?: {
notifications: boolean;
favoriteClasses?: string[];
};
}
```
## 4. Creación de Servicios
Ahora, implementaremos los servicios que gestionarán los datos:
```bash
# Generar servicios usando el CLI de Ionic
ionic generate service services/classes
ionic generate service services/bookings
ionic generate service services/auth
ionic generate service services/storage
ionic generate service services/upload
ionic generate service services/notification
```
### Implementación del Servicio de Almacenamiento
Primero vamos a instalar el plugin de Preferences de Capacitor:
> Este plugin es básicamente para guardar datos o caché de nuestra app, en caso de que la exportemos para móviles como Android o iOS
```bash
npm install @capacitor/preferences
```
Ahora implementemos el servicio de almacenamiento:
**services/storage.service.ts**:
```typescript
import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';
@Injectable({
providedIn: 'root'
})
export class StorageService {
constructor() { }
async set(key: string, value: any): Promise {
await Preferences.set({
key,
value: JSON.stringify(value)
});
}
async get(key: string): Promise {
const { value } = await Preferences.get({ key });
if (value) {
return JSON.parse(value);
}
return null;
}
async remove(key: string): Promise {
await Preferences.remove({ key });
}
async clear(): Promise {
await Preferences.clear();
}
}
```
## 5. Configuración de Entornos
Configuremos los archivos de entorno para facilitar el cambio entre desarrollo y producción:
**src/environments/environment.ts**:
```typescript
export const environment = {
production: false,
apiUrl: 'http://localhost:3000' // URL de la API de desarrollo
};
```
**src/environments/environment.prod.ts**:
```typescript
export const environment = {
production: true,
apiUrl: 'https://taller.ionic.lcespedes.dev' // URL de la API de producción
};
```
## 6. Integración con Backend
Nuestra aplicación se conectará a un backend real en lugar de utilizar datos mockeados. Instalemos las dependencias necesarias:
```bash
npm install @angular/common/http
```
### Configuración del Módulo HTTP
Agreguemos el módulo HTTP al app.module.ts:
```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
HttpClientModule
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
```
## 7. Implementación de Autenticación
Crearemos un módulo de autenticación completo:
```bash
ionic generate page pages/auth
ionic generate component pages/auth/components/login
ionic generate component pages/auth/components/register
ionic generate guard pages/auth/guards/auth
```
### Implementación del Guard de Autenticación
**src/app/pages/auth/guards/auth.guard.ts**:
```typescript
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
) {}
async canActivate(): Promise {
// Esperar a que el servicio de autenticación esté inicializado
await this.authService.waitForInitialization();
if (this.authService.isLoggedIn()) {
return true;
}
// Redirigir al login si no hay sesión
this.router.navigateByUrl('/auth');
return false;
}
}
```
### Configuración de Rutas Principales
Actualizaremos el app-routing.module.ts para implementar las rutas correctas:
```typescript
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './pages/auth/guards/auth.guard';
const routes: Routes = [
{
path: '',
redirectTo: 'auth',
pathMatch: 'full'
},
{
path: 'app',
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule),
canActivate: [AuthGuard]
},
{
path: 'auth',
loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
},
{
path: 'classes',
loadChildren: () => import('./pages/classes/classes.module').then(m => m.ClassesPageModule)
},
{
path: 'class-detail',
loadChildren: () => import('./pages/class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
},
{
path: 'bookings',
loadChildren: () => import('./pages/bookings/bookings.module').then(m => m.BookingsPageModule)
},
{
path: 'profile',
loadChildren: () => import('./pages/profile/profile.module').then(m => m.ProfilePageModule)
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule {}
```
## 8. Creación de Páginas Principales
Vamos a crear las páginas principales de nuestra aplicación:
```bash
# Eliminar las páginas de tabs generadas automáticamente
rm -rf src/app/tab1
rm -rf src/app/tab2
rm -rf src/app/tab3
# Generar nuestras propias páginas
ionic generate page pages/classes
ionic generate page pages/class-detail
ionic generate page pages/bookings
ionic generate page pages/profile
```
### Configuración de las rutas y tabs
Actualizamos el archivo de rutas de tabs:
**src/app/tabs/tabs-routing.module.ts**:
```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
const routes: Routes = [
{
path: '',
component: TabsPage,
children: [
{
path: 'classes',
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
},
{
path: 'bookings',
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
},
{
path: 'profile',
loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
},
{
path: '',
redirectTo: '/app/classes',
pathMatch: 'full'
}
]
},
{
path: '',
redirectTo: '/app/classes',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class TabsPageRoutingModule {}
```
Actualizamos el template de tabs:
**src/app/tabs/tabs.page.html**:
```html
ClasesMis ReservasPerfil
```
## 9. Implementar Servicio de Autenticación
Ahora implementaremos el servicio de autenticación real que se comunica con el backend:
**services/auth.service.ts**:
```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
import { environment } from '../../environments/environment';
import { StorageService } from './storage.service';
// Clave para almacenar información de sesión
const AUTH_TOKEN_KEY = 'auth_token';
const USER_KEY = 'user_data';
// Interfaz para credenciales de login
interface LoginCredentials {
email: string;
password: string;
}
// Interfaz para datos de registro
interface RegisterData {
name: string;
email: string;
password: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = `${environment.apiUrl}/api/users`;
private authUrl = `${environment.apiUrl}/api/auth`;
private currentUserSubject = new BehaviorSubject(null);
public currentUser$ = this.currentUserSubject.asObservable();
private isInitialized = false;
constructor(
private http: HttpClient,
private storageService: StorageService
) {
// Intentar recuperar sesión anterior
this.initializeSession();
}
/**
* Inicializa la sesión si hay datos guardados
*/
async initializeSession() {
if (this.isInitialized) return;
try {
const userData = await this.storageService.get(USER_KEY) as User | null;
if (userData) {
console.log('Sesión recuperada del almacenamiento local:', userData);
// Normalizar campos para compatibilidad
this.normalizeUserData(userData);
this.currentUserSubject.next(userData);
} else {
console.log('No hay sesión guardada, redirigiendo a login');
this.currentUserSubject.next(null);
}
} catch (error) {
console.error('Error al inicializar sesión:', error);
this.currentUserSubject.next(null);
}
this.isInitialized = true;
}
/**
* Espera a que se complete la inicialización
* Importante para evitar redirecciones erróneas cuando la app inicia
*/
async waitForInitialization(): Promise {
if (this.isInitialized) return;
// Si no se ha inicializado, iniciamos el proceso
await this.initializeSession();
// Esperar hasta que se inicialice (con timeout)
let attempts = 0;
while (!this.isInitialized && attempts < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
}
/**
* Normaliza los datos de usuario para compatibilidad entre frontend/backend
*/
private normalizeUserData(user: User) {
// Sincronizar campos de imagen
if (user.profilePicUrl && !user.profilePic) {
user.profilePic = user.profilePicUrl;
} else if (user.profilePic && !user.profilePicUrl) {
user.profilePicUrl = user.profilePic;
}
// Sincronizar preferencias de notificaciones
if (user.notificationsEnabled !== undefined && !user.preferences) {
user.preferences = {
notifications: user.notificationsEnabled,
favoriteClasses: []
};
} else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
user.notificationsEnabled = user.preferences.notifications;
}
}
getCurrentUser(): User | null {
return this.currentUserSubject.value;
}
/**
* Iniciar sesión con email y contraseña
*/
login(credentials: LoginCredentials): Observable {
console.log('Intentando login con:', credentials);
return this.http.post(`${this.authUrl}/login`, credentials).pipe(
tap(async (response) => {
console.log('Respuesta de login:', response);
// Guardar token si el backend lo devuelve
if (response.token) {
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
}
// Verificar y normalizar datos del usuario
const user = response.user || response;
this.normalizeUserData(user);
// Guardar datos del usuario
await this.storageService.set(USER_KEY, user);
// Actualizar estado de sesión
this.currentUserSubject.next(user);
}),
map(response => response.user || response)
);
}
/**
* Registrar un nuevo usuario
*/
register(userData: RegisterData): Observable {
console.log('Registrando usuario:', userData);
const registerData = {
name: userData.name,
email: userData.email,
password: userData.password
};
return this.http.post(`${this.authUrl}/register`, registerData).pipe(
tap(async (response) => {
console.log('Respuesta de registro:', response);
// Guardar token si el backend lo devuelve
if (response.token) {
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
}
// Verificar y normalizar datos del usuario
const user = response.user || response;
this.normalizeUserData(user);
// Guardar datos del usuario
await this.storageService.set(USER_KEY, user);
// Actualizar estado de sesión
this.currentUserSubject.next(user);
}),
map(response => response.user || response)
);
}
updateUserProfile(userData: Partial): Observable {
const currentUser = this.getCurrentUser();
if (!currentUser) {
return of(currentUser as unknown as User);
}
console.log('Actualizando perfil de usuario con:', userData);
// Asegurar que ambos campos de imagen estén sincronizados
if (userData.profilePic && !userData.profilePicUrl) {
userData.profilePicUrl = userData.profilePic;
} else if (userData.profilePicUrl && !userData.profilePic) {
userData.profilePic = userData.profilePicUrl;
}
// Sincronizar notificaciones entre los dos formatos
if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) {
userData.notificationsEnabled = userData.preferences.notifications;
} else if (userData.notificationsEnabled !== undefined &&
(!userData.preferences || userData.preferences.notifications === undefined)) {
if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] };
userData.preferences.notifications = userData.notificationsEnabled;
}
// Solo enviamos al servidor los campos que espera
const backendUserData = {
name: userData.name,
profilePicUrl: userData.profilePicUrl,
notificationsEnabled: userData.notificationsEnabled ||
(userData.preferences ? userData.preferences.notifications : undefined)
};
return this.http.put(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
tap(async (user) => {
console.log('Usuario actualizado correctamente:', user);
// Sincronizar campos para mantener compatibilidad
if (user.profilePicUrl && !user.profilePic) {
user.profilePic = user.profilePicUrl;
} else if (user.profilePic && !user.profilePicUrl) {
user.profilePicUrl = user.profilePic;
}
// Mantener los campos que espera el frontend
if (user.notificationsEnabled !== undefined && !user.preferences) {
user.preferences = {
notifications: user.notificationsEnabled,
favoriteClasses: currentUser.preferences?.favoriteClasses || []
};
}
// Actualizar almacenamiento local
await this.storageService.set(USER_KEY, user);
this.currentUserSubject.next(user);
}),
catchError(error => {
console.error('Error al actualizar usuario:', error);
// Devolver el usuario actualizado localmente para que la UI no se rompa
// en caso de error de red
const updatedUser = { ...currentUser, ...userData };
this.currentUserSubject.next(updatedUser);
return of(updatedUser);
})
);
}
/**
* Cerrar sesión y eliminar datos
*/
async logout(): Promise {
try {
// En una aplicación real, enviaríamos petición al backend
// this.http.post(`${this.authUrl}/logout`, {}).subscribe();
// Limpiar datos de sesión
await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
// Actualizar estado
this.currentUserSubject.next(null);
this.isInitialized = false; // Permitir nueva inicialización
return true;
} catch (error) {
console.error('Error al cerrar sesión:', error);
return false;
}
}
/**
* Verificar si hay sesión activa
*/
isLoggedIn(): boolean {
return !!this.currentUserSubject.value;
}
}
```
## 10. Creación de Componentes de Login y Registro
Implementemos los componentes de autenticación:
**pages/auth/components/login/login.component.html**:
```html
Tu app para reservar clases en tu gimnasio favorito
Iniciar SesiónRegistrarse
¿Quieres probar la app? Usa estos datos:
Email: usuario@ejemplo.com
Contraseña: password123
```
## 11. Servicio para Subir Imágenes
Implementemos un servicio para manejar la carga de imágenes al backend:
**services/upload.service.ts**:
```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class UploadService {
private apiUrl = `${environment.apiUrl}/api/upload`;
constructor(private http: HttpClient) { }
/**
* Sube una imagen al servidor
* @param file Archivo a subir
* @param type Tipo de imagen (avatar, class, etc)
* @returns URL de la imagen subida
*/
uploadImage(file: File, type: 'avatar' | 'class' = 'avatar'): Observable<{ imageUrl: string }> {
const formData = new FormData();
formData.append('image', file);
formData.append('type', type);
return this.http.post<{ imageUrl: string }>(`${this.apiUrl}`, formData);
}
/**
* Convierte una imagen dataURL (base64) a un archivo File
* @param dataUrl URL de datos base64
* @param filename Nombre del archivo
*/
dataURLtoFile(dataUrl: string, filename: string): File {
const arr = dataUrl.split(',');
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
}
```
## Resumen
En este capítulo, hemos establecido las bases sólidas para nuestra aplicación de reservas para gimnasio:
1. Creamos la estructura del proyecto
2. Implementamos los modelos de datos
3. Configuramos los servicios básicos
4. Establecimos la autenticación con un backend real
5. Implementamos las rutas principales
6. Creamos las páginas y componentes esenciales
7. Integramos el sistema de subida de imágenes
En el siguiente capítulo, completaremos la implementación con:
- Página de listado de clases
- Detalle de clase y reserva
- Gestión de reservas
- Perfil de usuario
- Integración con capacidades nativas mediante Capacitor
## Disclaimer
> **Nota importante:** Si durante el desarrollo encuentras errores o discrepancias en la ejecución del código, verifica la implementación correcta de cada módulo. El código completo de cada componente está disponible en el repositorio del curso. Asegúrate de revisar que los nombres de las clases, servicios e importaciones coincidan exactamente con lo especificado en este tutorial.
>
> Si tienes problemas con algún módulo específico, puedes verificar la estructura del proyecto y consultar la documentación oficial de Ionic y Angular para obtener más información sobre cómo resolver los errores más comunes.
> Adicionalmente igual puedes descargar el repositorio completo desde este [link](https://git.lcespedes.dev/lxluxo23/taller-ionic)
> Digo eso ya que es en ocasiones el ionic-cli funciona de manera distinta dependiendo de la versión de este mismo, además de la versión de node.
# Capítulo 5: Implementando Funcionalidades y Componentes Principales
En este capítulo completaremos nuestra aplicación de reservas para gimnasio implementando las páginas restantes y añadiendo funcionalidades para interactuar con nuestro backend.
## 1. Implementación de Servicios para Interactuar con el Backend
Antes de implementar las páginas, necesitamos actualizar nuestros servicios para interactuar con el backend en lugar de usar datos de ejemplo.
### Servicio de Clases
Actualizaremos el servicio de clases para obtener datos desde el backend:
**services/classes.service.ts**:
```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { GymClass } from '../models/gym-class.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ClassesService {
private apiUrl = `${environment.apiUrl}/api/classes`;
private classesSubject = new BehaviorSubject([]);
public classes$ = this.classesSubject.asObservable();
constructor(private http: HttpClient) {
// Inicializar cargando clases
this.loadClasses();
}
private loadClasses(): void {
this.http.get(this.apiUrl)
.pipe(
tap(classes => this.classesSubject.next(classes)),
catchError(error => {
console.error('Error al cargar clases:', error);
return throwError(() => error);
})
)
.subscribe();
}
getClasses(): Observable {
// Refrescar datos desde el servidor
this.loadClasses();
return this.classes$;
}
getClassById(id: string): Observable {
return this.http.get(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error al obtener clase con ID ${id}:`, error);
return throwError(() => error);
})
);
}
searchClasses(term: string): Observable {
return this.http.get(`${this.apiUrl}/search`, {
params: { term }
}).pipe(
catchError(error => {
console.error('Error al buscar clases:', error);
return throwError(() => error);
})
);
}
filterByCategory(category: string): Observable {
return this.http.get(`${this.apiUrl}`, {
params: { category }
}).pipe(
catchError(error => {
console.error('Error al filtrar clases por categoría:', error);
return throwError(() => error);
})
);
}
// Este método actualiza el contador de reservas de una clase
updateClassBookings(classId: string, change: number): Observable {
const url = `${this.apiUrl}/${classId}/bookings`;
return this.http.patch(url, { change }).pipe(
tap(updatedClass => {
// Actualizar la clase en la lista local
const currentClasses = this.classesSubject.value;
const index = currentClasses.findIndex(c => c.id === classId);
if (index !== -1) {
const updatedClasses = [...currentClasses];
updatedClasses[index] = updatedClass;
this.classesSubject.next(updatedClasses);
}
}),
catchError(error => {
console.error('Error al actualizar reservas de clase:', error);
return throwError(() => error);
})
);
}
}
```
### Servicio de Reservas
Ahora implementaremos el servicio de reservas para interactuar con el backend:
**services/bookings.service.ts**:
```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { Booking } from '../models/booking.model';
import { environment } from '../../environments/environment';
import { AuthService } from './auth.service';
import { ClassesService } from './classes.service';
import { NotificationService } from './notification.service';
@Injectable({
providedIn: 'root'
})
export class BookingsService {
private apiUrl = `${environment.apiUrl}/api/bookings`;
private bookingsSubject = new BehaviorSubject([]);
public bookings$ = this.bookingsSubject.asObservable();
constructor(
private http: HttpClient,
private authService: AuthService,
private classesService: ClassesService,
private notificationService: NotificationService
) {
// Cargamos las reservas del usuario al inicializar el servicio
this.loadUserBookings();
// Nos suscribimos a cambios de usuario para actualizar las reservas
this.authService.currentUser$.subscribe(user => {
if (user) {
this.loadUserBookings();
} else {
this.bookingsSubject.next([]);
}
});
}
private loadUserBookings(): void {
const currentUser = this.authService.getCurrentUser();
if (!currentUser) return;
this.http.get(`${this.apiUrl}/user/${currentUser.id}`)
.pipe(
tap(bookings => this.bookingsSubject.next(bookings)),
catchError(error => {
console.error('Error al cargar reservas:', error);
return throwError(() => error);
})
)
.subscribe();
}
getUserBookings(): Observable {
// Refrescar datos
this.loadUserBookings();
return this.bookings$;
}
getBookingById(id: string): Observable {
return this.http.get(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error(`Error al obtener reserva con ID ${id}:`, error);
return throwError(() => error);
})
);
}
addBooking(classId: string, className: string): Observable {
const currentUser = this.authService.getCurrentUser();
if (!currentUser) {
return throwError(() => new Error('No hay usuario autenticado'));
}
const bookingData = {
userId: currentUser.id,
classId,
className,
date: new Date(),
status: 'confirmed'
};
return this.http.post(this.apiUrl, bookingData).pipe(
switchMap(booking => {
// Actualizar el contador de la clase
return this.classesService.updateClassBookings(classId, 1).pipe(
switchMap(gymClass => {
// Programar notificación para esta reserva
this.notificationService.scheduleClassReminder(booking, gymClass);
// Actualizar la lista local de reservas
const bookings = [...this.bookingsSubject.value, booking];
this.bookingsSubject.next(bookings);
return of(booking);
})
);
}),
catchError(error => {
console.error('Error al crear reserva:', error);
return throwError(() => error);
})
);
}
cancelBooking(bookingId: string): Observable {
return this.http.patch(`${this.apiUrl}/${bookingId}/cancel`, {}).pipe(
switchMap(booking => {
// Actualizar contador de la clase
return this.classesService.updateClassBookings(booking.classId, -1).pipe(
map(() => booking)
);
}),
tap(booking => {
// Cancelar notificación programada
this.notificationService.cancelNotification(booking.id);
// Actualizar lista local
const bookings = this.bookingsSubject.value.map(b =>
b.id === bookingId ? { ...b, status: 'cancelled' } : b
);
this.bookingsSubject.next(bookings);
}),
catchError(error => {
console.error('Error al cancelar reserva:', error);
return throwError(() => error);
})
);
}
}
```
## 2. Implementación de la Página de Clases
Vamos a implementar la página que muestra el listado de clases disponibles:
**pages/classes/classes.page.html**:
```html
Clases DisponiblesTodasMente/CuerpoCardioFuerza
Cargando clases...
{{ gymClass.name }}
{{ gymClass.startTime | date:'EEE, d MMM, h:mm a' }}