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:
- Clase TypeScript: Contiene la lógica y datos
- Plantilla HTML: Define la estructura visual
- Estilos CSS: Define la apariencia (opcional)
- 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:
- ngOnChanges: Cuando cambian las propiedades de entrada (@Input)
- ngOnInit: Después de la primera inicialización
- ngAfterViewInit: Cuando la vista se ha inicializado
- 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
-
Navegación
ion-header,ion-toolbar,ion-buttonsion-tabsion-menuion-back-button
-
Contenido y Presentación
ion-cardy componentes relacionadosion-list,ion-itemion-grid,ion-row,ion-colion-avatar,ion-thumbnail
-
Formularios e Inputs
ion-input,ion-textareaion-select,ion-radio,ion-checkboxion-toggle,ion-rangeion-datetime
-
Feedback al Usuario
ion-loadingion-toastion-alertion-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:
- La estructura de carpetas sigue una organización lógica que separa código, estilos y recursos
- Los componentes Angular encapsulan HTML, CSS y lógica en unidades coherentes
- El enlace de datos permite la comunicación bidireccional entre vistas y lógica
- Las directivas extienden HTML con funcionalidades dinámicas
- Ionic proporciona componentes UI que se adaptan a iOS y Android automáticamente
- Los servicios permiten compartir lógica y estado entre componentes
- El enrutamiento facilita la navegación entre diferentes vistas
- 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.