Angular 9
Introducción a Angular
¿Qué es Angular?
-
Framework JS
-
SPA: Single Page Applications
-
Data binding
-
TypeScript
-
Código fuente y código compilado
-
¿Angular 2? ¿4? ¿9? ¿AngularJS?
Entorno de desarrollo
Entorno de desarrollo
-
Editor: Visual Studio Code
- TSLint
- Angular Language Service
- Live Share
-
Configurar:
-
Format on Paste y Format on Save
-
"editor.codeActionsOnSave": {
"source.fixAll.tslint": true
}
-
-
Node.js y npm
-
Extensión Augury para el navegador
Git
Comandos básicos
-
Clonar un repositorio:
git clone URL
-
Descargar última versión del repositorio:
git pull origin master
Configuración proxy
git config --global http.proxy http://username:password@host:port
git config --global https.proxy http://username:password@host:port
Node.js y npm
npm
-
Instalar última versión después de instalar Node.js
(configurar proxy si es necesario): npm install -g npm -
Repositorio de paquetes distribuibles
-
Paquetes globales y paquetes locales
-
La carpeta node_modules
-
El archivo package.json:
-
Registro de dependencias
-
Dependencias de desarrollo y de producción
-
Comandos npm
-
Instalar un paquete globalmente:
npm install -g paquete -
Instalar un paquete de producción:
npm install paquete -
Instalar un paquete de desarrollo:
npm install paquete --save-dev -
Instalar todas las dependencias:
npm install -
Instalar las dependencias de producción:
npm install --production -
Listar paquetes instalados:
npm list --depth=0 (locales)
npm list -g --depth=0 (globales)
Comandos npm
-
Lanzar el ejecutable de un paquete:
npx ejecutable
angular-cli
-
Instalación global:
npm install -g @angular/cli
Configuración proxy
npm config set proxy http://username:password@host:port
npm config set https-proxy http://username:password@host:port
JavaScript
JavaScript
-
Interpretado, compilado y ejecutado en el navegador
-
Cada navegador programa su propio motor de JS
-
Estandarización: ECMAScript
-
La versión ES6 o ES2015
-
Transpiladores: Babel, TypeScript
Organización del código JavaScript
-
¿2000 líneas en un solo archivo?
Ventajas
Inconvenientes
- Difícil de leer/entender
- Difícil de mantener
- Poca reusabilidad
- Difícil encontrar código no usado
- Colisiones de nombres
- Una sola petición HTTP
Organización del código JavaScript
-
Optimización: dividir el código en varios archivos/módulos
<head>
<meta charset="UTF-8">
<title>Mi web</title>
<script src="vendor/jquery/jquery.min.js"></script>
<script src="js/modules/tabs.js"></script>
<script src="js/modules/banners.js"></script>
<script src="js/modules/lightbox.js"></script>
<script src="js/modules/scroll.js"></script>
<script src="js/modules/carousel.js"></script>
<script src="js/modules/slideshow.js"></script>
<script src="js/modules/gallery.js"></script>
<script src="js/modules/navigation.js"></script>
</head>
Organización del código JavaScript
Ventajas
Inconvenientes
- Difícil encontrar código no usado (menos difícil que antes)
- Colisiones de nombres
- Muchas peticiones HTTP
- El orden importa: dependencias
- Legible e inteligible
- Fácil de mantener
- Reutilizable
- Cargamos sólo lo que necesitamos
<head>
<meta charset="UTF-8">
<title>Mi web</title>
<script src="vendor/jquery/jquery.min.js"></script>
<script src="js/modules/tabs.js"></script>
<script src="js/modules/banners.js"></script>
<script src="js/modules/lightbox.js"></script>
<script src="js/modules/scroll.js"></script>
<script src="js/modules/carousel.js"></script>
<script src="js/modules/slideshow.js"></script>
<script src="js/modules/gallery.js"></script>
<script src="js/modules/navigation.js"></script>
</head>
Organización del código JavaScript
-
Dependencias: es difícil asegurar el orden, y no es posible tener dependencias circulares
<head>
<meta charset="UTF-8">
<title>Mi web</title>
<script src="vendor/jquery/jquery.min.js"></script>
<script src="js/modules/tabs.js"></script>
<script src="js/modules/banners.js"></script>
<script src="js/modules/lightbox.js"></script>
<script src="js/modules/scroll.js"></script>
<script src="js/modules/carousel.js"></script>
<script src="js/modules/slideshow.js"></script>
<script src="js/modules/gallery.js"></script>
<script src="js/modules/navigation.js"></script>
</head>
Organización del código JavaScript: módulos
-
Module loaders: ellos gestionan las dependencias y cargan los módulos (RequireJS, SystemJS)
Ventajas
Inconvenientes
- Difícil encontrar código no usado (menos difícil que antes)
- Muchas peticiones HTTP
- Legible e inteligible
- Fácil de mantener
- Reutilizable
- Cargamos sólo lo que necesitamos
- Gestión automática de dependencias
- Encapsulación
Organización del código JavaScript: módulos
-
Module bundlers: además de lo anterior, generan un solo código encadenado y minificado (Browserify, webpack, Parcel)
Ventajas
- Legible e inteligible
- Fácil de mantener
- Reutilizable
- Cargamos sólo lo que necesitamos
- Gestión automática de dependencias
- Encapsulación
- Una o muy pocas conexiones HTTP
- Eliminación de código no usado (tree shaking)
Organización del código JavaScript: módulos
-
¿Puedo escribir mis módulos como yo quiera? ¿hay un estándar?
-
AMD: Asynchronous Module Definition
-
CommonJS
-
UMD: Universal Module Definition
-
ES6 Modules
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
var myModuleA = require('myModuleA');
function myModuleB() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModuleB;
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));
import { method1 } from './moduleA.js';
method1("hello");
export let method2 = function() {
console.log("Method 2");
}
Organización del código JavaScript: módulos
-
¿AMD, CommonJS, UMD, ES6?
-
Compatibilidad de los módulos ES6 en navegadores
-
¡Webpack!
-
TypeScript usa la sintaxis ES6
-
TS -> ES5 -> webpack -> bundle -> browser =
Angular CLI
ES6
let y const
let a = 3;
let a = 10; // Error
var a = 12; // Error
const b = 10;
b = 3; // Error
const obj = {
x: 10,
y: 12
}
obj.x = 15; // OK
obj = { // Error
x: 15,
y: 12
}
ES6
-
let y const
-
Template literals
let nombre = "Antonio";
let cuadrado = function(x) {
return x * x;
}
let n = Math.floor(Math.random() * 10);
let saludo1 = "Hola, " + nombre + ". El cuadrado de " + n + " es " + cuadrado(n) + ".";
let saludo2 = `Hola, ${nombre}. El cuadrado de ${n} es ${cuadrado(n)}.`;
ES6
-
let y const
-
Template literals
-
for ... of
let nombres = ["Patricia", "Zacarías", "Miguel", "Maite"];
for (let i in nombres) {
console.log(nombres[i]);
}
for (let nombre of nombres) {
console.log(nombre);
}
let obj = {
x: 3,
y: 4
}
for (let i in obj) {
console.log(obj[i]);
}
let nombre = "Antonio Jesús";
for (let c of nombre) {
console.log(c);
}
ES6
-
let y const
-
Template literals
-
for ... of
-
Funciones
-
Parámetros por defecto
-
function potencia(x, y = 2) {
return Math.pow(x, y);
}
console.log(`10 elevado a 8 es ${potencia(10, 8)}`);
console.log(`El cuadrado de 5 es ${potencia(5)}`);
ES6
-
let y const
-
Template literals
-
for ... of
-
Funciones
-
Parámetros por defecto
-
Función arrow:
(parámetros) => expresión_devuelta;
-
const potencia = function (x, y = 2) {
return Math.pow(x, y);
}
const potencia = (x, y = 2) => Math.pow(x, y);
setTimeout(() => console.log("pausa"), 2000);
ES6
-
Operador spread
-
Parámetros en funciones
-
Enviar varios parámetros a partir de un array
-
push y unshift
-
Intercalar un array dentro de otro
-
Copiar un array en otro
-
Copiar un objeto en otro
-
// function(a, b, c)
let nums = [1, 3, 6];
function sumar(a, b, c) {
console.log(a + b + c);
}
sumar(...nums);
// function(n parámetros)
let a = 3;
let b = 7;
let c = 8;
function sumar(...nums) {
let suma = 0;
for (n of nums) {
suma += n;
}
console.log("La suma es " + suma);
}
sumar(a, b, c);
// push y unshift
let nums1 = [1, 3, 6];
let nums2 = [0, 52, 15, 9];
nums1.push(...nums2);
console.log(nums1);
nums1.unshift(...nums2);
console.log(nums1);
// meter un array en medio de otra
let nums1 = [1, 3, 6];
let nums2 = [0, 52, 15, 9];
nums1.splice(1, 0, ...nums2);
console.log(nums1);
// copiar un array
let nums1 = [1, 3, 6];
let nums2 = [...nums1];
// mergear un objeto con defaults (ES2018)
let defaults = {
ancho: 100,
alto: 200,
color: "rojo"
}
let o1 = {
ancho: 200,
grosor: 10000
}
o1 = { ...defaults, ...o1 };
// convertir un NodeList a un array
let lis = [...document.getElementsByTagName("li")];
ES6
-
Clases
-
Propiedades y métodos
-
class A {
constructor(z) {
this.x = 3;
this.y = 10;
this.z = z;
}
suma() {
return this.x + this.y + this.z;
}
}
let a = new A(20);
console.log(a.suma());
ES6
-
Clases
-
Propiedades y métodos
-
Getters y setters
-
class A {
constructor(z) {
this.x = 3;
this.y = 10;
this.z = z;
}
suma() {
return this.x + this.y + this.z;
}
set zeta(z) {
this.z = z * 2;
}
get zeta() {
return this.z / 2;
}
}
let a = new A(20);
a.zeta = 15;
console.log(a.zeta);
ES6
-
Clases
-
Propiedades y métodos
-
Getters y setters
-
Métodos estáticos
-
class A {
constructor(z) {
this.x = 3;
this.y = 10;
this.z = z;
}
static getPI() {
return 3.14159;
}
suma() {
return this.x + this.y + this.z;
}
set zeta(z) {
this.z = z * 2;
}
get zeta() {
return this.z / 2;
}
}
let a = new A(20);
a.zeta = 15;
console.log(a.zeta);
console.log(A.getPI());
ES6
-
Clases
-
Propiedades y métodos
-
Getters y setters
-
Métodos estáticos
-
Herencia con extends y super()
-
class A {
constructor(z) {
this.x = 3;
this.y = 10;
this.z = z;
}
static getPI() {
return 3.14159;
}
suma() {
return this.x + this.y + this.z;
}
set zeta(z) {
this.z = z * 2;
}
get zeta() {
return this.z / 2;
}
}
class B extends A {
constructor() {
super(100);
this.x = 20;
}
suma() {
return this.x + this.z;
}
resta() {
return this.x - this.z;
}
}
let b = new B();
console.log(b.suma());
console.log(b.resta());
ES6
-
Módulos
-
import
import { literal } from 'ruta_modulo';
import literal from 'ruta_modulo';
import * as literal from 'ruta_modulo';
import 'ruta_modulo'; - export
export let a = 3;
export let class Clase {
...
}
export default {
key: value
}
-
ES6
-
Módulos
-
import dinámicos
import('ruta_modulo').then(
modulo => ...
);
-
Métodos de los arrays
-
Métodos:
-
map
-
let nombres = ["juan", "luisa", "amparo", "arturo"];
let nombresMays = nombres.map(nombre => nombre.toUpperCase());
console.log(nombresMays);
Métodos de los arrays
-
Métodos:
-
map
-
filter
-
let personas = [
{
nombre: "juan",
edad: 15
},
{
nombre: "luisa",
edad: 35
},
{
nombre: "amparo",
edad: 17
},
{
nombre: "arturo",
edad: 32
}
];
let mayoresEdad = personas.filter(persona => persona.edad >= 18);
console.log(mayoresEdad);
Métodos de los arrays
-
Métodos:
-
map
-
filter
-
reduce
-
let nums = [2, 4, 10, 15, 12];
let suma = nums.reduce((x, y) => x + y);
let objs = [
{
x: 3,
y: 2
},
{
x: 8,
y: 10
},
{
x: 10,
y: 15
}
]
let sumaX = objs.reduce((acc, obj) => acc + obj.x, 0); // Método 1
let sumaX = objs.map(obj => obj.x).reduce((obj1, obj2) => obj1 + obj2); // Método 2
Métodos de los arrays
-
Métodos:
-
map
-
filter
-
reduce
-
find
-
let notas = [
{
nombre: "juan",
nota: 6
},
{
nombre: "luisa",
nota: 8
},
{
nombre: "amparo",
nota: 4
},
{
nombre: "arturo",
nota: 3
}
];
let notaArturo = notas.find(n => n.nombre === "arturo");
Métodos de los arrays
-
Métodos:
-
map
-
filter
-
reduce
-
find
-
-
Encadenamiento
let notas = [
{
nombre: "juan",
nota: 6
},
{
nombre: "luisa",
nota: 8
},
{
nombre: "amparo",
nota: 4
},
{
nombre: "arturo",
nota: 3
}
];
let notasAprobados = notas.filter(n => n.nota >= 5).map(n => n.nota);
console.log(notasAprobados);
TypeScript
TypeScript
-
Superconjunto de JavaScript
-
Transpila a ES5 (o a otra versión)
TypeScript
-
Superconjunto de JavaScript
-
Transpila a ES5 (o a otra versión)
-
Tipado
-
Errores en tiempo de compilación
TypeScript
-
Superconjunto de JavaScript
-
Transpila a ES5 (o a otra versión)
-
Tipado
-
Errores en tiempo de compilación
-
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "./public/js/",
}
}
tsconfig.json
TypeScript - Tipos
-
Tipos básicos:
number
string
boolean
Array
any
void
let peso: number;
peso = 89.5;
let saludo: string;
saludo = 'Vais a petarlo con TypeScript';
let esVerano: boolean;
esVerano = false;
let nums: Array<number>;
nums = [10, 55, -3, 4.14];
let nombres: string[];
nombres = ['Juan', 'Paqui', 'Lorenzo', 'Alicia'];
let cosas: any[];
cosas = [10, 'Teruel', -5, true, [0, -10, 15], false];
function imprimeSaludo(s: string): void {
console.log(s);
}
imprimeSaludo('Buenas tardes');
TypeScript - Tipos
-
Tipos básicos:
-
number
-
string
-
boolean
-
Array
-
any
-
void
-
-
Enum
enum FormasPago {
TPV,
PayPal,
transferencia
}
let pago: FormasPago;
pago = FormasPago.PayPal;
procesarPago(pago);
function procesarPago(formaPago: FormasPago): void {
switch (formaPago) {
case FormasPago.TPV:
// ...
break;
case FormasPago.PayPal:
// ...
break;
case FormasPago.transferencia:
// ...
break;
}
}
TypeScript - Tipos
-
Tipos básicos:
-
number
-
string
-
boolean
-
Array
-
any
-
void
-
-
Enum
-
Union types
let numeros: Array<number | string>;
numeros = ['3', 6, '15.8', 0];
function procesar(a: string | number): void {
if (typeof a === 'string') {
console.log(a.toUpperCase());
} else {
console.log(a.toFixed(2));
}
}
TypeScript - Tipos
-
Tipos básicos:
-
number
-
string
-
boolean
-
Array
-
any
-
void
-
-
Enum
-
Union types
-
Genéricos
function verDoble<T>(elem: T): T[] {
let elemDoble: T[] = [elem, elem];
return elemDoble;
}
TypeScript - Tipos
-
Tipos básicos:
-
number
-
string
-
boolean
-
Array
-
any
-
void
-
-
Enum
-
Union types
-
Genéricos
-
Type assertion
const inputText = document.getElementById("nombre") as HTMLInputElement;
inputText.select();
TypeScript - Tipos
-
Tipos básicos:
-
number
-
string
-
boolean
-
Array
-
any
-
void
-
-
Enum
-
Union types
-
Genéricos
-
Type assertion
-
Alias
type Posicion = 1 | 2 | 3;
let p: Posicion;
p = 3; // OK
p = 5; // Error
TypeScript - Funciones
-
Sin flexibilidad en el número de parámetros
function sumar(a: number, b: number): number {
return a + b;
}
sumar(); // Error
sumar(3); // Error
sumar(10, 2); // OK
sumar(4, -3, 10, 8) // Error
TypeScript - Funciones
-
Sin flexibilidad en el número de parámetros
-
Parámetros opcionales
function sumar(a: number, b: number, c?: number): number {
if (c) {
return a + b + c;
} else {
return a + b;
}
}
sumar(10, 2);
sumar(10, 2, 15);
TypeScript - Funciones
-
Sin flexibilidad en el número de parámetros
-
Parámetros opcionales
-
Sobrecarga
function nChars(a: number): string;
function nChars(a: string): number;
function nChars(a: string | number): number | string {
if (typeof a === 'number') {
return '¡Es un número!';
} else if (typeof a === 'string') {
return a.length;
}
}
type RGB = [number, number, number];
function convierteColor(color: string): RGB;
function convierteColor(color: RGB): string;
function convierteColor(color: string | RGB): string | RGB {
if (typeof color === 'string') {
return [0, 128, 0];
} else {
return '#006600';
}
}
const colorRGB = convierteColor('#006600');
const colorHEX = convierteColor([0, 128, 0]);
TypeScript - Funciones
-
Sin flexibilidad en el número de parámetros
-
Parámetros opcionales
-
Sobrecarga
-
Function types
function transformaNumero(x: number, callback: (n: number) => void) {
callback(x);
}
let a = 10;
transformaNumero(a, m => console.log(m * 2));
TypeScript - Módulos
- Sintaxis ES6:
-
import { literal } from 'ruta_modulo';
import literal from 'ruta_modulo';
import * as literal from 'ruta_modulo';
import 'ruta_modulo'; - export let a = 3;
export class Clase {
...
}
export default {
key: value
}
-
TypeScript - Módulos
-
Módulos
-
import dinámicos
import('ruta_modulo').then(
modulo => ...
);
-
TypeScript - Módulos
- Sintaxis ES6
- Se omite la extensión .ts
- Importar de paquetes npm: nombre del paquete
import { } from 'paquete'; - Importar de nuestros módulos: rutas relativas
import { } from './modulo';
TypeScript - Clases
- Propiedades fuera del constructor
class Factura {
numero: string;
base: number;
tipoIva: number;
constructor(numero: string, base: number, tipoIva: number = 21) {
this.numero = numero;
this.base = base;
this.tipoIva = tipoIva;
}
}
TypeScript - Clases
- Propiedades fuera del constructor
- Visibilidad de los miembros
- Modificador readonly
- Propiedades estáticas
class Factura {
private static caracteresSerie = 2;
public num: string;
public serie: string;
public base: number;
private readonly intTipoIva: number;
constructor(base: number, tipoIva: number = 21) {
this.base = base;
this.intTipoIva = tipoIva;
}
get numero(): string {
return this.serie + this.num;
}
set numero(n: string) {
this.serie = n.slice(0, Factura.caracteresSerie - 1);
this.num = n.slice(Factura.caracteresSerie);
}
}
let f = new Factura(100);
f.numero = 'AB600';
console.log(f.numero);
TypeScript - Clases
- Propiedades fuera del constructor
- Visibilidad de los miembros
- Getters y setters
- Modificador readonly
- Propiedades estáticas
- Métodos abstractos
abstract class Vehiculo {
public manual: boolean;
constructor(public ruedas: number, public motor: Motor) {
this.manual = this.motor === Motor.ninguno;
}
public abstract arrancar(): void;
}
class Bici extends Vehiculo {
public arrancar(): void {
console.log('Me pongo de pie y pedaleo');
}
}
TypeScript - Clases
- Propiedades fuera del constructor
- Visibilidad de los miembros
- Getters y setters
- Modificador readonly
- Propiedades estáticas
- Métodos abstractos
- Interfaces
interface Arrancable {
arrancar(): void;
apagar(): void;
}
class Vehiculo {
public manual: boolean;
constructor(public ruedas: number, public motor: Motor) {
this.manual = this.motor === Motor.ninguno;
}
}
class Bici extends Vehiculo implements Arrancable {
public arrancar(): void {
console.log('Me pongo de pie y pedaleo');
}
public apagar(): void {
console.log('Me bajo de la bici');
}
}
interface Cliente {
id: number;
login: string;
nombre: string;
tipo: TiposCliente;
fechaAlta: Date;
}
function getClientes(): Cliente[] {
let clientes: Cliente[] = conectaBD('clientes');
return clientes;
}
TypeScript - Decoradores
-
@
-
Asignar metadatos
-
Muy utilizados en Angular
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-factura',
templateUrl: './factura.component.html',
styleUrls: ['./factura.component.css']
})
export class FacturaComponent {
@Input()
facturaId: number;
}
Angular
Primeros pasos
-
Generar la app:
ng new <nombre-app> --prefix <prefijo> -
El comando crea un nuevo directorio y lanza npm install dentro
-
Opciones de ng new:
--prefix: prefijo de la app
--skip-tests: no genera los archivos *.spec.ts
--skip-install: se salta el paso de npm install
Primeros pasos
-
Ejecutar la app y verla en el navegador:
ng serve -o -
Entornos dev y prod
-
Archivos de configuración
-
Activar strictTemplates en tsconfig.json para mejor debug de plantillas
-
-
Módulos: contenedores lógicos
-
Componentes
-
Divisiones de la UI
-
Tienen clase y template (controlador y vista)
-
Estilos propios (:host para el propio componente)
-
-
Creando piezas:
ng generate <tipo-pieza> <nombre-pieza> [params]
Esqueleto de una pieza en Angular
-
clase =>
-
=> clase exportada =>
-
=> clase decorada =>
-
=> dependencias
Examinando un módulo
-
Metadata
-
declarations:
- componentes, directivas y pipes del módulo -
imports:
- otros módulos cuyos componentes, directivas o pipes exportados queremos usar -
exports:
- componentes, directivas o pipes que exponemos para que los usen otros módulos -
providers:
- objetos inyectables que están disponibles para el inyector del módulo -
bootstrap:
- componente(s) inicial de la app
-
Examinando un componente
-
Metadata
-
selector:
Selector CSS que se corresponde con una etiqueta HTML -
template / templateUrl:
String con el HTML / fichero con el HTML -
styles / styleUrls:
Strings con los estilos / ficheros con los estilos
-
-
ngOnInit
Componente inicializado (con su vista renderizada y sus valores cargados), se usa para los procesos iniciales (no usar el constructor). -
ngOnDestroy
Examinando un template
-
Custom elements
-
Data binding
-
Interpolation
-
Property binding
-
Class & style binding
-
Event binding
-
Two-way binding
Examinando un template
-
Directivas de atributo
-
ngClass
-
ngStyle
-
-
Directivas estructurales
-
ngIf
-
ngFor
-
-
Pipes
Servicios
-
Dependency Injection:
Proveedores y jerarquía de inyectores -
Injectable()
-
Singleton: tiene como ámbito su inyector y todos sus inyectores hijos.
Formularios
-
Template driven y Reactive forms
-
[(ngModel)]: Two-way binding
-
Importar el módulo FormsModule
-
Variables de template con #:
#formulario="ngForm"
#control="ngModel" -
Validaciones: ng-invalid, ng-dirty y ng-touched
-
Capturar el envío: ngSubmit
-
Resetear los estados
Despliegue a producción
-
Pruebas con ng build
-
ng build:
-
--prod: código optimizado para producción
-
--base-href=: cambia el directorio base
-
--sourcemaps: genera los source maps
-
-
lite-server --baseDir="dist/project-name"
-
Entornos propios
Links
Angular 9 y TypeScript remoto
By mariogl
Angular 9 y TypeScript remoto
Curso Angular 9 y TypeScript remoto 8-11 junio 2020
- 584