angular

John Cardozo

John Cardozo

angular

qué es angular?

Framework de desarrollo web para frontend basado en componentes

Typescript

Librerías

Routing

Formularios

Comunicación cliente-servidor

Administración de Estado

Herramientas

Desarrollo

Debug

Testing

Building

SPA

2010

JS

2016

2.0

2017

4

5

2018

6

7

2023

17

...

...

características de angular

Fácil manipulación del DOM

Typescript

2-way data binding

Organización Modular

Pruebas unitarias

Larga curva de aprendizaje

Opciones SEO limitadas

Comunidad Robusta

Angular CLI

Apps web, desktop, móviles

uso de angular

State of JS

Stackoverflow Insights 2023

Who Uses Angular in 2023? 12 Global Websites Built With Angular

nodejs

Instalación

Verificación

node --version

npm --version

Node Package Manager

Node

instalación del entorno

Angular CLI

Creación del proyecto

npm install -g @angular/cli

Instalación

Generar aplicaciones

Agregar recursos

Ejecución de pruebas

Bundling

Deployment

Verificación

ng version

Habilitar la ejecución de Scripts

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Si hay problemas de ejecución se puede ejecutar el comando anterior

creación del proyecto

ng new nombre-proyecto

Workspace

Project

*

Creación de Workspace + Proyecto

cd nombre-proyecto
ng serve --open

Ejecución del proyecto

El parámetro --open abre automáticamente el browser

Extension: Angular Language Service

ng new expenses-tracker

Ejemplo

estructura de archivos

Pruebas unitarias

Configuración de Typescript

Dependencias y Scripts

Información del Workspace

Folder de código fuente

Estilos globales

Punto inicial de la aplicación

SPA: única página de la app

Componente Inicial

Configuración de la aplicación

Definición de rutas

PUNTOS iniciales deL proyecto

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

main.ts

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>expenses-tracker</title>
  <base href="/">
</head>
<body>
  <app-root></app-root>
</body>
</html>

index.html

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  title = 'expenses-tracker';
}

app.component.ts

<main class="main">
  <div class="content">
    <div class="left-side">
      <h1>Hello, {{ title }}</h1>
    </div>
    <div class="right-side">
      <div class="pill-group">
        @for (item of [
          { title: 'Docs', link: 'https://...' },
          { title: 'Tutorials', link: 'https://...' },
          { title: 'CLI', link: 'https://...' },
        ]; track item.title) {
          <a class="pill" [href]="item.link">
            <span>{{ item.title }}</span>
          </a>
        }
      </div>
    </div>
  </div>
</main>

<router-outlet></router-outlet>

app.component.html

Router oulet

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};

app.config.ts

programación modular

Módulo

Contenedor de código dedicado a un dominio de la aplicación o un conjunto de capacidades estrechamente relacionadas

Módulo

Componentes

Servicios

Directivas

Componente

Sección funcional de la aplicación que cuenta con lógica de negocio, presentación, visualización, pruebas, etc

Componente

Presentación

Visualización

Lógica

Pruebas

HTML

CSS

TS

ejemplo a desarrollar

creación de un componente

ng g c components/balance

Instrucción para generar un nuevo componente

generate

component

ruta y nombre

Archivos generados

Presentación

Visualización

Pruebas unitarias

Lógica

app.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { BalanceComponent } 
from './components/balance/balance.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, BalanceComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  title = 'expenses-tracker';
}

components / balance / balance.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
  selector: 'app-balance',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './balance.component.html',
  styleUrl: './balance.component.scss'
})
export class BalanceComponent {  }

@Component( ) define un nuevo componente

app.component.html

<div class="content">
  <app-balance />
</div>

enlazando un valor en el componente

<h2>Welcome to {{ title }}</h2>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'My application';
  updateTitle() {
    this.title = 'New value for my application';
  }
}

app.component.html

app.component.ts

El template obtiene el valor de la variable

One way binding

Two way binding: Forms module

import { FormsModule } 
from '@angular/forms';
@Component({
  selector: 'app-root',
  imports: [CommonModule, 
            RouterOutlet, 
            FormsModule],
})
export class AppComponent {
  search: string = 'iPhone';
}

app.component.ts

<input type="text" [(ngModel)]="search" />
<span>{{ search }}</span>

app.component.html

[ ]

Propiedad

( )

Evento

función

necesario!

parámetros

parámetros

Decorador @Input

input: parámetros básicos al componente

components / balance / balance.component.ts

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

@Component({
  selector: 'app-balance',
  templateUrl: './balance.component.html',
  styleUrls: ['./balance.component.scss'],
})
export class BalanceComponent {
  @Input() title: string = '';
}

El decorator Input define un dato de entrada al componente

components / balance / balance.component.html

<div>
  <h1>{{ title }}</h1>
</div>

app.component.html

<app-balance title="Balance" />

El selector definido en el componente es usado en el template de algún otro componente

El template envía el dato al componente

datos complejos en el componente

components / balance / balance.component.ts

import { Component, Input } from '@angular/core';
import { Balance } from '../../models/balance.model';

@Component({ ... })

export class BalanceComponent {
  @Input() balance: Balance = {
    amount: 55_000,
    income: 100_000,
    expenses: 45_000,
  };
}

components / balance / balance.component.html

<div>
  <h1>Amount {{ balance.amount }}</h1>
  <h1>Income {{ balance.income }}</h1>
  <h1>Expenses {{ balance.expenses }}</h1>
</div>

models / balance.model.ts

export interface Balance {
  amount: number;
  income: number;
  expenses: number;
}

Se usan los atributos de la interface para inicializar el componente

app.component.html

<app-balance />

Uso del componente

control flow: @if - @else

components / balance / balance.component.ts

import { Component, Input } from '@angular/core';
import { Balance } from '../../models/balance.model';

@Component({ ... })
export class BalanceComponent {
  @Input() balance!: Balance;
}

components / balance / balance.component.html

<div>
  <h1>Amount {{ balance.amount }}</h1>
  <h1>Income {{ balance.income }}</h1>
  <h1>Expenses {{ balance.expenses }}</h1>
</div>

El operador ! indica que no se proveen datos aún pero en el futuro existirán

Dado que los atributos no existen, se genera un error

@if(balance) {
  <h1>Amount {{ balance.amount }}</h1>
  <h1>Income {{ balance.income }}</h1>
  <h1>Expenses {{ balance.expenses }}</h1>
}

@if

Condicional en el template que permite verificar el valor del objeto

@if(balance) {
  <h1>Amount {{ balance.amount }}</h1>
  <h1>Income {{ balance.income }}</h1>
  <h1>Expenses {{ balance.expenses }}</h1>
} @else {
  <h1>No balance available</h1>
}

@else

uso del componente y envío de parámetros

app.component.ts

import { Component } from '@angular/core';
import { Balance } from './models/balance.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  balance: Balance = {
    amount: 55_000,
    income: 100_000,
    expenses: 45_000,
  };
}

app.component.html

<app-balance [balance]="balance" />

Importa el modelo

Crea el objeto en la lógica

Usa el objeto en el template

scss: estilos globales + app inicial

styles.scss

@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

$font-family: "Montserrat", sans-serif;

app / app.component.scss

.container {
  background-color: #f3f5f7;
  width: 100vw;
  height: 100vh;
  display: grid;
  grid-template-rows: 150px auto;
  padding: 20px;
}

app / app.component.html

<div class="container">
  <app-balance [balance]="balance" />
</div>

componente balance: scss + html

components / balance / balance.component.scss

@use "../../../styles" as v;

.balance {
  background-image: linear-gradient(
    to right bottom,
    #2da9e4, #31a4f2, #4d9cfc, #7292ff,
    #9984ff, #c079f3, #df6de3, #f960ce,
    #ff65b0, #ff7297, #ff8284, #fb937a
  );
  box-shadow: rgba(50, 50, 105, 0.15) 0px 2px 5px 0px,
    rgba(0, 0, 0, 0.05) 0px 1px 1px 0px;
  border-radius: 20px;
  padding: 25px;
  color: #fff;
  font-family: v.$font-family;

  &__header {
    h4 {
      font-weight: 200;
      text-align: center;
    }
    h2 {
      font-size: 2.2rem;
      font-weight: 500;
      text-align: center;
    }
  }
  &__info {
    display: flex;
    justify-content: space-between;

    h4 {
      font-weight: 300;
      font-size: 0.9rem;
    }
    h3 {
      font-size: 1.3rem;
      font-weight: 600;
    }
  }
}

components / balance / balance.component.html

@if(balance) {
<div class="balance">
  <div class="balance__header">
    <h4>Total Balance</h4>
    <h2>{{ balance.amount | currency }}</h2>
  </div>
  <div class="balance__info">
    <div>
      <h4>Income</h4>
      <h3>{{ balance.income | currency }}</h3>
    </div>
    <div>
      <h4>Expenses</h4>
      <h3>{{ balance.expenses | currency }}</h3>
    </div>
  </div>
</div>
}

Paleta de colores

Gradient Generator

Box shadow examples

Puede tener opciones - currency: "EUR"

Pipe

mostrar lista de componentes: @for - I

app.component.ts

import { Transaction }
from './models/transaction.model';
import { TransactionsComponent } 
from './components/transactions/transactions.component';

@Component({
  imports: [TransactionComponent]
})
export class AppComponent {
  transactions: Transaction[] = [
    { id: "1", type: 'expense', amount: 45, 
      category: 'food', date: new Date(2023, 6, 26),
    },
    { id: "2", type: 'expense', amount: 280,
      category: 'shopping', date: new Date(2023, 6, 24),
    },
    { id: "3", type: 'expense', amount: 60,
      category: 'entertainment', date: new Date(2023, 6, 22),
    },
    { id: "4", type: 'income', amount: 500,
      category: 'payroll', date: new Date(2023, 6, 20),
    },
  ];
}

models / transaction.model.ts

export interface Transaction {
  id: string;
  type: string;
  amount: number;
  category: string;
  date: Date;
}

Este componente se crea en el siguiente Slide

<div class="container">
  <app-balance 
    [balance]="balance" />
  <app-transactions 
    [transactions]="transactions" />
</div>

app.component.html

mostrar lista de componentes: @for - II

components / transactions / transactions.component.ts

import { Component, Input } from '@angular/core';
import { Transaction } from '../../models/transaction.model';

@Component({
  selector: 'app-transactions',
  templateUrl: './transactions.component.html',
  styleUrls: ['./transactions.component.scss'],
})
export class TransactionsComponent {
  @Input() transactions!: Transaction[];
}

models / transaction.model.ts

export interface Transaction {
  id: string;
  type: string;
  amount: number;
  category: string;
  date: Date;
}

components / transactions / transactions.component.html

@for(transaction of transactions; track transaction.id) {
  <div>
    <p>{{ transaction.category }}</p>
    <p>{{ transaction.amount }}</p>
    <p>{{ transaction.date }}</p>
  </div>  
} @empty {
  <h3>No transactions</h3>
}

ngFor

Recorre una lista de objetos en el template

ng g c components/transactions

Crear componente

Extensión para Chrome

componente hijo: transaction - I

components / transaction / transaction.component.ts

import { Component, Input } from '@angular/core';
import { Transaction } 
  from '../../models/transaction.model';
@Component({
  selector: 'app-transaction',
  templateUrl: './transaction.component.html',
  styleUrls: ['./transaction.component.scss'],
})
export class TransactionComponent {
  @Input() transaction!: Transaction;
}
ng g c components/transaction

Crear componente

components / transactions / transactions.component.html

<div class="transactions">
  @for (transaction of transactions; track transaction.id) {
    <app-transaction [transaction]="transaction" />
  } @empty {
    <h3>No transactions</h3>
  }
</div>

@if - componente hijo: transaction - II

components / transaction / transaction.component.html

<div class="transaction">
  <div class="transaction__title">
    @if (transaction.type === 'income') {
    <div class="transaction__icon transaction__income-icon">
      ↑
    </div>
    }
    @if(transaction.type === 'expense') {
    <div class="transaction__icon transaction__expense-icon">
      ↓
    </div>
    }
    <h5 class="transaction__category">{{ transaction.category }}</h5>
  </div>
  <div>
    <h5 class="transaction__amount">{{ transaction.amount | currency }}</h5>
    <h5 class="transaction__date">{{ transaction.date | date }}</h5>
  </div>
</div>
@if(transaction.type === 'income') {
  <div class="transaction__icon transaction__income-icon">
    ↑
  </div>
} @else {
  <div class="transaction__icon transaction__expense-icon">
    ↓
  </div>
}

@if

Alternativa: @else

date: "short"

Opciones del Pipe

date: "medium"

date: "long"

date: "full"

componente hijo: transaction - III

components / transaction / transaction.component.scss

@use "../../../styles" as v;
.transaction {
  background-color: #fff;
  border-radius: 15px;
  font-family: v.$font-family;
  padding: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px;
  &__title {
    display: flex;
    gap: 10px;
  }
  &__category {
    color: #324760;
    font-weight: 600;
    font-size: 1.1rem;
    text-transform: capitalize;
  }
  &__amount {
    color: #324760;
    font-size: 1rem;
    font-weight: 500;
    text-align: right;
  }
  &__date {
    color: #a0a0a0;
    font-weight: 400;
    text-align: right;
  }
  &__icon {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    text-align: center;
  }
  &__expense-icon {
    color: #db4d99;
    background-color: #ffc0e2;
  }
  &__income-icon {
    color: #5bb130;
    background-color: #cdffb3;
  }
}
.transactions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

components / transactions / transactions.component.scss

directivas de estilos

directivas de estilos

ngStyle + ngclass - Google Fonts

uso de iconos - google fonts

Google Fonts - Material Symbols

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />

index.html

Selecciona el ícono y se extrae la ruta CSS del CDN

Se obtiene el markup y se utiliza en los templates

<span class="material-symbols-outlined">
  delete
</span>

components / transaction / transaction.component.html

Este código inserta el ícono trash en el template

<div class="transaction">
  ...
  <div class="transaction__title">
    <h5 class="transaction__category">
      {{ transaction.category }}
      <span class="material-symbols-outlined">
        delete
      </span>
    </h5>
  </div>
  ...
</div>

components / transaction / transaction.component.scss

.transaction {
  ...
  &__remove {
    color: #ff7791;
    font-size: 0.9rem;
    vertical-align: middle;
    cursor: pointer;
  }
}

directiva - ng class

components / transaction / transaction.component.html

<div>
  <h5 class="transaction__amount"
      [ngClass]="
                 transaction.type === 'expense'
                 ? 'transaction__amount-expense'
                 : 'transaction__amount-income'
                 ">
    {{ transaction.amount | currency }}
  </h5>
  <h5 class="transaction__date">{{ transaction.date | date }}</h5>
</div>

Modifica la manera en que se ve el campo "amount"

.transaction {
  &__amount-expense {
    color: #db4d99;
  }
  &__amount-income {
    color: #5bb130;
  }
}

Una directiva es una clase que agrega comportamiento adicional a los elementos de un template

NgClass: agrega una clase CSS condicionalmente

components / transaction / transaction.component.scss

directiva - ng style

components / transaction / transaction.component.html

<h5 class="transaction__date" [ngStyle]="dateStyles">
  {{ transaction.date | date }}
</h5>

components / transaction / transaction.component.ts

export class TransactionComponent {
  
  dateStyles: Record<string, string> = {
    'border-bottom': '1px dashed gray',
  };

}

Agrega estilos CSS inline

Modifica la manera en que se ve el campo "date"

Eventos

eventos

Event listeners  + Directiva @Output

eventos

components / transaction / transaction.component.scss

.transaction {
  ...
  &__remove {
    color: #ff7791;
    font-size: 0.9rem;
    vertical-align: middle;
    cursor: pointer;
  }
}

components / transaction / transaction.component.html

<h5 class="transaction__category">
  {{ transaction.category }}
  <span 
    class="material-symbols-outlined transaction__remove"
    (click)="removeHandler()">
    delete
  </span>
</h5>

components / transaction / transaction.component.ts

export class TransactionComponent {
  removeHandler = () => {
    console.log('removing transaction...');
  };
}

Evento Click

enviando eventos al componente padre

components / transaction / transaction.component.ts

import { Output, EventEmitter } from '@angular/core';

export class TransactionComponent {
  @Output() removeTransacionEvent = new EventEmitter<string>();
  removeHandler = () => {
    this.removeTransacionEvent.emit(this.transaction.id);
  };
}

Output

Permite el envío de datos de un componente hijo al padre

EventEmitter

Permite la generación de eventos

Establece la propiedad como dato de salida

Emite el evento al componente padre

components / transactions / transactions.component.html

@for(transaction of transactions; track transaction.id) {
  <app-transaction
    [transaction]="transaction"
    (removeTransactionEvent)="removeTransaction($event)"
  />
}

components / transactions / transactions.component.ts

export class TransactionsComponent {
  removeTransaction(transactionId: string) {
    console.log('Removing on Transactions component...');
    console.log(transactionId);
  }
}

El componente Padre escucha el evento emitido por el componente hijo

ejemplo: eliminando una transacción

components / transactions / transactions.component.ts

export class TransactionsComponent {
  @Output() removeTransacionEvent = new EventEmitter<string>();
  removeTransaction(transactionId: string) {
    this.removeTransacionEvent.emit(transactionId);
  }
}

app.component.html

<app-transactions 
  [transactions]="transactions"
  (removeTransacionEvent)="removeTransaction($event)"
/>

app.component.ts

export class AppComponent {
  removeTransaction(transactionId: string) {
    console.log(`removing ${transactionId}...`);
    this.transactions = this.transactions.filter((t) => t.id !== transactionId);
  }
}

Child

Parent

mostrar templates condicionales

components / transactions / transactions.component.scss

.transactions {
  &__empty-title {
    font-family: s.$font-family;
    color: #eb68da;
    font-size: 1.1rem;
    font-weight: 400;
    text-align: center;
    padding: 20px;
    background-color: #fff;
    border-radius: 10px;
    box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px;
  }
}

componentes / transactions / transactions.component.html

<div class="transactions">
  <h3>Transactions</h3>
  <div *ngIf="transactions.length > 0; then noEmpty; else empty">
    Contenido ignorado
  </div>
  <ng-template #noEmpty>
    <app-transaction
      *ngFor="let transaction of transactions"
      [transaction]="transaction"
      (removeTransacionEvent)="removeTransaction($event)"
    />
  </ng-template>
  <ng-template #empty>
    <h2 class="transactions__empty-title">No transactions</h2>
  </ng-template>
</div>

routing

Angular Router

routing

routing - navegación entre páginas

Página 1

Página 2

Página 3

Componentes

App

Navegación entre diferentes páginas

Angular Router

routing: agregar una transacción

ng g c pages/addTransaction
ng g c pages/about

Crear "páginas"

ng g c pages/home

app.routes.ts

import { Routes } from '@angular/router';

import { AboutComponent } from './pages/about/about.component';
import { AddTransactionComponent } from './pages/add-transaction/add-transaction.component';
import { HomeComponent } from './pages/home/home.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'add', component: AddTransactionComponent },
];

Se configuran las rutas de cada componente

Se importan los componentes

Determina el path

app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient(withFetch())],
};

mover la funcionalidad de app a home

pages / home / home.component.ts

export class HomeComponent {
  balance: Balance = { ... };
  transactions: Transaction[] = [... ];
  removeTransaction(transactionId: string) { ... }
}

pages / home / home.component.html

<div class="main">
  <app-balance [balance]="balance" />
  <app-transactions
    [transactions]="transactions"
    (removeTransacionEvent)="removeTransaction($event)"
  />
</div>

pages / home / home.component.scss

.main {
  display: grid;
  grid-template-rows: 150px auto;
  gap: 20px;
  padding: 20px;
}

app.component.ts

export class AppComponent {
  
}

app.component.html

<div class="container">
  <router-outlet></router-outlet>
</div>

app.component.scss

.container {
  background-color: #f3f5f7;
  width: 100vw;
  height: 100vh;
}

HOME

APP

En <router-outlet> se carga el componente que hace match con la ruta

agregar link hacia add transaction

components / transactions / transactions.component.html

<div class="transactions">
  <div class="transactions__header">
    <h3>Transactions</h3>
    <a routerLink="add">Add</a>
  </div>
</div>

components / transactions / transactions.component.scss

.transactions {
  &__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 10px;
    a {
      font-family: s.$font-family;
      text-decoration: none;
      font-size: 0.9rem;
      color: #2fa5ef;
    }
  }
}

Ruta de navegación

Rutas:

/

add

about

routerLink como atributo del anchor

Si se usa href, la página se recarga

<a routerLink="usuarios" [queryParams]="{id: 1}" fragment="basics">
texto
</a>

/usuarios/?id=1#basics

components / transactions / transactions.component.ts

import { Router, RouterLink } 
from '@angular/router';

@Component({ ... })
export class AddTransactionComponent {
  constructor(
    private router: Router
  ) {}
}

formularios

Reactive Forms

formularios

formularios

Template Driven Form

Reactive Form

Formulario básicos

Fáciles de utilizar

Basados en template HTML

Formularios complejos

Ofrecen más control

Basados en clase Typescript

formulario add transaction - markup

pages / add-transaction / add-transaction.component.html

<div class="add-transaction">
  <h2>Add transaction</h2>
  <form class="add-transaction__form" autocomplete="off">
    <input type="text" id="amount" class="add-transaction__amount" autofocus />
    <div class="add-transaction__type">
      <input type="radio" name="type" value="expense" checked />
      <label for="expense">Expense</label>
      <input type="radio" name="type" value="income" />
      <label for="income">Income</label>
    </div>
    <select id="category">
      <option value="food">Food</option>
      <option value="entertainment">Entertainment</option>
      <option value="payroll">Payroll</option>
      <option value="shopping">Shopping</option>
    </select>
    <input type="date" id="date" />
    <button type="submit">Save</button>
  </form>
  <a routerLink="/">Cancel</a>
</div>

Detalles del formulario

formulario add transaction - style

pages / add-transaction / add-transaction.component.scss

@use "../../../styles" as s;

.add-transaction {
  padding: 50px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;

  h2 {
    font-family: s.$font-family;
    text-align: center;
    color: #677cb3;
    font-weight: 500;
  }
  &__form {
    margin-top: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
  }
  &__amount {
    border-radius: 9999px;
    border: none;
    outline: none;
    font-size: 3rem;
    padding: 0.3rem 2rem;
    font-family: s.$font-family;
    font-weight: 300;
    color: #7196eb;
    width: 20rem;
  }
  &__type {
    display: flex;
    font-family: s.$font-family;
    font-size: 1.2rem;
    color: #415481;
    margin: 5px;
    label {
      margin-left: 0.4rem;
      margin-right: 1.5rem;
    }
    //https://moderncss.dev/pure-css-custom-styled-radio-buttons/
    input[type="radio"] {
      // Reset appearance
      appearance: none;
      // Normal state
      background-color: #fff;
      width: 1.2rem;
      height: 1.2rem;
      border: 1px solid #68428c;
      border-radius: 50%;
      transform: translateY(0.2rem);
      // Display checke mark
      display: grid;
      place-content: center;
      &::before {
        content: "";
        width: 0.8rem;
        height: 0.8rem;
        border-radius: 50%;
        transform: scale(0);
        box-shadow: 
          inset 1rem 1rem #ba7af4;
      }
      &:checked::before {
        transform: scale(1);
      }
    }
  } // Fin de &__type
  input[type="date"],
  select {
    font-family: s.$font-family;
    font-size: 1.5rem;
    font-weight: 200;
    color: #415481;
    padding: 1rem 1.5rem;
    border-radius: 8px;
    border: none;
    outline: none;
  }
  button {
    border: none;
    outline: none;
    font-family: s.$font-family;
    background-image: linear-gradient(
      to right bottom,
      #2da9e4, #31a4f2, #4d9cfc, #7292ff,
      #9984ff, #c079f3, #df6de3, #f960ce,
      #ff65b0, #ff7297, #ff8284, #fb937a
    );
    color: #fff;
    font-size: 1.5rem;
    padding: 1rem 1.5rem;
    border-radius: 10px;
    width: 15rem;
  }
  a {
    font-family: s.$font-family;
    text-decoration: none;
    color: #677cb3;
  }

} // Fin de .add-transaction

creación de un reactive form

app.modute.ts

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [ ... ],
  imports: [BrowserModule, AppRoutingModule, ReactiveFormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Incluye la librería en el módulo

pages / add-transaction / add-transaction.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-add-transaction',
  templateUrl: './add-transaction.component.html',
  styleUrls: ['./add-transaction.component.scss'],
})
export class AddTransactionComponent implements OnInit {
  addTransactionForm!: FormGroup;
  ngOnInit() {
    this.addTransactionForm = new FormGroup({
      amount: new FormControl(0),
      type: new FormControl('expense'),
      category: new FormControl('food'),
      date: new FormControl(''),
    });
  }
}

Componentes para el formulario

ngOnInit

Creación del FormGroup

Declaración de la propiedad FormGroup

markup del reactive form

<div class="add-transaction">
  <h2>Add transaction</h2>
  <form
    class="add-transaction__form"
    autocomplete="off"
    [formGroup]="addTransactionForm">
    <input
      type="text"
      id="amount"
      class="add-transaction__amount"
      formControlName="amount"
    />
    <div class="add-transaction__type">
      <input
        type="radio"
        name="type"
        value="expense"
        formControlName="type"
      /><label for="expense">Expense</label>
      <input
        type="radio"
        name="type"
        value="income"
        formControlName="type"
      /><label for="income">Income</label>
    </div>
    <select id="category" formControlName="category">
      <option value="food">Food</option>
      <option value="entertainment">Entertainment</option>
      <option value="payroll">Payroll</option>
      <option value="shopping">Shopping</option>
    </select>
    <input type="date" id="date" formControlName="date" />
    <button type="submit">Save</button>
  </form>
</div>

Enlaza el formulario con la propiedad declarada en la lógica

pages / add-transaction / add-transaction.component.html

Enlaza las propiedades del objeto FormGroup

formControlName

onsubmit del formulario

<div class="add-transaction">
  <h2>Add transaction</h2>
  <form
    class="add-transaction__form"
    autocomplete="off"
    [formGroup]="addTransactionForm"
    (ngSubmit)="onSubmit()">
    ...
  </form>
</div>

Escucha el evento ngSubmit

pages / add-transaction / add-transaction.component.html

export class AddTransactionComponent implements OnInit {
  addTransactionForm!: FormGroup;
  ngOnInit() {
    this.addTransactionForm = new FormGroup({
      amount: new FormControl(0),
      type: new FormControl('expense'),
      category: new FormControl('food'),
      date: new FormControl(''),
    });
  }
  onSubmit() {
    console.log(this.addTransactionForm);
  }
}

Ejecuta el método asociado al evento ngSubmit

pages / add-transaction / add-transaction.component.ts

El objeto addTransactionForm tiene propiedades con los datos del formulario

validación del formulario

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-add-transaction',
  templateUrl: './add-transaction.component.html',
  styleUrls: ['./add-transaction.component.scss'],
})
export class AddTransactionComponent implements OnInit {
  addTransactionForm!: FormGroup;
  ngOnInit() {
    this.addTransactionForm = new FormGroup({
      amount: new FormControl(null, [
        Validators.required,
        Validators.pattern('^[0-9]+$'),
      ]),
      type: new FormControl('expense', Validators.required),
      category: new FormControl('food', Validators.required),
      date: new FormControl('', Validators.required),
    });
  }
  onSubmit() {
    console.log(this.addTransactionForm);
  }
}

pages / add-transaction / add-transaction.component.ts

Establece las validaciones de cada campo

<input _ngcontent-ng-c2448798775="" type="text" id="amount" formcontrolname="amount" class="add-transaction__amount ng-untouched ng-pristine ng-valid" ng-reflect-name="amount">

ng-valid

ng-untouched

ng-pristine

Clases dinámicas para validación

mostrar mensajes de error

 <form
    class="add-transaction__form"
    autocomplete="off"
    [formGroup]="addTransactionForm"
    (ngSubmit)="onSubmit()">
    <div style="border: 1px solid red">
      {{ addTransactionForm.get("amount")?.errors | json }}
    </div>
    <input
      type="text"
      id="amount"
      class="add-transaction__amount"
      formControlName="amount"
    />
    @if(addTransactionForm.get("amount")?.invalid && addTransactionForm.get("amount")?.touched ) {

      @if(addTransactionForm.get("amount")?.errors?.["required"]) {
        <div class="add-transaction__error-message">Amount required</div>
      }
      @if(addTransactionForm.get("amount")?.errors?.["pattern"]) {
        <div class="add-transaction__error-message">Valid number required</div>
      }
   
    }
   ...
</form>

pages / add-transaction / add-transaction.component.html

Todos los errores de un campo

.add-transaction {
  input.ng-invalid.ng-touched {
    border: 1px solid #ef66d6;
  }
  &__error-message {
    font-family: s.$font-family;
    font-style: italic;
    font-size: 0.8rem;
    color: #ef66d6;
  }
}

pages / add-transaction / add-transaction.component.scss

touched

El usuario ya interactuó con el componente

invalid

El valor digitado no es válido

tipos de error

obtener un valor numérico del formulario

<input
  type="number"
  id="amount"
  class="add-transaction__amount"
  formControlName="amount"
  autofocus
/>

pages / add-transaction / add-transaction.component.html

Se cambia text por number

.add-transaction {
  /* Chrome, Safari, Edge, Opera */
  input::-webkit-outer-spin-button,
  input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }
  /* Firefox */
  input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
  }
}

pages / add-transaction / add-transaction.component.scss

export class AddTransactionComponent implements OnInit {
  addTransactionForm!: FormGroup;
  onSubmit() {
    console.log(this.addTransactionForm.value);
  }
}

pages / add-transaction / add-transaction.component.ts

Si el input es de tipo text, se obtendrá un valor de tipo texto y no uno numérico

Oculta las flechas del input de tipo number

services

Dependency Injection y Services

services

services + dependency injection

Dependency Injection - DI

DI permite declarar las dependencias de las clases sin ocuparse de la creación de instancias

Patrón de diseño que permite escribir código más flexible y menos dependiente de otras clases

Services

Un servicio es una categoría amplia que abarca cualquier valor, función o característica que necesita una aplicación

Un servicio es típicamente una clase con un propósito bien definido que puede ser usada por un componente utilizando DI

creación del servicio - función GET

Angular CLI

ng g s services/transactions
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class TransactionsService {

  constructor() { }
}

services / transactions.service.ts

Servicio "inyectable"

Visible en toda la aplicación

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Transaction } from '../models/transaction.model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class TransactionsService {
  constructor(private httpClient: HttpClient) {}

  public get(): Observable<Transaction[]> {
    return this.httpClient.get<Transaction[]>(
      'http://localhost:3000/transactions'
    );
  }
}

services / transactions.service.ts

DI

Observable

El patrón Observer/Observable es un patrón de software que permite monitorear cambios en objetos

uso del servicio en el componente: GET

import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Transaction } from '../../models/transaction.model';
import { TransactionsService } from 'src/app/services/transactions.service';

@Component({
  selector: 'app-transactions',
  templateUrl: './transactions.component.html',
  styleUrls: ['./transactions.component.scss'],
})
export class TransactionsComponent implements OnInit {
  transactions!: Transaction[];

  constructor(private transactionsService: TransactionsService) {}

  ngOnInit(): void {
    this.transactionsService.get().subscribe((response: Transaction[]) => {
      this.transactions = response;
    });
  }
}

components / home / home.component.ts

DI

Uso del servicio

<div class="transactions">
  <div *ngIf="transactions; then noEmpty; else empty">Contenido ignorado</div>
  <ng-template #noEmpty>
    <app-transaction
      *ngFor="let transaction of transactions"
      [transaction]="transaction"
      (removeTransacionEvent)="removeTransaction($event)"
    />
</div>

components / transactions / transactions.component.ts

uso del servicio en el componente: create

import { Transaction } from 'src/app/models/transaction.model';
import { TransactionsService } from 'src/app/services/transactions.service';
import { Router } from '@angular/router';

export class AddTransactionComponent implements OnInit {
  addTransactionForm!: FormGroup;
  constructor(
    private transactionsService: TransactionsService,
    private router: Router
  ) {}
  ngOnInit() { ... }
  onSubmit() {
    if (this.addTransactionForm.valid) {
      const transaction: Transaction = this.addTransactionForm.value;
      this.transactionsService
        .create(transaction)
        .subscribe((response: Transaction) => {
          this.router.navigate(['']);
        });
    }
  }
}

components / add-transaction / add-transaction.component.ts

DI

Uso del servicio

@Injectable({
  providedIn: 'root',
})
export class TransactionsService {
  constructor(private httpClient: HttpClient) {}
  public create(transaction: Transaction): Observable<Transaction> {
    return this.httpClient.post<Transaction>(
      'http://localhost:3000/transactions',
      transaction
    );
  }
}

services / transactions.service.ts

Redirecciona al Home

uso del servicio en el componente: remove

export class TransactionsComponent implements OnInit {
  removeTransaction(transactionId: string) {
    this.transactionsService
      .remove(transactionId)
      .subscribe((response: Transaction) => {
        console.log(response);
        this.transactions = this.transactions.filter(
          (t) => t.id !== transactionId
        );
      });
  }
}

components / home / home.component.ts

Uso del servicio

@Injectable({
  providedIn: 'root',
})
export class TransactionsService {
  constructor(private httpClient: HttpClient) {}
  public remove(id: string): Observable<Transaction> {
    return this.httpClient.delete<Transaction>(
      `http://localhost:3000/transactions/${id}`
    );
  }
}

services / transactions.service.ts

transactions service completo

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

// Models
import { Transaction } from '../models/transaction.model';

@Injectable({
  providedIn: 'root',
})
export class TransactionsService {
  url: string = 'http://localhost:3000/transactions';

  // Construye un httpClient usando Dependency Injection
  constructor(private httpClient: HttpClient) {}

  public get(): Observable<Transaction[]> {
    return this.httpClient.get<Transaction[]>(this.url);
  }

  public create(transaction: Transaction): Observable<Transaction> {
    return this.httpClient.post<Transaction>(this.url, transaction);
  }

  public remove(id: string): Observable<Transaction> {
    return this.httpClient.delete<Transaction>(`${this.url}/${id}`);
  }
}

services / transactions.service.ts

lazy loading

Módulos y Carga por demanda

lazy loading

lazy loading + modulos

Permite cargar recursos de una ruta sólo cuando el usuario navega a la ruta

Lazy Loading

Módulo

Rutas

Ruta 1

Ruta 2

Ruta 3

Cuando se accede a alguna ruta del módulo se cargan los recursos asociados

Angular CLI

ng g m auth --routing

Creación de un módulo con routing

auth-routing.module.ts

auth.module.ts

Módulo de rutas

Módulo principal

Parámetro que agrega routing al módulo creado

módulo principal + routing

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AuthRoutingModule { }

app / auth / auth-routing.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { AuthRoutingModule } from './auth-routing.module';


@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    AuthRoutingModule
  ]
})
export class AuthModule { }

app / auth / auth.module.ts

Módulo Principal

Módulo de routing

Definición de rutas del módulo

Agregar componentes al módulo

Las páginas en Angular son simples componentes que agrupan otros componentes

Angular CLI

ng g c auth/pages/signin
ng g c auth/pages/signup
ng g c auth/pages/forgot

Los componentes se agregan automáticamente al módulo correspondiente dada su ruta

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { AuthRoutingModule } from './auth-routing.module';
import { SigninComponent } from './pages/signin/signin.component';
import { SignupComponent } from './pages/signup/signup.component';
import { ForgotComponent } from './pages/forgot/forgot.component';

@NgModule({
  declarations: [SigninComponent, SignupComponent, ForgotComponent],
  imports: [CommonModule, AuthRoutingModule],
})
export class AuthModule {}

app / auth / auth.module.ts

Creación de rutas para cada página

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SigninComponent } from './pages/signin/signin.component';
import { SignupComponent } from './pages/signup/signup.component';
import { ForgotComponent } from './pages/forgot/forgot.component';

const routes: Routes = [
  { path: 'signin', component: SigninComponent },
  { path: 'signup', component: SignupComponent },
  { path: 'forgot', component: ForgotComponent },
  { path: '**', redirectTo: '/' },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AuthRoutingModule {}

app / auth / auth-routing.module.ts

Ruta para cada página (componente)

Redirecciona al Home si se accedió a alguna ruta no definida

configuración de lazy loading para el módulo

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HomeComponent } from './components/home/home.component';
import { AboutComponent } from './components/about/about.component';
import { AddTransactionComponent } from './components/add-transaction/add-transaction.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'add', component: AddTransactionComponent },
  {
    path: 'auth',
    loadChildren: () =>
      import('./auth/auth.module').then((module) => module.AuthModule),
  },
];

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

app-routing.module.ts

Se carga dinámicamente el módulo "auth" desde el módulo principal "app"

Configuración de Lazy Loading

verificación de lazy loading - I

Como ejemplo, se agregan rutas a about.component.html

<ul>
  <li><a routerLink="/auth/signin">Sign in</a></li>
  <li><a routerLink="/auth/signup">Sign up</a></li>
  <li><a routerLink="/auth/forgot">Forgot</a></li>
</ul>

components / about / about.component.html

El prefijo "auth" fue definido en la ruta del Lazy Loading

Al navegar a cada ruta se cargan los recursos de la ruta

Se puede comprobar la carga en la pestaña "Network" del DevTools del browser

verificación de lazy loading - II

Ruta

Recursos cargados sólo cuando el usuario accede a la ruta

Tamaño de la transferencia

Tiempo de transferencia

Recursos cargados sólo cuando el usuario accede a la ruta

unit testing

Jasmine + Karma

unit testing

unit testing con karma

Al crear cada componente se crea un archivo con extensión "spec.ts"

"spec.ts" - Archivo de testing

Ejecución de pruebas

ng test

Ejecuta los tests de todos los archivos "spec.ts" de la aplicación

TransactionsService

HttpClient

HomeComponent

TransactionComponent

En la primera ejecución genera errores por DI

!

testing de HTML de balance component

Pruebas sólo para el componente BalanceComponent

ng test --include='src/app/components/balance'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BalanceComponent } from './balance.component';
describe('BalanceComponent', () => {
  let component: BalanceComponent;
  let fixture: ComponentFixture<BalanceComponent>;
  let compiled: HTMLElement;
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [BalanceComponent],
    });
    fixture = TestBed.createComponent(BalanceComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    compiled = fixture.nativeElement;
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  it('should have the right amount, expenses and income', () => {
    component.balance = {
      amount: 30,
      expenses: 15,
      income: 45,
    };
    fixture.detectChanges();
    const amountElement = compiled.querySelector('.balance__header h2');
    expect(amountElement?.textContent).toBe('$30.00');
    const incomeElement = compiled.querySelector('.balance__info div:nth-child(1) h3');
    expect(incomeElement?.textContent).toBe('$45.00');
    const expensesElement = compiled.querySelector('.balance__info div:nth-child(2) h3');
    expect(expensesElement?.textContent).toBe('$15.00');
  });
});

components / balance / balance.component.spec.ts

HTML compilado

Monta el componente

Prueba las condiciones esperadas

Prueba de componente creado

Datos del componente

TestBed

objeto que permite probar el comportamiento del componente

fixture

wrapper para el componente y su template

testing de html de transactions component

Pruebas sólo para el componente TransactionsComponent

ng test --include='src/app/components/transactions'
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TransactionsComponent } from './transactions.component';
import { TransactionComponent } from '../transaction/transaction.component';
describe('TransactionsComponent', () => {
  let component: TransactionsComponent;
  let fixture: ComponentFixture<TransactionsComponent>;
  let compiled: HTMLElement;
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ TransactionsComponent, TransactionComponent, RouterTestingModule, ],
      providers: [
        {
          provide: ActivatedRoute,
          useValue: {},
        },
      ],
    }).compileComponents();
    fixture = TestBed.createComponent(TransactionsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    compiled = fixture.nativeElement;
  });
  it('should display all transactions', () => {
    const dummyTransactions = [
      { id: '1', type: 'income', amount: 200, category: 'payroll', date: new Date(), },
      { id: '2', type: 'expense', amount: 50, category: 'food', date: new Date(), },
      { id: '3', type: 'expense', amount: 100, category: 'entertainment', date: new Date(), },
    ];
    component.transactions = dummyTransactions;
    fixture.detectChanges();
    const appTransactionElements = compiled.getElementsByTagName('app-transaction');
    expect(appTransactionElements.length).toBe(dummyTransactions.length);
    const icons = compiled.querySelectorAll('app-transaction .transaction .transaction__icon');
    expect(icons[0]).toHaveClass('transaction__icon-income');
    expect(icons[1]).toHaveClass('transaction__icon-expense');
    expect(icons[2]).toHaveClass('transaction__icon-expense');
  });
});

components / transactions / transactions.component.spec.ts

HTML compilado

Monta el componente

Prueba de CSS

Parámetros Dummy

Prueba de datos

Dependencias

solucionar problema del test del service

Ejecución de pruebas sólo para el folder "services"

ng test --include='src/app/services'
import { TestBed } from '@angular/core/testing';

import { TransactionsService } from './transactions.service';
import { HttpClientModule } from '@angular/common/http';

describe('TransactionsService', () => {
  let service: TransactionsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
    });
    service = TestBed.inject(TransactionsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

services / transactions.service.spec.ts

Se incluye la dependencia HttpClientModule

La prueba de creación del componente es exitosa

expect: valor esperado

adicionar un test al servicio

import { TestBed } from '@angular/core/testing';

import { TransactionsService } from './transactions.service';
import { HttpClientModule } from '@angular/common/http';
import { Transaction } from '../models/transaction.model';

describe('TransactionsService', () => {
  let service: TransactionsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
    });
    service = TestBed.inject(TransactionsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should get 5 transactions', (done) => {
    service.get().subscribe((transactions: Transaction[]) => {
      expect(transactions.length).toBe(5);
      done();
    });
  });
});

services / transactions.service.spec.ts

Creación de una prueba que obtiene valores del backend

Si no se usa done, se genera un WARN que dice que no se ejecutó la prueba

Sólo en operaciones asíncronas

Si no se usa done, se genera un WARN que dice que no se ejecutó la prueba

Sólo en operaciones asíncronas

Verifica que la cantidad de transacciones sea 5

adicionar otro test al servicio

describe('TransactionsService', () => {
  let service: TransactionsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
    });
    service = TestBed.inject(TransactionsService);
  });
  
  it('should get the name of a transaction', (done) => {
    service.get().subscribe((transactions: Transaction[]) => {
      const transaction: Transaction[] = transactions.filter(
        (t) => t.id === '1'
      );
      expect(transaction[0].category).toBe('payroll');
      done();
    });
  });
});

services / transactions.service.spec.ts

Prueba que verifica la categoría de una transacción

Reporte de la ejecución de pruebas en el Browser

El browser es controlado por el software de automatización de pruebas

agrega dependencias de home component

Ejecución de pruebas sólo para HomeComponent

ng test --include='src/app/components/home'

HomeComponent

BalanceComponent

TransactionsComponent

utiliza

depende de

TransactionService

HttpClient

depende de

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { BalanceComponent } from '../../components/balance/balance.component';
import { TransactionsComponent } from '../../components/transactions/transactions.component';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TransactionsService } from '../../services/transactions.service';
import { HttpClientModule } from '@angular/common/http';
describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let compiled: HTMLElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        HomeComponent,
        BalanceComponent,
        TransactionsComponent,
        HttpClientModule,
        RouterTestingModule,
      ],
      providers: [
        TransactionsService,
        {
          provide: ActivatedRoute,
          useValue: {},
        },
      ],
    }).compileComponents();
  });
});

components / home / home.component.spec.ts

services

routing

components

imports

providers

crear un test para home component

describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let compiled: HTMLElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HomeComponent, BalanceComponent, TransactionsComponent],
      imports: [HttpClientTestingModule],
      providers: [TransactionsService],
    });
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    compiled = fixture.nativeElement;
  });

  it('should have a main div', () => {
    const div = compiled.getElementsByClassName('main');
    expect(div).toBeTruthy();
  });
});

components / home / home.component.spec.ts

Crea una prueba basada en el template del componente

<div class="main">
  <app-balance [balance]="balance" />
  <app-transactions
    [transactions]="transactions"
    (removeTransacionEvent)="removeTransaction($event)"
  />
</div>

components / home / home.component.html

test de una función en home component

describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let compiled: HTMLElement;
  let service: TransactionsService;
  let httpMock: HttpTestingController;
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HomeComponent, BalanceComponent, TransactionsComponent],
      imports: [HttpClientTestingModule],
      providers: [TransactionsService],
    });
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    service = TestBed.inject(TransactionsService);
    httpMock = TestBed.inject(HttpTestingController);
    fixture.detectChanges();
    compiled = fixture.nativeElement;
  });
  it('should calculate the balance', () => {
    const dummyTransactions: Transaction[] = [
      { id: '1', type: 'income', amount: 100, category: 'food', date: new Date(), },
      { id: '2', type: 'expense', amount: 25, category: 'food', date: new Date(), },
      { id: '3', type: 'expense', amount: 10, category: 'entertainment', date: new Date(), },
    ];
    component.transactions = dummyTransactions;
    component.calculateBalance();
    expect(component.balance.income).toBe(300);
    expect(component.balance.expenses).toBe(35);
    expect(component.balance.amount).toBe(265);
  });
});

components / home / home.component.spec.ts

Datos de prueba

Ejecución de la función

Validación

principales lifecycle hooks

constructor

ngInit

ngDestroy

Constructor de Typescript

Es ejecutado cuando el componente es agregado al DOM

Es ejecutado cuando el componente es eliminado del DOM

Es ejecutado cuando el componente es creado

OnInit

OnDestroy

ngChanges

Es ejecutado cada vez que cambia el valor de un valor @Input

OnChanges

ngDoCheck

Es ejecutado antes de cada vez que Angular verifica un cambio en el template del componente.

DoCheck

Interfaces

introducción a microfrontend

introducción a microfrontend

Arquitectura

arquitectura microfrontend

Divide estructuras monolíticas en componentes más pequeños que pueden ser ensamblados en una sola página o aplicación

Unidades de ejecución

Componentes

Páginas

Aplicaciones

dominio 1

dominio 2

dominio 3

Aplicación Host

Arquitectura del lado cliente

2016

ThoughtWorks technology Radar

pros y cons de microfrontends

Arquitectura modular y reutilizable

Escalabilidad

Mantenimiento

Desarrollo independiente y más rápido

Diferentes tecnologías para diferentes proyectos

Más difícil testing de toda la aplicación

Compartir código, estado y datos no es fácil

Trabajo adicional de Devops para deploy

retos de microfrontends

Desafío mayor: comunicación entre aplicaciones

La capa de datos de cada micro aplicación debe separarse en una capa de datos compartibles

Estado

Utilidades

Lógica de negocio

LocalStorage

SessionStorage

Cookies

QueryParams

Comunicación común entre aplicaciones

variables de ambiente

Inter-app communication

arquitecturas - i

Monolith

MPA

FE

BE

dev, test, deploy

(1) ASP.NET, JEE, PHP, Django

1 git

arquitecturas - iI

Monolith

MPA

FE

BE

Microservices

SPA

FE

App

BE

Auth

Products

Shopping Cart

Reviews

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

(1) ASP.NET, JEE, PHP, Django

(1) Angular, React, Vue

(*) Node, Python, Java, C#

(*)

1 git

5 git

arquitecturas - iII

Monolith

MPA

FE

BE

Microservices

SPA

FE

App

BE

Auth

Products

Shopping Cart

Reviews

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

(1) ASP.NET, JEE, PHP, Django

(1) Angular, React, Vue

(*) Node, Python, Java, C#

Microservices + Microfrontend

FE

Header

BE

Auth

Products

Shopping Cart

Reviews

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

(*) Angular, React, Vue

(*) Node, Python, Java, C#

Products

Reviews

Shopping Cart

dev, test, deploy

dev, test, deploy

dev, test, deploy

dev, test, deploy

(*)

(*)

1 git

5 git

8 git

Monorepo

frameworks para microfrontend

Bit

Webpack 5 - Module Federation

Single SPA

SystemJS

Piral

Open Components

Qiankun

Luigi

FrintJS

PuzzleJS

single-spa

single-spa es un framework que permite utilizar en conjunto múltiples microfrontends Javascript en una sola aplicación web

Características

Múltiples frameworks en la misma página

Despliegue independiente de cada microfrontend

Lazy code para mejora de carga inicial

React

Vue

Angular

AngularJS

Svelte

Ember

Backbone

AlpineJS

Dojo

Estructura de la aplicación

Contenedor Root

MF 1

MF 2

MF n

...

index

aplicación de ejemplo

header

vue

root

single-spa

product

react

reviews

angular

creación de la aplicación root

Creación del contenedor- parámetros

npx create-single-spa --moduleType root-config

Creación del contenedor- wizard

npx create-single-spa

Directorio del proyecto

Tipo del proyecto

.

single-spa root config

Package manager

npm

Typescript

yes

single-spa Layout Engine (SSR)

yes

Nombre de la organización

p.e. ecommerce

Ejecución

npm start

chrome extension: single-spa inspector

Instalar la extensión

single-spa inspector

Aplicaciones instaladas en el root

Aplicaciones instaladas en el root

Welcome: app de ejemplo

punto de entrada de la aplicación

<single-spa-router>
  <main>
    <route default>
      <application name="@single-spa/welcome"></application>
    </route>
  </main>
</single-spa-router>

microfrontend-layout.html

Aplicación a cargar

Nombre del equipo

Nombre de la aplicación

<% if (isLocal) { %>
  <script type="systemjs-importmap">
  {
    "imports": {
      "@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js",
      "@ecommerce/root-config": "//localhost:9000/news-portal-root-config.js"
    }
  }
</script>

index.ejs

Embedded Javascript Templating

Registro de la aplicación

Aplicación de ejemplo ya compilada

No sé sabe en qué lenguaje está desarrollada

Se puede eliminar dado que no es necesaria

crear micro aplicación mf-product

Creación del contenedor- wizard

npx create-single-spa

Directorio del proyecto

Tipo del proyecto

mf-product

single-spa application

Package manager

npm

Typescript

yes

single-spa Layout Engine (SSR)

yes

Nombre de la organización

team-product

Nombre del proyecto

mf-product

framework

react

ejecución de aplicación mf-product

Ejecución

cd mf-product
npm install
npm start

Identificador

registro de mf-Product -react

index.ejs

<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "react": "https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js",
        "react-dom": "https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js",
        "@news-portal/root-config": "//localhost:9000/news-portal-root-config.js",
        "@team-product/mf-product": "//localhost:8080/team-product-mf-product.js"
      }
    }
  </script>
<% } %>

identificador de la aplicación : ruta compilada de la aplicación

<single-spa-router>
  <main>
    <route default>
      <application name="@team-product/mf-product"></application>
    </route>
  </main>
</single-spa-router>

microfrontend-layout.html

Incluye la aplicación en el HTML del root

Librerías de React

CDNJS.com

Uso de la librerías de React y registro de la aplicación

creación de micro aplicación mf-reviews

Creación del contenedor- wizard

npx create-single-spa

Directorio del proyecto

Tipo del proyecto

mf-reviews

single-spa application

single-spa-angular

yes

Nombre del proyecto

mf-reviews

framework

angular

Angular routing

no

Stylesheets

SCSS

plugin de single-spa

Ejecución

cd mf-reviews/mf-reviews
npm install
npm start

Ruta de la app compilada

registro de mf-reviews - angular

<script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script>

index.ejs

Uso de la librería Zone y registro de la aplicación

<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@news-portal/root-config": "//localhost:9000/news-portal-root-config.js",
        "@mf-reviews/mf-reviews": "//localhost:4200/main.js"
      }
    }
  </script>
<% } %>

identificador de la aplicación : ruta compilada de la aplicación

<single-spa-router>
  <main>
    <route default>
      <application name="@mf-reviews/mf-reviews"></application>
    </route>
  </main>
</single-spa-router>

microfrontend-layout.html

Incluye la aplicación en el HTML del root

creación de micro aplicación mf-header

Creación del contenedor- wizard

npx create-single-spa

Directorio del proyecto

Tipo del proyecto

mf-header

single-spa application

framework

vue

Nombre de la organización

team-header

Package manager

NPM

Vue CLI

Vue 3

ejecución de micro aplicación mf-header

Ejecución

cd mf-header
npm install
npm run serve

Identificador

registro de mf-header - vue

  <% if (!isLocal) { %>
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
  <% } %>

index.ejs

Uso condicional de meta + importar la aplicación compilada

<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@news-portal/root-config": "//localhost:9000/news-portal-root-config.js",
        "@team-header/mf-header": "//localhost:8081/js/app.js"
      }
    }
  </script>
  <% } %>

identificador de la aplicación : ruta compilada de la aplicación

<single-spa-router>
  <main>
    <route default>
      <application name="@team-header/mf-header"></application>
    </route>
  </main>
</single-spa-router>

microfrontend-layout.html

Incluye la aplicación en el HTML del root

const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  configureWebpack: {
    output: {
      libraryTarget: "system",
    },
  },
  transpileDependencies: true,
});

vue.config.js

Configuración de módulos ES6

Reboot de la aplicación

mf-header - vue

<template>
  <HeaderComponent text="Atenea Ecommerce" />
</template>

<script>
import HeaderComponent from "./components/HeaderComponent.vue";

export default {
  name: "App",
  components: {
    HeaderComponent,
  },
};
</script>

App.vue

<template>
  <header>
    <h2>{{ text }}</h2>
    <ul>
      <li><a href="#">Home</a></li>
      <li><a href="#">Deals</a></li>
      <li><a href="#">Cart</a></li>
    </ul>
  </header>
</template>
<script>
export default {
  name: "HeaderComponent",
  props: {
    text: String,
  },
};
</script>
<style scoped>
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
header {
  padding: 10px 40px;
  background-color: #282877;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
header h2 {
  font-family: "Montserrat", sans-serif;
  color: #fff;
  margin: 0;
  padding: 0;
  font-weight: 400;
}
ul {
  list-style-type: none;
  display: flex;
  gap: 20px;
}
ul a {
  font-family: "Montserrat", sans-serif;
  color: #fff;
  text-decoration: none;
}
</style>

components / HeaderComponent.vue

mf-product - react

import Product from "./components/Product";

export default function Root() {
  return <Product />;
}

root.component.tsx

import "./Product.css";

const Product = () => {
  return (
    <div className="product">
      <div className="product__image">
        <img
          src="https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80"
          alt="PS5"
        />
      </div>
      <div className="product__description">
        <h2>PlayStation 5 Console</h2>
        <p>
          Lorem ipsum dolor, sit amet consectetur adipisicing elit. Sequi
          eligendi culpa, perspiciatis distinctio dolores similique nemo earum
          aperiam enim sunt aliquid consequuntur numquam est laudantium delectus
          quasi debitis! Doloribus, saepe.
        </p>
        <h4>$400.00</h4>
      </div>
    </div>
  );
};

export default Product;

components / Product.tsx

@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
.product {
  display: flex;
  max-width: 70%;
  margin: auto;
  border-bottom: 1px solid #d6d6d6;
}
.product__image {
  padding: 20px;
}
.product__image img {
  width: 250px;
}
.product__description {
  padding: 20px;
}
.product__description h2 {
  font-family: "Montserrat", sans-serif;
  color: #35356a;
}
.product__description p {
  font-family: "Montserrat", sans-serif;
  font-size: 0.9rem;
  color: #3a3a4e;
}
.product__description h4 {
  font-family: "Montserrat", sans-serif;
  color: #35356a;
}

components / Product.css

mf-reviews - angular - i

import { Component, Input } from '@angular/core';
import { Review } from '../../models/review';

@Component({
  selector: 'app-review',
  templateUrl: './review.component.html',
  styleUrls: ['./review.component.scss'],
})
export class ReviewComponent {
  @Input() review!: Review;
}

components / review / review.component.ts

@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
.review {
  display: flex;
  font-family: "Montserrat", sans-serif;
  border-bottom: 1px solid #cccccc;
  margin-bottom: 10px;
  img {
    margin-top: 14px;
    border-radius: 50%;
  }
  &__info {
    padding: 10px;
    h5,
    h6,
    p {
      margin: 0;
    }
    h5 {
      color: #2f2f77;
    }
    h6 {
      color: #959595;
      font-weight: 400;
      margin-bottom: 10px;
    }
    p {
      color: #757575;
      font-size: 0.7rem;
      line-height: 18px;
    }
  }
}

components / review / review.component.scss

<div class="review">
  <div class="review__avatar">
    <img src="{{ 'https://i.pravatar.cc/40?img=' + review.id }}" alt="" />
  </div>
  <div class="review__info">
    <h5>{{ review.username }}</h5>
    <h6>{{ review.date | date }}</h6>
    <p>{{ review.body }}</p>
  </div>
</div>

components / review / review.component.html

mf-reviews - angular - iI

import { Component } from '@angular/core';
import { Review } from '../../models/review';
@Component({
  selector: 'app-reviews',
  templateUrl: './reviews.component.html',
  styleUrls: ['./reviews.component.scss'],
})
export class ReviewsComponent {
  reviews: Review[] = [
    {
      id: '1',
      username: 'Diana Prince',
      date: new Date(),
      body: 'Lorem ipsum dolor sit amet',
    },
    {
      id: '2',
      username: 'Clark Kent',
      date: new Date(),
      body: 'Lorem ipsum dolor sit amet',
    },
    {
      id: '3',
      username: 'Bruce Wayne',
      date: new Date(),
      body: 'Lorem ipsum dolor sit amet',
    },
  ];
}

components / reviews / reviews.component.ts

@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");

.reviews {
  max-width: 70%;
  margin: auto;
}

.reviews > h3 {
  font-family: "Montserrat", "Helvetica Neue";
  color: #35356a;
}

components / reviews / reviews.component.scss

<div class="reviews">
  <h3>Reviews</h3>
  <app-review *ngFor="let review of reviews" [review]="review" />
</div>

components / reviews / reviews.component.html

mf-reviews - angular - iII

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

import { AppComponent } from './app.component';
import { ReviewsComponent } from './components/reviews/reviews.component';
import { ReviewComponent } from './components/review/review.component';

@NgModule({
  declarations: [AppComponent, ReviewsComponent, ReviewComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.module.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'mf-reviews';
}

app.component.ts

<app-reviews />

app.component.html

john cardozo

johncardozo@gmail.com

Angular

By John Cardozo

Angular

  • 123