Skip to content

LecturaAngular

José Bocanegra edited this page Mar 7, 2023 · 4 revisions

Banner Curso

Angular

Sitio oficial con la documentación completa, ejemplos y tutoriales angular.io

Conceptos Básicos de la Arquitectura de una aplicación Angular

Angular es una plataforma para desarrollar SPA (single-page applications). Angular está escrito en Typescript.

El concepto fundamental de Angular (Building-Block) es el de componente. Un componente controla una zona de la pantalla llamada vista (view), esto es, muestra información y/o elementos de interacción para el usuario como botones, menús, etc. Las siguientes imágenes muestran algunos componentes básicos: una forma, una lista, unos botones, un menú.

Imgur
Figura 1: Ejemplos de componentes

Cada componente se define en una clase anotada/decorada con @Component y tiene asociado una vista o template en Html donde está la definición de los elementos que se van a desplegar. La clase del componente es quien se ocupa de mostrar información o de ejecutar comportamiento como consecuencia de las acciones del usuario. Entonces se establece una relación entre la clase del componente y el Html que lo va a mostrar en la pantalla.

Los componentes se "componen" para crear componentes más complejos. Los componentes se agrupan de manera lógica en módulos que van a permitir organizar la aplicación.

Módulos y Componentes

Una aplicación Angular se organiza en un conjunto de módulos (@NgModules). Siempre habrá un módulo principal raíz y otros módulos que agrupan de manera lógica las funcionalidades de la aplicación.

Serán decisiones del diseñador de la aplicación definir: cuántos módulos se crearán, qué responsabilidad tiene cada uno, cómo se relacionan los módulos entre sí.

Un módulo define (declara) uno o más componentes. Los componentes son los elementos básicos que definen una funcionalidad de la aplicación y con la que el usuario podrá interactuar. Un componente tiene una vista (Html) y una lógica que permitirá ver, elegir y modificar información de la aplicación.

Los componentes utilizan servicios que proporcionan una funcionalidad específica que no está directamente relacionada con las vistas. Los servicios pueden inyectarse en los componentes como dependencias, haciendo el código más modular, reutilizable y eficiente.

Relaciones entre los Módulos
Un módulo Angular declara componentes, exporta componentes y provee servicios. También puede importar otros módulos que contienen componentes y/o servicios que se necesitan para su funcionalidad.

Librerías de Angular

Cuando construimos una aplicación web utilizando Angular debemos importar una serie de librerías predefinidas. Estas librerías comienzan por @angular y las básicas son: '@angular/core', '@angular/platform-browser' y '@angular/common/http'.

Estructura de archivos de una aplicación Angular

En una aplicación Angular podemos encontrar archivo de Typescript .ts, archivos Html .html, archivos de estilos .css y archivos de configuración para distintos propósitos que típicamente son .json.

Cuando se crea una aplicación Angular se crean varias carpetas. En la carpeta src tendremos algunos de los siguientes archivos:

- src
  + app       // aquí van los módulos, componentes, servicios, pruebas
  index.html  // página principal
  main.ts     // declaración del módulo principal
  style.css   // estilos

En la carpeta app tenemos los módulos. El principal y su componente van en la raíz y la recomendación es tener una carpeta aparte por cada módulo adicional y dentro de esta, una carpeta por cada componente:

- src
  - app 
    + moduloA
    + moduloB
    + ...
    app.module.ts           // módulo principal
    app.componente.ts       // componente principal del módulo principal
    app.component.spec.ts   // pruebas para el componente principal
    app.component.html      // template del componente principal
    app.components.css      // estilos del módulo principal

Clases y Decoradores (Anotaciones)

Cada módulo y cada componente es una clase Typescript anotada o decorada con decoradores de Angular. Un decorador tiene un nombre y un conjunto de propiedades.

Decorador de un Módulo

El decorador de un módulo es @NgModule y, en el siguiente ejemplo decora la clase AppModule. Por ejemplo, en el archivo app.module.ts (el módulo principal) tenemos:

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, ShoppingCartComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

La clase AppModule está anotada con @NgModule y tiene por propiedades:

imports: [ BrowserModule, FormsModule ] // Lista de módulos que importa. En este caso son módulos de framework Angular.`
declarations: [ AppComponent, ShoppingCartComponent ] // Lista de los componentes que define. En este ejemplo, el componente principal y otro llamado ShoppingCartComponent `
bootstrap: [ AppComponent ] // El componente principal.

Todos los nombres de módulos y componentes anteriores son nombres lógicos. La asociación con los archivos físicos donde se encuentran se hace al comienzo del archivo:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';

Decorador de un Componente

El decorador o anotación de un componente es @Component y en el siguiente ejemplo decora la clase AppComponent. Este decorador tiene las siguientes propiedades:

@Component({
  selector: 'my-app', // define la marca o tag que podrá utilizarse en una página para incrustar (invocar) el componente 
  templateUrl: './app.component.html' // define el nombre del archivo donde está el template o html del componente
})
export class AppComponent { }

Crear una aplicación Angular

En el siguiente vídeo creamos una aplicación Angular en VSCode y explicamos su estructura. Esta aplicación por defecto tiene un módulo, el principal llamado AppModule. El módulo AppModule es cargado desde el código en el archivo main.ts. Ese módulo declara y exporta el componentes AppComponent.

El componente AppComponent tiene definido un selector llamado app-root. Este selector es utilizado desde el index.html. El código en el template asociado con el componente es el que va a remplazar esta directiva.

Video 1: Aplicación Angular por defecto en VSCode

Ejemplo crear un componente para mostrar una Lista de Editoriales

En este ejemplo vamos a crear una aplicación Angular con un componente para mostrar una Lista de Editoriales. Para esto vamos a definir, además del módulo principal, un módulo funcional llamado EditorialModule que tendrá un componente que se ocupa de listar las editoriales. Al ejecutar el ejemplo vamos a obtener el siguiente despliegue:

Imgur
Figura 2: Lista de editoriales

El ejemplo muestra:

  • La definición del nuevo módulo y cómo se define y exporta el componente
  • La vista del componente.
  • La definición de un servicio y el uso de http para obtener la lista de editoriales.
  • El concepto de Observable y subscribe
Código completo está https://editorial-list.stackblitz.io

Diseño

Video 2: Crear el módulo Editorial y el componente de Listar

En este ejemplo tenemos dos módulos: AppModule que es el módulo principal y EditorialModule que es un módulo que contendrá las distintas funcionalidades que se le ofrecerán al usuario, para manipular el recurso editorial. En este ejemplo, el módulo EditorialModule sólo contiene un componente que se ocupa de desplegar en una lista las editoriales que existen.

El siguiente diagrama muestra los elementos de la solución:

Imgur
Figura 2
Elemento Responsabilidad
AppModule Es una clase anotada con @NgModule. Representa el módulo principal de la aplicación. Declara el componente AppComponent e importa dos módulos de angular BrowserModule, que provee servicios para todas las aplicaciones angular y HttpClientModule que provee servicios para hacer llamados http (lo vamos a usar para obtener las editoriales get editorials. También importa EditorialModule
AppComponent Es una clase anotada con @Component. Representa el componente principal de la aplicación. Define en la anotación el selector app-root y los archivos html y css del template y de los estilos de este componente. Si vemos el index.html este utiliza el selector de este componente principal.
<body>
	<app-root></app-root>
</body>
Elemento Responsabilidad
EditorialModule Es una clase anotada con @NgModule. Representa el módulo para los requerimientos del recurso editorial. Declara y exporta el componente EditorialListComponent, provee el servicio EditorialService e importa el módulo de angular CommonModule
EditorialListComponent Es una clase anotada con @Component. Representa el componente que se encarga de listar las editoriales.
Define en la anotación el selector list-editorial y los archivos html y css del template y de los estilos de este componente. Si vemos el app-component.html este utiliza el selector de este componente: <list-editorial></list-editorial>.
El constructor del componente recibe el servicio que se ocupa de traer las editoriales.
Define un método que utiliza el servicio y deja las editoriales en un arreglo Editorial[].
Editorial Es una interface que define el tipo Editorial que es una tupla de dos atributos: id: number y name: string
EditorialService Es el servicio que va a utilizar http para acceder a la colección de editoriales. Explicación detallada más adelante.

La siguiente figura muestra la relación de los distintos elementos en las vistas o templates html. En ejecución, primero se carga el archivo index.html que contiene el selector app-root. Este selector está definido en el componente AppModule. app-root es reemplazado por el contenido del template de AppModule que en este ejemplo consiste del selector list-editorial. Este, a su vez, está definido en el componente EditorialListComponente. Finalmente list-editorial es reemplazado por el contenido del template de EditorialListComponente que se ocupa del despliegue de la lista.

  • Las flechas azules muestran el reemplazo de los templates/vistas.
  • Las flechas anaranjadas muestran en dónde se define el selector y en dónde se usa
  • Las flechas moradas muestran en dónde se declara el template/vista y en qué archivo está contenido
Imgur
Figura 3

La vista

La vista del componente se refiere al template o archivo html que contiene lo que se desplegará en la pantalla. En este caso, la vista va a desplegar la lista de editoriales. Con el propósito de ir explicando gradualmente, vamos primero a desplegar una lista que ha sido construida directamente en el componente. Esto nos permitirá entender la relación entre el componente y la vista.

Video 3: La vista del componente de Listar

El servicio

EditorialService

Los servicios son los elementos de la aplicación que se encargan de realizar lógica funcional. En particular, habrá servicios para comunicar a los componentes de la aplicación front con la aplicación que se encuentra en algún servidor para pedir/recibir aquello que se va a mostrar, o para realizar las transacciones.

Si la aplicación exterior tiene un API REST, el servicio se ocupará de hacer la invocación construyendo la URL que se necesite y enviando/recibiendo los objetos que se requieran.

Para simplificar, en este ejemplo, nuestro servicio no se conecta a un API REST para obtener las editoriales sino que las obtendrá de un archivo local llamado editoriales.json. Sin embargo, utilizaremos el módulo Angular de HttpClient pero la url será la ruta donde se encuentra el archivo json.

Inyección de dependencias en Angular

Referencia: dependency-injection

Las dependencias son servicios u objetos que una clase necesita para realizar su función. En Angular, se debe declarar la dependencia utilizando una anotación, y el framework la inyectará cuando construya el objeto; en el constructor se debe indicar como parámetro la dependencia.

En nuestro ejemplo, nuestro servicio depende de HttpClient. La inyección se hace de la siguiente forma:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
...
@Injectable()
export class EditorialService {
    constructor(private http: HttpClient) { }
    ...

La anotación @Injectable() indica que vamos a inyectar una dependencia. La dependencia se declara como parámetro del constructor private http: HttpClient, esto significa que dentro de la clase EditorialService podremos utilizar la variable http y a partir de ella todos los servicios que ofrece HttpClient.

HttpClient

A través de HttpClient podremos realizar get, post, put, delete y otros servicios. La signatura de get es la siguiente:

get(url: string, options?: {...}) : Observable<any>

El primer parámetro es la url para acceder a los datos. El segundo parámetro es opcional y se trata de un conjunto de opciones predefinidas para indicar la información que queremos de la respuesta.

Todas las funciones http son asíncronas, es decir se hace el llamado pero la aplicación que llamó, en este caso el front, sigue su curso. Por esta razón, estas funciones devuelven el resultado en un Observable, en un momento explicamos lo que significa, y los datos serán retornados por defecto en formato json. El tipo any se puede reemplazar por el tipo exacto de lo que estamos esperando:

@Injectable()
export class EditorialService {
    constructor(private http: HttpClient) { }
    getEditorials() : Observable<Editorial[]> {
        return this.http.get<Editorial[]>(API_URL + editorials);
    }
}

En nuestra función getEditorials() invocamos:

http.get<Editorial[]>(API_URL + editorials); sin opciones, solo la url. Esta Retorna un arreglo (Observable) de objetos conformes con la interface Editorial.

Observable

En la función estamos declarando que la función getEditorials() es Observable porque está haciendo una invocación a http.get que es una función asíncrona.

Quiere decir que la función publicará su resultado, pero esta no se ejecuta hasta que un consumidor se subscribe a ella. El consumidor suscrito recibirá notificación cuando la función termine (asíncronamente).

En otras palabras, quien invoque la función, debe suscribirse a ella. En nuestro ejemplo, esto se hace en la clase del componente EditorialListComponent. Veamos el código:

...
export class EditorialListComponent implements OnInit {
    constructor(private editorialService: EditorialService) { } 
    editorials: Editorial[];
    getEditorials(): void {
        this.editorialService.getEditorials().subscribe(editorials => this.editorials = editorials);
    }
    ngOnInit() {
        this.getEditorials();
    }
}

La figura muestra los elementos que intervienen:

Imgur
Figura 4

El atributo editorials es el que se utiliza para el despliegue en el template html. Este atributo será actualizado cuando el Observable de la función editorialService.getEditorials() le anuncie (gracias a la suscripción) a getEditorials(), de este componente, que se terminó la ejecución del get.

Este componente implementa OnInit que es una interface que define el método ngOnInit() el cual será ejecutado en creación del componente. Aquí, entonces, se llama a la función que está suscrita al servicio de traer las editoriales.

El template de la lista tiene el siguiente fragmento donde podemos ver una iteración sobre la variable editorials:

<tbody>
   <tr *ngFor = "let editorial of editorials">
        <td>{{editorial.name}}</td>
   </tr>
</tbody>

En este código, *ngFor es una directiva de angular que permite hacer un ciclo directamente sobre el html. el ciclo termina donde se cierra el elemento que tiene el atributo *ngFor. En este caso es tr. El valor de *ngFor es una expresión en typescript donde se declara la variable editorial que va a iterar sobre el arreglo editorials.

La expresión {{.. }} significa reemplazar el contenido del elemento, en este caso td, con resultado de evaluar editorial.name.

Video cómo crear el servicio

Ejemplo ShoppingCart

Este ejemplo es una adaptación del que se encuentra aquí.

Vamos a desarrollar el ejemplo del Shopping Cart en Angular. Los requerimientos:

  • Se debe poder crear ítems de compra con descripción, cantidad y costo
  • Se debe incluir el subtotal de cada ítem.
  • Se debe totalizar toda la lista del Shopping Cart
  • Se debe poder borrar un ítem de compra.
El código de este ejemplo está en: https://stackblitz.com/edit/shoppingcart-isis

Para poder modificar el ejercicio, cree una cuenta en Stackblitz, y haga clic en "Fork". De esta forma, podrá guardar los cambios que realice al resolver los desafíos.

Diseño de la Interfaz

Primero definimos un prototipo de cómo debería verse la aplicación:

Imgur
Figura 1: Interfaz usuario applicación shopping cart

Diseño de la aplicación

Por ser esta una aplicación simple, vamos a definir un módulo principal y el componente de ShoppingCart. El siguiente diagrama muestra la decisión de diseño.

Imgur
Figura 2: Diagrama de clases de la aplicación shopping cart

Como se puede ver, tenemos el módulo principal AppModule con su componente por defecto AppComponent. Tenemos un componente adicional ShoppingCartComponent que tiene el modelo del shopping cart, llamado invoice, y, dado lo simple de la lógica, la hemos definido directamente como métodos del componente.

Vale la pena mencionar que siempre que hablamos de un componente, estamos incluyendo al menos 4 elementos:

  1. La clase componente que es la que se muestra en el diagrama,
  2. La vista del componente (el Html),
  3. Los estilos que se le aplicarán a la vista y
  4. La especificación de las pruebas.

También, en el diagrama estamos mostrando un comentario asociado con cada componente que incluye "decoraciones" o metadatos que serán utilizados por Angular cuando despliegue la aplicación. En el caso del componente estos son: el selector que se refiere al tag de html que se crea para que cuando se utilice dentro de un html allí se despliegue el componente y el nombre del archivo que contiene la definición de la vista del componente.

Estructura del proyecto

Este proyecto sigue una estructura estándar de proyectos Angular. Dentro de la carpeta appestán los módulos (en este caso sólo uno) y los componentes. El componente shopping-cart está dentro de su propia carpeta. En la siguiente figura, los archivos que están encerrados en el cuadro verde corresponden al componente ShoppingCartComponent.

Imgur
Figura 3: Estructura de archivos de la applicación shopping cart

Note que hay un archivo llamado item.model.ts, contiene la definición de la clase item.

El archivo que está abierto index.html utiliza el selector o tag html que fue definido en el AppComponent, ver Figura 2, es decir my-app. Allí también hay otro meta-dato llamado templateUrlcuyo valor es el nombre del archivo html de la vista del componente ShoppingCartComponent. Esto significa que cuando se encuentre el tag my-app este se reemplazará por la vista del componente.

Este template se muestra en el siguiente código html:

<h2 class="text-center">Shopping Cart Example</h2>
<div class="row">
	<div class="col-sm">
	</div>
	<div class="col-sm">
		<div class="container-fluid">
			<table class="table table-hover">
				<thead class="thead-light">
					<tr>
						<th scope="col">Description</th>
						<th scope="col">Qty</th>
						<th scope="col">Price</th>
						<th scope="col">Total</th>
						<th></th>
					</tr>
				</thead>
				<tr *ngFor="let item of invoice; let i = index;">
					<td scope="row">
						<input type="text" [(ngModel)]="item.description">
					</td>
					<td>
						<input type="number" [(ngModel)]="item.qty" required>
					</td>
					<td>
						<input type="number" [(ngModel)]="item.price" required>
					</td>
					<td>{{subTotal(item) | currency}}</td>
					<td><button class="btn btn-danger" (click)="deleteItem(i)">Delete item</button></td>
				</tr>
				<tr>
					<td><button class="btn btn-success" (click)="addItem()">Add item</button></td>
					<td></td>
					<td>Total:</td>
					<td>{{total() | currency}}</td>
				</tr>
			</table>
		</div>
	</div>
	<div class="col-sm">
	</div>
</div>

El html anterior utiliza:

  • Estilos de bootstrap +4.1 que fueron importados en el archivo stype.css que se encuentra en la raíz del proyecto.
  • Evaluación de expresiones:
{{ expresion | filtro}}
  • Tiene dos partes, la expresión que se evaluará y el filtro que se le aplica a la expresión. En este caso se trata del formato en el que se desplegará la respuesta. Por ejemplo, subtotal y la respuesta se formatea como una moneda.
  • Marcado de Angular como por ejemplo [(ngModel)] esta directiva significa un "two-way binding"; cuando el campo es actualizado desde la vista, por ejemplo por el usuario, la variable también toma ese valor. Si la variable es actualizada desde el código, el campo en la vista también es actualizado.

Este comportamiento se puede apreciar si ud. cambia los valores sobre la forma de la cuenta de carrito. Este cambio queda reflejado en la variable invoice.

Imgur
Figura 5: "two-way binding"

Descripción del componente ShoppingCartComponent

La razón por que se inicia importando el modelo de un ítem es para lograr que su definición y tratamiento (en este caso del ítem) sean los mismos en toda la aplicación. Las ventajas de esto son más claras en una aplicación más grande, pero imagine que va a agregar un nuevo componente para manejar las facturas.

Si no existiera el modelo, el código perdería claridad con respecto a qué es un ítem en cada uno de los componentes. Esto se tendría que definir en cada uno de ellos, o en el peor de los casos en ninguno, y las modificaciones podrían por ejemplo no replicarse.

El constructor del componente inicializa la factura como una lista vacía, y le agrega un nuevo ítem utilizando el modelo.

import {Component} from '@angular/core'
// Se importa el modelo Item
import {Item} from './item.model'

// Declarar el componente utilizando @Component, definiendo el selector del
// componente para ser reutilizado dentro del HTML y la ubicación del template.
@Component({
  selector: 'shoppingCart-component',
  templateUrl: './shopping-cart.component.html'
})
export class ShoppingCartComponent {
  // En TS las variables se pueden definir con un tipo. En este caso se está
  // declarando que invoice es una variable de tipo Object con un único atributo
  // llamado items, que es un array de Object. Donde cada elemento de ese array
  // tiene 3 atributos, un string, y dos number. Acá solo se está definiendo
  // el tipo, no se está inicializando.
  invoice: Item[];
  
  constructor() {
    // Se inicializa la variable invoice, con un primer ítem de ejemplo.
    this.invoice = [];
    var item = new Item('item', 9.95, 1);
    this.invoice.push(item);
  }
  ...
  }

Con respecto a las operaciones del ShoppingCart, para este ejemplo de introducción, se ha decidido implementarlas dentro del componente.

El componente cuenta con 4 métodos:

  • addItem que agrega un nuevo ítem a la factura
  • deleteItem que elimina el item que recibe por parámetro de la factura
  • subTotal que calcula y mantiene actualizada la variable del subtotal de cada item (su precio * su cantidad)
  • total que calcula y mantiene actualizado el valor de la factura
...

/**
   * Función para agregar un nuevo elemento a la lista de la factura.
   */
  addItem(): void {
     var item = new Item('item', 9.95, 1);
    this.invoice.push(item);
  }
  
  /**
   * Función para eliminar un elemento de la factura.
   * @param index El índice del elemento a eliminar
   */
  deleteItem(index): void {
    this.invoice.splice(index, 1);
  }
  
  /**
   * Función para calcular el precio total de un elemento.
   * @param ítem El elemento del cual se quiere calcular el precio.
   * @returns El precio total del elemento.
   */
  subTotal(item): number {
    return item.qty * item.price;
  }
  
  /**
   * Función para calcular el precio total de la factura.
   * @returns El precio total de la factura.
   */
  total(): number {
    let total = 0;
    for (const item of this.invoice) {
      total = total + (item.getQty() * item.getPrice());
    }
    return total;
  }
}

Desafíos sobre el ShoppingCart

  1. Modifique el componente ShoppingCartComponent para que al inicializar, el producto que agregue tenga un valor de 99,95.
  2. Agregue al modelo ítem el atributo type que puede ser de 3 tipos: meat, beverage o fruit. Haga las modificaciones necesarias para agregar esta columna a la tabla del shopping cart. El campo debe ser de tipo select.
  3. Utilizando los estilos de Bootstrap 4.1, cambie los botones para que sean de tipo "Outline button" y haga una modificación adicional de su preferencia para personalizarlos (ej. tamaño, fuente, etc.)
  4. Es usual en la estructura de los proyectos Angular que la lógica de los componentes se defina en un servicio. Cambie a un servicio(@Injectable) la lógica del componente ShopingCartComponent. Es decir, todas aquellas operaciones cuya responsabilidad difiera de interactuar con la vista. El componente debe inyectar un servicio y hacer llamados a este para recibir el resultado de las operaciones.

Navegación y manejo de error

Navegación y definición de rutas

Información oficial sobre este tema aquí.

Angular ofrece una serie de librerías para facilitar la navegación dentro de una aplicación. Por ejemplo, al hacer clic en un menú, el resultado puede ser "navegar" o ir (también podemos decir llamar/invocar)a un componente de la aplicación.

Los servicios principales de estas librerías nos van a permitir:

  1. Definir las rutas y la asociación con el componente al que se quiere ir. Esto se hace en las clases de typescript.
  2. Invocar el componente y definir en dónde queremos que ese componente se despliegue (el outlet). Esto se hace en los templates html.

Adicionalmente, en la invocación del componente podemos enviar parámetros de entrada al componente. Esto último lo veremos más adelante.

Definición de las rutas y asociación del componente

Por defecto, según la estructura de una aplicación angular, AppRoutingModule es el módulo donde definimos los enrutamientos o la navegación de la aplicación y funciona gracias a los paquetes Routes y RouterModule de la librería @angular/router.

La clase AppRoutingModule será un Singleton y debe contener un arreglo de la definición de las rutas. El tipo del arreglo de las rutas es Routes. Ese arreglo se puede llamar como uno quiera pero hay que configurarlo y decirle a la aplicación en (el módulo principal) cuando se importa AppRoutingModule.

Configuración de las rutas en el módulo principal

En el siguiente código vemos la configuración de tres rutas de primer nivel: /business, /portafolio y /more. Cada una invoca un componente distinto y lo despliega en un outlet distinto (cada uno tiene su propio nombre):

  • El componente asociado con la ruta /business será desplegado en el outlet llamado left.
  • El componente asociado con la ruta /portafolio será desplegado en el outlet llamado right.
  • El componente asociado con la ruta /more será desplegado en el outlet llamado botton.

La ruta /business tiene dos hijos: services y products.

const routes: Routes = [
  {
    path: 'business', component: BusinessMainComponent, outlet: 'left',
    children: [
      { path: 'services', component: BusinessServicesComponent},
      { path: 'products', component: BusinessProductsComponent },
    ]
  },
  { path: 'portafolio', component: PortafolioMainComponent, outlet: 'right' },
  {path: 'more', component: MoreinfoComponent, outlet: 'botton' },
  { path: '**', redirectTo: 'business' }

];

Invocar el componente

routerLink

La sintaxis básica para acceder a una ruta es:

<a routerLink="home"></a>

En ese caso "home" es un string (literal) y es el nombre de la ruta. Si el nombre de la ruta estuviera en una variable no se puede usar la sintaxis de string sino que hay que definir:

<a [routerLink]="['varquecontienelvalordelstringdelpath']"></a>

En este post de stackoverflow explican claramente la diferencia entre routerLink y [routerLink].

En nuestro ejemplo de business, además del path debemos indicar el outlet donde queremos que se despliegue el componente:

<a class="nav-link" [routerLink]="[ {outlets: {left:['business']}}]" routerLinkActive="active">Business</a>

router-outlet

Cuando se navega hacia una ruta definida en la aplicación, el resultado de la navegación es inicializar el componente y desplegar la vista en el html "actual" donde esté en la directiva:

<router-outlet></router-outlet>

El outlet puede tener un nombre. Por ejemplo,

<router-outlet name="left"></router-outlet>

En este enlace de Stackblitz puede encontrar un ejemplo de las rutas y los outlets (con nombres).

Manejo de errores

Angular ofrece una librería, con un conjunto de servicios, para interceptar las peticiones http ya sea al momento de la salida de la petición hacia el API-Rest o del regreso de la petición desde el API-Rest.

Recordemos que los llamados a http como http.get o http.post se realizan desde distintos componentes, tantos como recursos y servicios tiene el API. Cada clase servicio @Service de un módulo funcional, con seguridad tiene muchos llamados a http. Si uno quiere postprocesar la invocación del request, en un solo lugar de la aplicación, por ejemplo, agregando información para autenticación, o preprocesar el retorno, por ejemplo para manejar los errores si el request viene en error, tendría que escribirse en cada lugar de la aplicación donde se hace un llamado hht.

La ventaja de tener esta librería que implementa el patrón de diseño Interceptor, es que el código se escribe en un solo lugar (se factoriza) de tal forma que los llamados individuales no hay que modificarlos sino que el interceptor se ocupará de las acciones que, eventualmente, hay que realizar antes o después del llamado.

La figura 5 ilustra los elementos del patrón de diseño interceptor en Angular. Tenemos dos módulos y cada uno tiene dos componentes que utilizan una clase Servicio. Desde la clase servicio se escriben los llamados a http. Estos llamados pueden ser atrapados y preprocesados antes de salir hacia el API y también, pueden ser atrapados cuando regresan del API y ser postprocesados antes de volver al lugar original del llamado.

Imgur
Figura 5: Interceptores en Angular

En la figura 5, solo se muestra un interceptor (httpinterceptor) pero puede haber una lista de varios interceptores cada uno con un propósito definido.

HttpErrorInterceptor: Interceptos para manejar los errores de invocaciones HTTP

Nuestra clase HttpErrorInterceptor debe ser una clase decorada con @Injectable() dado que va a ser un servicio que se va a incluir en la aplicación. Su responsabilidad es procesar los errores que vengan del llamado Http, es decir, mostrar al usuario correctamente los mensajes para que se sepa qué pasó.

Para implementar esta clase, nos apoyamos en clases e interfaces ya definidas en Angular. Nuestra clase debe extender de HttpErrorResponse que es una clase predefinida de Angular que representa un error asociado con un estatus "non-successful HTTP". La clase contiene, en sus distintos atributos, la información del error:

La figura 6 muestra en azul la clase que estamos definiendo y en amarillo las clases e interfaces predefinidas en Angular.

Imgur
Figura 6: Interceptor para manejar errores Http
@Injectable()
export class HttpErrorInterceptor  extends HttpErrorResponse implements HttpInterceptor

La clase debe implementar el método intercept . Este método recibe el request que va a interceptar y el siguiente interceptor que se va a invocar. Lo que hace es construir un mensaje de error y llamar al servicio que lo va a desplegar.

Primero identifica el tipo del error: si es del lado del cliente o dele lado del servidor. Si es del lado del cliente o es un error de la red, el error es una instancia de ErrorEvent de lo contrario se trata de un error del llamado a http.

Lo que hace el código es obtener la información de cuál es el mensaje y construir una respuesta para el usuario.

En el caso de nuestra implementación estamos utilizando el servicio Toastr para hacer el despliegue del error.

@Injectable()
export class HttpErrorInterceptor  extends HttpErrorResponse implements HttpInterceptor {
    
    constructor (private toastrService: ToastrService){ super (toastrService) }
    
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request)
            .pipe(
                catchError((error: HttpErrorResponse) => {
                    let errMsg: string = '';
                    let errorType : string = 'Error';
                    // Client Side Error
                    if (error.error instanceof ErrorEvent) {
                        errMsg = `Error: ${error.error.message}`;                     
                    }
                    else {  // Server Side Error                       
                        if (error.status == 0) {
                            errMsg = `${error.status}, "No hay conexión con el servidor"`;    
                            errorType = 'Major Error';
                        }                      
                        else {
                             errMsg = `${error.status}: ${error.error}`;                             
                        }                      
                        this.toastrService.error(errMsg, errorType, {closeButton: true});
                    }
                    
                    console.log(errMsg);
                    return throwError(errMsg);
                })
            )
    }
}   

Angular implementación Maestro Detalle

Sitio oficial con la documentación completa, ejemplos y tutoriales angular.io

Decisiones de diseño

El patrón de diseño Maestro-Detalle se puede aplicar en las distintas capas de una aplicación, desde la interfaz usuario hasta el modelo en la base de datos. Aquí nos vamos a enfocar en la implementación de Maestro-Detalle en la Interface Usuario utilizando Angular. El Maestro-Detalle es una relación uno a muchos que puede tener varios niveles de anidamiento. En la figura 1, podemos ver que la lista de autores es un primer Maestro cuyo detalle es un autor. Un autor tiene a su vez muchos libros. En ese caso el Autor Juega el rol de Maestro y la lista de sus libros el de detalle.

Imgur
Figura 1: Maestro-Detalle-Detalle

Diseño de Módulos y Componentes

En las aplicaciones Angular que hemos venido desarrollando, nuestra decisión de diseño con respecto a qué es que por cada recurso hay un Módulo y por cada caso de uso básico hay un Componente.

Diseño de la página

Como se explica en el libro de Designing Web Interaces, el ideal desde el punto de vista del usuario final es organizar la pantalla de tal forma que sea posible ver la lista de los maestros, navegar por ella mostrando sus detalles pero todo en la misma página.

Diseño de la navegación

Una vez decidida la organización de los distintos elementos en la página lo primero que debe ser claro es qué componente es el que queremos desplegar en cada zona y cuál es la acción del usuario que hará que ese despliegue ocurra.

En el caso de la Figura 1, los componentes son:

El código Angular del ejemplo de la Figura 1 se puede encontrar aquí a partir del Tag Paso-2 y la documentación detallada.

  1. La lista de autores. En nuestra implementación, hay un módulo AuthorModule que contiene un componente llamado AuthorListComponent.
  2. El detalle del autor. También es un componente del módulo AuthorModule y se llama AuthorDetailComponent.
  3. La lista de los libros del autor. En nuestra implementación, hay un módulo BookModule que contiene un componente llamado BookListComponent.

La acción del usuario es sobre la lista de autores: Al seleccionar un autor se debe cambiar toda la información de la derecha de la página. En términos de Angular se debe "llamar" el componente" AuthorDetailComponent con la información de cuál fue el autor seleccionado. De la misma forma, se debe "llamar" el componente de listar los libros con los libros del autor seleccionado.

Como ya hemos visto, hay dos formas de "llamar" un componente:

  1. Utilizando el enrutador de Angular lo que implica dos cosas: a) Definir la ruta hacia el componente para utilizarla en un elemento html que acepte clic; b) Definir la zona o outlet donde queremos que se despliegue el componente. Por ejemplo:
<router-outlet></router-outlet>
  1. Utilizando su selector en el html donde se quiere hacer el despliegue. Por ejemplo:
<mi-componente></mi-componente>

Para hacer la implementación, debemos entender varias cosas:

  1. Cómo saber cuál elemento fue el que se seleccionó y cómo pasar esta información al componente del detalle.
  2. Cómo cambiar/actualizar el componente del detalle cuando se selecciona un elemento
  3. Cómo "llamar" un componente de otro módulo, teniendo en cuenta los dos puntos anteriores.

Vamos a explicar estos pasos con un ejemplo sencillo que extiende el ejemplo de listar las editoriales que vimos en Crear el componente Lista de Editoriales. Vamos a hacer dos implementaciones de acuerdo con las dos formas de llamar un componente.

Versión 1: Implementación con RouterLink

El código completo del ejemplo en stackblitz aquí.

Un video con la explicación del ejemplo aquí.

Versión 2: Implementación con la directiva del componente

EL código completo del ejemplo en stackblitz aquí.

La Figura 2 muestra el resultado que queremos obtener. Hay una ruta definida para el componente EditorialListComponent que se activa cuando el usuario hace clic en el menú de la parte superior. Esto trae como consecuencia que se muestra la lista de editoriales. Después el usuario puede navegar por cada una de ellas y el detalle aparecerá en la parte de abajo.

Nota: En este ejemplo no tenemos una conexión al back sino que tenemos los archivos json que representan las editoriales.

Imgur)
Figura 2:Ejemplo Detalle Editorial version 2

La Figura 3 muestra el contenido del AppComponent.html que es el template inicial. En la parte de arriba está el html que construye el menú de Editorials. Allí se puede ver que el elemento de enlace <a></a> tiene definido un routerLink que es el que se definió en las rutas de la aplicación en el módulo AppRoutingModule. En la parte de la mitad se muestra el código donde se deine el outlet principal, es decir, el lugar en la pantalla donde se desplegará el template del componente de la lista (editorial-list.component.html).

Imgur
Figura 2:Ejemplo Detalle Editorial versión 2

La responsabilidad del componente de listar

El siguiente listado es el template del componente de la lista (editorial-list.component.html). Note que tiene dos filas ("row"), cada una con una columna. La primera fila es la lista. Se puede ver que se construye una tabla y se itera (*ngFor) sobre un elemento de fila de la tabla. Cada elemento de fila tiene un elemento de enlace <a> lo que dice es que cuando el usuario haga clic en el elemento de la lista, llame al método onClick. Este método debe estar definido en el componente asociado con el template.

En la segunda fila se está haciendo el llamado al componente detalle utilizando la directiva del componente app-editorial-detail. En el llamado estamos enviando un parámetro que corresponde con el objeto que se va a mostrar en el detalle.

<div class="container-fluid">
	<div class="row">
		<div class="col">
			<table class="table table-striped">
				<thead class="thead-dark">
					<tr>
						<th>Editorial Name</th>
					</tr>
				</thead>
				<tbody>
					<tr *ngFor="let i of editorials">
						<td style="cursor: pointer;">
							<a class="figure-caption" (click)="onSelected(i.id)">{{i.name}}</a>
						</td>

					</tr>
				</tbody>
			</table>
		</div>
	</div>
	<div class="border row">
		<div class="col">
			<div *ngIf="selectedEditorial">
				<app-editorial-detail [editorialDetail]="selectedEditorial"> </app-editorial-detail>
			</div>
		</div>
	</div>
</div>

El componente EditorialListComponente es el responsable de definir la función onSelected y esta función es la responsable de obtener la editorial que se va a desplegar y que va a ser recibida como parámetro por el componente EditorialDetailComponent. La Figura 4 muestra los elementos involucrados en la solución: El módulo de editorial, el componente de listar, el componente del detalle, el servicio y el modelo o datos de las editoriales.

La Figura 4 muestra los elementos involucrados en la solución: El módulo de editorial, el componente de listar, el componente del detalle, el servicio y el modelo o datos de las editoriales.

Imgur
Figura 4: Clases ejemplo Detalle Editorial versión 2

La implementación del método OnSelected es:

  1. Recibe el id de la editorial que necesita
  2. Invoca de la clase EditorialService el método que trae específicamente esa editorial. Como este método es un Observable, se suscribe y en el momento que se termine, el valor lo va a dejar en la variable que vamos a enviar de parámetro al componente del detalle.

El código es:

onSelected(editorial_id: number): void {
    this.editorial_id = editorial_id;
    this.selectedEditorial = new EditorialDetail();
    this.editorialService.getEditorialDetail(editorial_id).subscribe(o => this.selectedEditorial = o);
  }

La responsabilidad del componente detalle

La clase del componente detalle, como se muestra en la Figura 4, debe declarar como @Input() el atributo que recibe de parámetro. Este atributo es lo que el componente utiliza en su template para hacer el despliegue.

@Component({
  selector: 'app-editorial-detail',
  templateUrl: './editorial-detail.component.html',
  styleUrls: ['./editorial-detail.component.css']
})
export class EditorialDetailComponent implements OnInit {

  /**
  * The component's constructor
  * @param editorialService The editorial's service
  * @param route The route element which helps to obtain the editorial's id
  * @param toastrService The toastr to show messages to the user
  */
  constructor(
    private editorialService: EditorialService,
    private route: ActivatedRoute
  ) { }

  /**
  * The editorial whose details we want to show
  */
  @Input() editorialDetail: EditorialDetail;
  ...
  
  }
Clone this wiki locally