John Cardozo
John Cardozo
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
...
...
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
State of JS
Stackoverflow Insights 2023
Who Uses Angular in 2023? 12 Global Websites Built With Angular
Instalación
Verificación
node --version
npm --version
Node Package Manager
Node
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
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
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
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
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
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>
<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!
Decorador @Input
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
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
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
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
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>
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
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
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
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>
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"
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
ngStyle + ngclass - 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;
}
}
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
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"
Event listeners + Directiva @Output
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
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
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
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>
Angular Router
Página 1
Página 2
Página 3
Componentes
App
Navegación entre diferentes páginas
Angular Router
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())],
};
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
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
) {}
}
Reactive Forms
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
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
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
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
<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
<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
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
<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
<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
Dependency Injection y Services
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
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
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
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
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
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
Módulos y Carga por demanda
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
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
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
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
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
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
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
Jasmine + 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
!
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
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
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
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
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
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
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
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
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
Arquitectura
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
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
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
Monolith
MPA
FE
BE
dev, test, deploy
(1) ASP.NET, JEE, PHP, Django
1 git
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
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
Bit
Webpack 5 - Module Federation
Single SPA
SystemJS
Piral
Open Components
Qiankun
Luigi
FrintJS
PuzzleJS
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
header
vue
root
single-spa
product
react
reviews
angular
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
Instalar la extensión
single-spa inspector
Aplicaciones instaladas en el root
Aplicaciones instaladas en el root
Welcome: app de ejemplo
<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
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
cd mf-product
npm install
npm start
Identificador
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 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
<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 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
cd mf-header
npm install
npm run serve
Identificador
<% 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
<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
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
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
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
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
johncardozo@gmail.com