Fundamentos

Comandos

  • npm i -g @angular/cli Instala el CLI de Angular

  • ng version Verifica tu instalación

  • ng new my-project Crea tu primer proyecto

  • ng serve -o --port=3500 Lanzar el servidor en un puerto especifico

  • ng g c components/[name] Generar componentes, para generar en un modulo específico al final agregar

    ng g component nameComponent --module=app.module.ts

    In Angular 14:

    ng g component nameComponent --module=app --dry-run // Evalua que se afectará, pero no hace cambiso
  • ng g s services/[name] Generar servicios

  • ng g p [name] Genera pipes

  • ng g d [d] Genera directivas

  • ng g interceptor interceptors/time --flat Crea interceptor

  • ng generate config karma Agrega la configuración de karma, en la version 15+ de Angular ya no aparece este archivo de configuracion

  • ng generate environments Genera los archivos para los diferentes ambientes v15+

  • ng g m <module-name> Genera un módulo, puede ser un módulo compartido por ejemplo, con --routing le indica a Angular que tiene que crear, además del módulo, el archivo base para agregarle rutas a este nuevo módulo.

  • ng g g <guardian-name> Crea un guardian

  • ng update Actualiza versón de Angular

String Interpolation

Es la manera de enviar datos desde nuestro componente hacia la vista. Utilizando el doble símbolo de llaves {{ }}. Si son privados no. Usado para renderizar el contenido de los elementos html.

<p>{{name}}<p>
<img src="{{ img }}" alt="imagen random">

Property Binding

Es la manera que dispone Angular para controlar y modificar las propiedades de los distintos elementos de HTML. Para esto, simplemente utiliza los corchetes [] para poder modificar dinámicamente ese atributo desde el controlador. Recomendado para cuando se usan objetos. Cominicación del componente a la vista y los datos van en una única dirección

Se puede usar en

  • El atributo src de la etiqueta <img> para modificar dinámicamente una imagen.

  • El atributo href de un <a> para modificar un enlace. El atributo value de un <input> para autocompletar un valor de un formulario.

  • El atributo disable de un <input> para habilitar/deshabilitar un campo de un formulario.

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent {
    empresa = 'Plata';
    habilitado = true;
}
//    ...
<input [value]="empresa" [disabled]="habilitado"  />

Event Binding

Permite controlar los eventos que suceden en estos elementos. Para esto utiliza los paréntesis () para el bindeo de la propiedad del elemento. Comunicación de la vista al componente

<button (click)="enviarFormulario()">Send</button>
<input (onKeyUp)="buttonClick($event)">

Puedes enviarle al controlador el evento completo que se produce en la vista. Para esto, solo declara un parámetro $event en el método que se encuentra escuchando el evento.

ngModel

El atributo ngModel permite el intercambio de datos de forma bidireccional entre el componente y la vista. Lo que suceda en el componente, se verá reflejado en la vista. Lo que se suceda en la vista, inmediatamente impactará en el componente, es decir ya no necesitamos esto:

<input [value]="person.name" (keyup)="changeName($event)" />

Por

<input #inputName="ngModel" [(ngModel)]="name">

Para utilizar ngModel, es necesario hacer uso e importar FormsModule y agrégalo en los imports en archivo app.module.ts, que es el módulo principal de toda aplicación Angular.

Podemos agregar una variable en el template #inputName="ngModel" (inputName es un ejemplo, puede ser otro nombre) para ver sus propiedades o validaciones en el archivo html

<p [class.active-color]="inputName.valid">
  {{ name }}, Valid: {{ inputName.valid }}
</p>

If … else

<div *ngIf="isPlatzi; else templateElse">Hola, soy Platzi</div>
<ng-template #templateElse>
    <div>No soy Platzi</div>
</ng-template>

ngFor

<ul>
    <li *ngFor="let str of myArray; index as i">
        {{ i }}. {{ str }}
    </li>
</ul>

ngSwitch

<div [ngSwitch]="color">
    <p *ngSwitchCase="'azul'">
        El color el Azul
    </p>
    <p *ngSwitchCase="'verde'">
        El color el Verde
    </p>
    <p *ngSwitchCase="'rojo'">
        El color el Rojo
    </p>
    <p *ngSwitchDefault>
        No hay ningún color
    </p>
</div>

Class binding

La versatilidad de Angular te permite agregar o quitar clases y estilos a tus elementos HTML a partir de condicionantes bindeando la propiedad [class]

<div [class.active-color]="isActive">
</div>

Imagina que tienes en tu componente una propiedad llamada isActive que agregará la clase active-color si esta es verdadera o quitará la clase si es falsa. Luego ya puedes darle los estilos que más te gusten al elemento HTML en tu hoja de estilos utilizando la clase active-color.

Style binding

También puedes añadir estilos inline a los elementos HTML bindeando la propiedad [style] seguido de la propiedad CSS que quieres modificar dinámicamente:

<p [style.color]="isActive ? 'blue' : 'red'"></p>

NgClass y NgStyle

Puedes bindear la directiva [ngStyle] o [ngClass] y pasarle un objeto con cada propiedad o clase que deseas agregar:

<p
    [ngStyle]="{
        'color': textColorVar,
        'background': textBackgroundVar
    }"
></p>

<hr class="line-error"
    [ngClass]="{
        'validClass': nameInput4.valid,
        'invalidClass': nameInput4.invalid
    }"
/>

<div
    [ngClass]="isAvailable ? 'active-class' : 'deactivate-class'"
></div>

// O puedes usar las Interpolaciones en lugar del binding:
<p
    ngClass="{{ isAvailable ? 'active-class' : 'deactivate-class' }}"
></p>

Componentes en Angular

Un componente es una pieza de software con una responsabilidad única y una estructura y funcionalidad determinada, además de ser reutilizable.

Es la parte minima de una página web, consta de un template HTML, archivo de SASS o CSS y archivo de logica (Typescript).

Comunicando componentes

Para enviar información de padre a hijo

Puedes utilizar el decorador @Input() para marcar una propiedad de una clase como punto de entrada de un dato.

Los decoradores pueden ser usados en clases y propiedades

// Componente hijo
import { Component, Input } from '@angular/core';

@Component({
    selector: 'app-chlid',
    templateUrl: './chlid.component.html',
    styleUrls: ['./chlid.component.scss']
})
export class TestNameComponent {
    @Input() firstname: string;
    constructor() { }
}

También puedes cambiar el nombre el Input especificando el nombre de la propiedad que quieras que este utilice al inicializar el componente.

@Input('my-name') firstname!: string;
// ó asi para hacer algo cuando se ingresan datos al Input, alter
@Input() set saludar(firstname: string) {
    console.log('Hola', firstname);
}

Con el signo de exclamación le decimos a Angular (o Typescript) que esa propiedad sí va a existir. @Input() set es una alternativa al ngOnChanges para poder escuchar en particular un cambio de un solo @Input Lo que va dentro de los parentesis dentro del @Input('<name>') es para cambiarle el nombre el input cuando se llama desde otro componente. Se usa property binding para acceder a este método.

<!-- Componente padre -->
<app-test-name [my-name]="'John'"></app-test-name>

Comunicación hijo a padre

A partir de la emisión de un evento, el decorador @Output() permite enviar mensajes desde un componente hijo hacia el padre.

// Componente hijo
@Component({
    selector: 'app-chlid',
    templateUrl: './chlid.component.html',
    styleUrls: ['./chlid.component.scss']
})
export class TestNameComponent {
    @Output() messageEvent = new EventEmitter<string>();
    sendMessage() {
        this.messageEvent.emit('Hola soy Platzi');
    }
}
<!-- Desde el componente padre se recibe -->
<app-test-name (messageEvent)="receiveMessage($event)">
</app-test-name>
// Componente padre
receiveMessage(event: string) {
    console.log(event);
}

Comunicación desde el componente hijo al padre mediante con ViewChild()

Mediante ViewChild, el padre crea el componente hijo y tiene acceso a sus datos y atributos:

En el hijo:

import { Component } from "@angular/core";

@Component({
  selector: "app-child",
  template: ``,
  styleUrls: ["./child.component.css"],
})
export class ChildComponent {
  message: string = "Hola Mundo!";

  constructor() {}
}

En el padre:

import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { ChildComponent } from "../child/child.component";

@Component({
  selector: "app-parent",
  template: `
    Message: {{ message }}
    <app-child></app-child>
  `,
  styleUrls: ["./parent.component.css"],
})
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent) child;

  constructor() {}

  message: string;

  ngAfterViewInit() {
    this.message = this.child.message;
  }
}

La particularidad de éste método es que tenemos que esperar a que la vista esté totalmente cargada para acceder a los atributos del hijo. Para ello creamos un método de Angular llamado ngAfterViewInit() en el que simplemente inicializamos la variable con el valor del atributo del hijo (el hijo lo declaramos como @ViewChild(ChildComponent)).

@ViewChild

Es posible que existan situaciones en las que desee acceder a una directiva, componente secundario o elemento DOM de una clase principal de componentes. El decorador ViewChild devuelve el primer elemento que coincide con una directiva, un componente o un selector de referencia de plantillas concreto.

Differencia entre ng-template, ng-container, and ng-content

1) <ng-template></ng-template>

El elemento <ng-template> se utiliza en Angular junto con directivas estructurales (como *ngIf, *ngFor, [ngSwitch] y directivas personalizadas). Estos elementos de plantilla no se renderizan por sí mismos, sino que se renderizan condicionalmente en el DOM. Permiten crear plantillas dinámicas que pueden ser personalizadas y configuradas.

<div> 
  Ng-template Content 
  <div *ngIf="false; else showNgTemplateContent"> 
    No se debería mostrar 
  </div>
</div>

<ng-template #showNgTemplateContent> Se debería mostrar </ng-template>

En el ejemplo, si la condición es falsa, se mostrará "No se debería mostrar"; de lo contrario, se mostrará el contenido del <ng-template> como "Se debería mostrar".

2) <ng-container></ng-container>

<ng-container> es una directiva sencilla que agrupa elementos en una plantilla sin afectar el DOM, estilos o diseño. Es útil cuando no deseas agregar elementos adicionales como <div> al DOM. (Parecido al React.Fragment)

<ng-container *ngIf="details">
  <div *ngFor="let info of details">
    {{ info.content }}
  </div>
</ng-container>

Este ejemplo muestra cómo usar <ng-container> para evitar el error de tener múltiples directivas estructurales en un solo elemento, sin agregar elementos adicionales al DOM.

3) <ng-content></ng-content>

Imagina que tienes un componente padre (ParentComponent) y un componente hijo (ChildComponent). Quieres que el componente padre pase contenido dinámico al componente hijo para ser mostrado.

Componente Hijo (child.component.html)

<h1>Información del Hijo</h1>
<ng-content></ng-content>

Componente Hijo (child.component.ts)

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

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {}

Componente Padre (parent.component.html)

<app-child>
  <div>Detalles del Componente Hijo</div>
</app-child>

Componente Padre (parent.component.ts)

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

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent {}

Resultado

En el navegador, se verá lo siguiente dentro del componente hijo:

Información del Hijo
Detalles del Componente Hijo

El contenido que se encuentra entre las etiquetas <app-child></app-child> en el componente padre se proyecta en el componente hijo en el lugar donde se encuentra <ng-content></ng-content>.

Uso de Selectores en <ng-content>

Puedes utilizar el atributo select para proyectar contenido específico basado en selectores. Aquí tienes un ejemplo más avanzado:

Componente Hijo con Selectores (child.component.html)

<h1>Información del Hijo</h1>
<ng-content select="[header]"></ng-content>
<ng-content select="[body]"></ng-content>
<ng-content select="[footer]"></ng-content>

Componente Padre con Selectores (parent.component.html)

<app-child>
  <div header>Cabecera del Contenido</div>
  <div body>Cuerpo del Contenido</div>
  <div footer>Pie del Contenido</div>
</app-child>

Resultado

En el navegador, el contenido se organizará de la siguiente manera dentro del componente hijo:

Información del Hijo
Cabecera del Contenido
Cuerpo del Contenido
Pie del Contenido

Este ejemplo muestra cómo puedes proyectar diferentes partes del contenido en ubicaciones específicas dentro de tu componente hijo usando selectores con <ng-content>.

Referencias

Para más información, puedes consultar la documentación oficial de Angular:

Resumiendo:

  • ng-content muestra contenido hijo en una plantilla.

  • ng-container agrupa elementos sin renderizar nada adicional en el DOM.

  • ng-template agrupa contenido que no se renderiza directamente, pero puede usarse en otras partes de la plantilla o código.

Decoradores

En Angular, los decoradores son una característica fundamental que permite a los desarrolladores añadir metadatos a las clases, métodos, propiedades y parámetros. Estos metadatos son utilizados por Angular para proporcionar funcionalidad adicional a las aplicaciones. Los decoradores en Angular se basan en la propuesta de decoradores de JavaScript y son funciones que toman un argumento y devuelven otro, generalmente una versión mejorada del original.

Tipos de Decoradores en Angular

  1. @Component: Este decorador se utiliza para definir una clase como un componente de Angular, asociando la clase a una plantilla HTML, estilos y lógica de negocio.

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'my-app';
    }
  2. @Directive: Define una clase como una directiva. Las directivas pueden modificar el comportamiento de elementos del DOM.

    import { Directive, ElementRef } from '@angular/core';
    
    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {
      constructor(el: ElementRef) {
        el.nativeElement.style.backgroundColor = 'yellow';
      }
    }
  3. @Injectable: Marca una clase como disponible para ser inyectada mediante el sistema de inyección de dependencias de Angular. Esto es crucial para los servicios.

    import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root',
    })
    export class DataService {
      constructor() { }
    }
  4. @NgModule: Declara una clase como un módulo de Angular. Los módulos organizan un conjunto de componentes, directivas, servicios y otros elementos relacionados.

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
  5. @Input y @Output: Estos decoradores se utilizan en componentes para definir propiedades que pueden recibir datos de componentes padre (@Input) o emitir eventos hacia componentes padre (@Output).

    import { Component, Input, Output, EventEmitter } from '@angular/core';
    
    @Component({
      selector: 'child-component',
      template: `<div>{{childProperty}}</div>`
    })
    export class ChildComponent {
      @Input() childProperty: string;
      @Output() childEvent = new EventEmitter<string>();
    
      emitEvent() {
        this.childEvent.emit('Hello Parent!');
      }
    }

Beneficios de Usar Decoradores

  • Claridad y Mantenibilidad: Los decoradores proporcionan una forma clara y concisa de declarar el comportamiento y la configuración de las clases.

  • Reutilización de Código: Permiten aplicar lógica común a múltiples clases de manera uniforme.

  • Metadatos: Facilitan la adición de metadatos a las clases, que pueden ser utilizados por Angular y otras bibliotecas para personalizar el comportamiento.

Ciclo de Vida

  • Constructor: Se ejecuta una sola vez antes del render, no se deben ejecutar funciones asincronas.

  • ngOnChanges: Corre antes y durante en el render, siempre que detecte cambios en el @Input.

  • ngOnInit: Se ejecuta una sola vez despues del render, aqui se pueden ejecutar funciones asincronas.

  • ngAfterViewInit: Se ejecuta despues del render y de que el componente y sus componentes hijos se han renderizado.

  • NgOnDestroy: Se ejecuta cuando se elimina el componente.

Estilos

Cada componente tiene un scope hacia sus propis estilos, lo que significa que un componente padre no pude actualizar los estilos del componente hijo directamente, para lograrlo de ese modo se pueden usar en los estilos globales, ahí si se pueden modificar los estilos utilizando las etiquetas que empiezan con app, ejemplo:

.products--grid{
    app-img {
        img {
            border: 1x solid red;
        }
    }
}

Uso de ng lint

Revisa el código del proyecto para ver si se están cumpliendo las reglas definidas en el archivo tslint.json

ng lint

Nos permite visualizar el resultado de la revisión con un formato json.

ng lint --format json

Corrige automáticamente, dentro de lo posible, los errores encontrados.

ng lint --fix

Servicios

Un servicio es la forma que utiliza Angular para modularizar nuestra aplicación y crear código reutilizable, este tendrá una determinada lógica de negocio que puede ser usada por varios componentes u otros servicios.

Dentro de los servicios Angular tambien puede hacer peticion http, y utiliza observables en lugar de Promesas puras, ademas de poder almacenar y compartir el estado de la aplicacion.

Inyección de dependencias

Imagínate que tienes el siguiente panorama: Un Servicio A que emplea el Servicio B y este a su vez utiliza el Servicio C.

Si tuvieses que instanciar el Servicio A, primero deberías: instanciar el C para poder continuar con el B y luego sí hacerlo con el A. Se vuelve confuso y poco escalable si en algún momento también tienes que instanciar el Servicio D o E.

La inyección de dependencias soluciona las dependencias de una clase por nosotros.

Cuando instanciamos en el constructor el servicio A, Angular por detrás genera automáticamente la instancia del servicio B y C sin que nosotros nos tengamos que preocupar por estos.

Este le proporcionó a la clase el decorador @Injectable({ ... }) con el valor providedIn: 'root' que determina el scope del servicio, o sea, determina que el mismo estará disponible en toda el módulo de tu aplicación por default.

Inyección de Dependencias (Dependency Injection o DI) es un patrón de diseño en el que una clase requiere instancias de una o más clases y en vez de generarlas dentro de su propio constructor, las recibe ya instanciadas por un mecanismo externo.

Singleton

La inyección de dependencias no es el único patrón de diseño que Angular usa con sus servicios. También hace uso del patrón Singleton para crear una instancia única de cada servicio.

Si tienes un servicio que se utiliza en N cantidad de componentes (u otros servicios), todos estarán utilizando la misma instancia del servicio y compartiendo el valor de sus variables y todo su estado.

Nota: EL patrón Singleton se refiere a la creación de una sola instancia de una clase, mientras que Dependency Injection se refiere a la forma en que las dependencias de una clase son administradas y pasadas. Aunque pueden parecer similares y ser utilizados juntos en algunos casos, son conceptos separados con objetivos distintos.

Solo debes tener cuidado con las dependencias circulares. Cuando un servicio importa a otro y este al anterior. Angular no sabrá si vino primero el huevo o la gallina y tendrás un error al momento de compilar tu aplicación.

Pipes

En Angular, los "pipes" son una característica poderosa que permite transformar los datos en las vistas de manera declarativa. Los pipes pueden ser usados para formatear los datos de diversas maneras sin necesidad de modificar el modelo de datos. Son similares a los filtros en otras tecnologías de front-end como AngularJS y permiten realizar operaciones comunes como formatear fechas, números, cadenas de texto, y más.

Características de los Pipes

  1. Declarativos: Se usan directamente en las plantillas (templates) de los componentes.

  2. Reutilizables: Se pueden definir una vez y reutilizar en múltiples componentes y plantillas.

  3. Puros e Impuros: Los pipes puros se recomiendan para transformaciones simples y no mutables, mientras que los impuros se utilizan para transformaciones más complejas y mutables.

Uso de Pipes en Angular

Sintaxis Básica

Los pipes se aplican en las plantillas usando el símbolo de barra vertical (|). La sintaxis básica es:

htmlCopy code{{ data | pipeName }}

Pipes Incorporados en Angular

Angular proporciona varios pipes incorporados que cubren las necesidades comunes:

  • DatePipe: Formatea fechas.

  • UpperCasePipe y LowerCasePipe: Convierte el texto a mayúsculas o minúsculas.

  • CurrencyPipe: Formatea números como moneda.

  • DecimalPipe: Formatea números decimales.

  • PercentPipe: Formatea números como porcentaje.

  • JsonPipe: Convierte un objeto a una cadena JSON.

Ejemplos de Uso

Formatear una Fecha

htmlCopy code<p>{{ currentDate | date:'fullDate' }}</p>

Esto mostrará currentDate formateada como una fecha completa, por ejemplo, "Wednesday, June 14, 2023".

Convertir a Mayúsculas

htmlCopy code<p>{{ name | uppercase }}</p>

Esto convertirá el valor de name a mayúsculas.

Formatear un Número como Moneda

htmlCopy code<p>{{ price | currency:'USD':true }}</p>

Esto mostrará price formateado como una moneda en dólares estadounidenses.

Creación de Pipes Personalizados

Además de los pipes incorporados, puedes crear tus propios pipes personalizados para casos de uso específicos. Aquí hay un ejemplo de cómo crear y usar un pipe personalizado.

Definir un Pipe Personalizado

Primero, se define el pipe utilizando el decorador @Pipe y se implementa la interfaz PipeTransform:

typescriptCopy codeimport { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'exponentialStrength'
})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: number): number {
    return Math.pow(value, exponent);
  }
}

Usar el Pipe Personalizado

Después de definir el pipe, se puede usar en una plantilla de la misma manera que los pipes incorporados:

htmlCopy code<p>{{ 2 | exponentialStrength: 10 }}</p>

Esto elevará 2 a la potencia de 10, mostrando 1024.

Conclusión

Los pipes en Angular son una herramienta poderosa y flexible para transformar y formatear datos directamente en las plantillas. Los pipes incorporados cubren muchas necesidades comunes, y los pipes personalizados permiten extender la funcionalidad de manera específica para la aplicación. Usar pipes mejora la legibilidad y la mantenibilidad del código al separar la lógica de formateo de los datos del propio modelo de datos.

Directivas

Angular utiliza el concepto de directiva para cambiar la apariencia o el comportamiento de un elemento en el HTML. Las directivas se usan para modificar el DOM de forma directa y también se pueden modificar los atributos aunque no es lo recomendable.

Se usa @HostListener('mouseleave') para escuchar eventos del elemento

Pasando datos a una directiva Finalmente, si tienes la necesidad de que tu directiva reciba algún tipo de valor, lo mejor es apoyarte en el decorador que ya conoces @Input().

import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
  selector:'[appHighlight]'
})
export classHighlightDirectiveimplementsOnInit {

  constructor(
    private element: ElementRef
  ) { }

  @Input() backgroundColor: string;
  @Input() color: string;
  ngOnInit(): void {
    this.element.nativeElement.style.backgroundColor = this.backgroundColor;
    this.element.nativeElement.style.color = this.color;
  }
}

Estado de la aplicación

Esto se logra implementando un store de datos, es decir, un lugar donde estan los estados que se desean compartir, los store de datos suelen implementarse haciendo uso de Observables. Esto se logracon la libreir rxjs.

Sepuede usar de la siguiente manera

//store.service.ts

private myShoppingCart: Product[] = [];
private myCart = new BehaviorSubject<Product[]>([]);

// Luego se expone un observable
public myCart$ = this.myCart.asObservable();

// La manera de notificar a todos los subscriptores de ese observable se hará así por ejemplo
addProduct(product: Product): void {
    // El observable emitirá un nuevo valor con cada producto que se agregue al carrito.
    this.myCart.next(this.myShoppingCart);
}

//nav.component.ts
private sub$!: Subscription;

ngOnInit(): void {
    this.storeService.myCart$.subscribe((products) => {
        // Cada vez que el observable emita un valor, se ejecutará este código
        this.totalProducts = products.length;
        this.total = this.storeService.getTotal();
    });
}

// Eliminar la subscripción
ngOnDestroy(): void {
    this.sub$.unsubscribe();
}

Consumo de API con Angular

  1. Comienza importando el módulo HttpClientModule, en el módulo principal de tu aplicación desde @angular/common/http.

  2. Crea un servicio para realizar todos los llamados a una API que necesites. En este servicio tienes que importar el cliente HTTP de Angular y crear un método por cada endpoint para el que necesites efectuar una petición.

  3. Importa este nuevo servicio en tus componentes para efectuar los llamados a la API. Todo el cliente HTTP de Angular está basado en Observables, por lo tanto, es recomendable suscribirse al método del servicio para obtener los datos cuando la API responda.

Parámetros y headers

Los endpoints del tipo GET suelen recibir información por parámetros de URL. Por ejemplo:

https://example.com/api/productos?offset=0&limit=10 https://example.com/api/productos?q=XXXXX

getAllProducts(limit?: number, offset?: number){
    let params = new HttpParams();
    if(limit != undefined && offset != undefined){
      params = params.set('limit', limit);
      params = params.set('offset', offset);
    }
    let headers = new HttpHeaders();
    headers = headers.set('Authorization', `Bearer ${token}`);
    return this.http.get<Product[]>(this.apiUrl, { params, headers });
}

Observable vs. Promesa

Observables

Utilizan el patrón de diseño “observador” que centraliza la tarea de informar un cambio de estado de un determinado dato o la finalización de un proceso, notificando a múltiples interesados cuando esto sucede sin necesidad de que tengan que consultar cambios activamente.

Características:

  • Emiten múltiples datos

  • Permiten escuchar cualquier tipo de proceso, (peticiones a una API, lectura de archivos, etc.)

  • Notifican a múltiples interesados

  • Pueden cancelarse

  • Manipulan otros datos (transformar, filtrar, etc.) con rxjs.

  • Son propensos al callback hell

Ejemplo:

import { Observable } from 'rxjs';

const getAnObservable$ = () => {
    return new Observable(observer => {
        observer.next('Valor 1');
        observer.next('Valor 2');
        observer.next('Valor 3');
    });
};
(() => {
    getAnObservable$
        .pipe(
            // Manipulación de resultados con RxJS
        )
        .subscribe(res => {
        console.log(res);
        });
})

Promesas

Las promesas son un método algo más sencillo y directo para manipular procesos asincrónicos en Javascript.

Características:

  • Tienen dos posibles estados resuelto y rechazado

  • Ofrecen mayor simplicidad

  • Emiten un único valor

  • Evitan el callback hell

  • No se puede cancelar

  • Proveen una robusta API nativa de Javascript disponible desde ES 2017

  • Constituyen librerías populares como AXIOS o Fetch

Observable a Promesa

import { of, firstValueFrom, lastValueFrom } from 'rxjs';

observableToPromise(): Promise<string> {
    return lastValueFrom(of('¡Soy una promesa!'));
}

Promesa a Observable

import { from } from 'rxjs';

PromiseToObservable(): Observable<any> {
    return from(new Promise((resolve, reject) => { console.log('¡Soy un observable!') }));
}

En los pipes se pueden usar otras funciones de rxjs como filter, retry(3), catchError, map, tap etc.

CORS

Se pueden saltar los CORS con un archivo proxy.config.json

{
  "/api/*": {
    "target": "<API_URL>",
    "secure": true,
    "logLevel": "debug",
    "changeOrigin": true
  }
}
// en package.json
"start:proxy": "ng serve --proxy-config ./proxy.config.json",

Manejo de errores

En el servicio

// service/api.service.ts
getProduct(idProduct: number): Observable<Product> {
  return this.http.get<Product>(`https://example.com/api/productos/${idProduct}`)
    .pipe(
      catchError((err: HttpErrorResponse) => {
        return this.handleErrors(err)
      })
    );
}

handleErrors(error: HttpErrorResponse): Observable<never>  {
  if (error.status == HttpStatusCode.Forbidden)
    return throwError('No tiene permisos para realizar la solicitud.');
  if (error.status == HttpStatusCode.NotFound)
    return throwError('El producto no existe.');
  if (error.status == HttpStatusCode.InternalServerError)
    return throwError('Error en el servidor.');
  return throwError('Un error inesperado ha ocurrido.');
}

En el componente

// components/catalogo/catalogo.component.ts
this.apiService.getProducts()
  .subscribe(res => {
    this.productos = res;
  }, err => {
    alert(err);     // Aquí se emitirá el alerta con el mensaje que `throwError` devuelva.
});

Solucionar el Infierno de Callbacks

import { switchMap } from 'rxjs/operators';
readAndUpdate(): void {
  // Solución callback hell
  this.apiService.getProduct(1)
    .pipe(
      switchMap(products => this.apiService.updateProduct(1, { name: 'Nuevo nombre' }) ),
      switchMap(...),
      switchMap(...),
    )
    .subscribe(res => {
      // Producto actualizado
    });
}

Promise.all([]) en Observables

import { zip } from 'rxjs';
readAndUpdate(): void {
  // Agrupando observables en un mismo subscribe
  zip(
    this.apiService.getProduct(1),
    this.apiService.updateProductPATCH(1, { name: 'Nuevo nombre' })
  )
  .subscribe(res => {
    const get = res[0];
    const update = res[1];
  });
}

Interceptors

// interceptors/token-interceptor.interceptor.ts
@Injectable()
export class TokenInterceptorService implements HttpInterceptor {

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    request = this.addHeaders(request);
    return next.handle(request)
  }

  private addHeaders(request: HttpRequest<any>) {
    let token: string | null = '';
    token = localStorage.getItem('platzi_token');
    if (token) {
      return request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    } else {
      return request;
    }

  }
}

// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TokenInterceptorService } from './interceptors/token.interceptor';

@NgModule({
  // ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptorService,
      multi: true
    }
  ]
})
export class AppModule { }

Aplicando el patrón: Smart and Dumb components

Dumb component

Debe encargarse de la desplegar y visualizar datos e interacción.

Smart component

Trae datos para mandarlos a al dumb component y manejan la logica y flujos de datos.

Los dumb components tienen de apellido .component y los smart components .container

Ambientes de Desarrollo y Producción

En tu proyecto de encontrarás una carpeta llamada environments y por defecto con dos archivos dentro (Los nombres son diferentes dependendo de la versión):

  • environment.development.ts

  • environment.ts

Ambos lucen de manera muy similar.

// environment.development.ts
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.

export const environment = {
  production: false
};
// environments.ts
export const environment = {
  production: true
};

Nota: Con la versión 15+ se necesitan generar manualmente con el comando

ng generate environments

En el archivo angular.json agrega la configuración para cada ambiente

...
"configurations": {
...
  "development": {
    ...
    "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.development.ts"
    }
  }
...

Build

Para mejorar la caché en Angular cambiando el nombre de los archivos generados por ng build, puedes utilizar la técnica de "output hashing". Esta técnica agrega un hash único a los nombres de los archivos cada vez que se genera una nueva versión de la aplicación. Así, cuando el contenido de un archivo cambia, también lo hace su nombre, obligando al navegador a descargar la nueva versión en lugar de usar la versión en caché.

Pasos para Implementar Output Hashing en Angular

  1. Configura el Output Hashing en el Archivo angular.json:

    Debes asegurarte de que el archivo angular.json esté configurado para incluir hashing en los nombres de los archivos. Esto se puede hacer en la configuración de construcción de producción.

    Abre angular.json y encuentra la configuración de build dentro de projects.[your-project-name].architect.build.options. Añade o asegúrate de que la opción outputHashing esté establecida en all.

    {
      "projects": {
        "your-project-name": {
          "architect": {
            "build": {
              "options": {
                "outputHashing": "all"
              }
            }
          }
        }
      }
    }
  2. Construir la Aplicación con el Comando ng build:

    Ejecuta el comando ng build con la opción --prod para asegurarte de que se construya en modo producción y que se apliquen los hashes.

    ng build --prod --output-hashing=all

    Esto generará los archivos de tu aplicación en la carpeta dist con nombres que incluyen un hash, por ejemplo, main.abc123.js.

Verificación de los Archivos Generados

Después de ejecutar el comando de build, revisa la carpeta dist para asegurarte de que los archivos tienen nombres con hashes:

dist/
  |- index.html
  |- main.abc123.js
  |- polyfills.abc123.js
  |- runtime.abc123.js
  |- styles.abc123.css
  |- vendor.abc123.js

Conclusión

Usar el "output hashing" en Angular es una técnica efectiva para mejorar la gestión de la caché. Esta práctica asegura que los usuarios siempre obtengan la versión más reciente de los archivos, evitando problemas de caché obsoleta y mejorando la experiencia del usuario.

Last updated