Angular workshop
Gonzalo Ruiz de Villa Suárez
@gruizdevilla
@gft_es
Angular Madrid
https://goo.gl/IggjlA
'AngularJS was a framework, Angular is a platform' The father of Angular Misko Hevery
https://goo.gl/IggjlA
Web
Mobile Web
Android
iOS
Windows
Mac
Linux
Angular tiene versionado semántico
Progressive Web Apps
- pensado para construir aplicaciones del tamaño que sea
- mobile first
- rendimiento brutal
- sencillo y expresivo
- inmutabilidad
- inyección de dependencias jerárquica
- integración con webcomponents
- i18n
- server side rendering
- web workers
- aplicaciones nativas, de escritorio
- PWA
- Angular CLI
¿Por qué Angular?
Empecemos
1. Configuremos
el entorno
{
"name": "angular-particles",
"version": "1.0.0",
"description": "Tutorial demo",
"scripts": {
"start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
"lite": "lite-server",
"tsc": "tsc",
"tsc:w": "tsc -w"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@angular/animations": "~4.2.0",
"@angular/common": "~4.2.0",
"@angular/compiler": "~4.2.0",
"@angular/compiler-cli": "~4.2.0",
"@angular/core": "~4.2.0",
"@angular/forms": "~4.2.0",
"@angular/http": "~4.2.0",
"@angular/platform-browser": "~4.2.0",
"@angular/platform-browser-dynamic": "~4.2.0",
"@angular/platform-server": "~4.2.0",
"@angular/router": "~4.2.0",
"angular-in-memory-web-api": "~0.3.0",
"systemjs": "0.19.40",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"zone.js": "^0.8.4"
},
"devDependencies": {
"concurrently": "^3.2.0",
"lite-server": "^2.2.2",
"typescript": "~2.3.2",
"@types/node": "^6.0.46"
}
}
package.json
Configuramos el proyecto
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2016", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
tsconfig.json
Indicamos como funcionar al compilador de TypeScript
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our entry point is within the current folder
app: '',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
systemjs.config.js
Configuramos systemjs
Casi estamos listos para implementar un hola mundo
> npm install
Instalamos las dependencias
Ahora si podemos empezar tirar código 😊
o, si quieres, también vale:
> yarn
2. Hola mundo o si mi entorno ya funciona
Creamos una carpeta para la aplicación
> mkdir app
app/app.module.ts
Toda aplicación tiene al menos un módulo, el raíz
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [ BrowserModule ]
})
export class AppModule { }
import {Component} from '@angular/core';
@Component({
selector: 'my-app',
template: '<h1>Mi primera app: chispas.✨</h1>'
})
export class AppComponent { }
app/app.component.ts
¡Mi primer componente, chispas! ✨
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app/app.module.ts
Importamos el componente en el módulo de la aplicación
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
main.ts
Donde decimos que hay que arrancar
¿'@angular/platform-browser-dynamic'? ¿por qué no está en '@angular/core'?
¿por qué crear main.ts y no introducir el código en app.module.ts?
Cómo se arranca una aplicación es algo específico de la plataforma. Otras opciones pueden ser NativeScript, Apache Cordoba o incluso el servidor.
<html>
<head>
<title>Angular 2 Workshop</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/styles.css">
<!-- 1. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<!-- 2. Configure SystemJS -->
<script src="/systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
<base href="/">
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
index.html
index.html
- Polyfills de navegadores
- angular2 polyfills: zones
- SystemJS como module loader, aunque podrías usar otro como webpack
/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
a {
cursor: pointer;
cursor: hand;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.router-link-active {
color: #039be5;
}
/* items class */
.items {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 24em;
}
.items li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.items li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.items li.selected:hover {
background-color: #BBD8DC;
color: white;
}
.items .text {
position: relative;
top: -3px;
}
.items {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 24em;
}
.items li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.items li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.items li.selected {
background-color: #CFD8DC;
color: white;
}
.items li.selected:hover {
background-color: #BBD8DC;
}
.items .text {
position: relative;
top: -3px;
}
.items .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}
styles.css
> npm start
🍀
3. Introduciendo el modelo estándar
y los componentes de Angular
Un poco de interpolación.
import {Component} from '@angular/core';
@Component({
selector: 'my-app',
template: '<h1>{{titulo}}</h1>'
})
export class AppComponent {
titulo = 'Modelo estándar';
}
app/app.component.ts
//...
export class Particle {
id: string;
name: string;
mass: string;
charge: string;
spin: string;
type: ParticleType;
forces: Force[];
}
type ParticleType = 'Quark' | 'Lepton' | 'Boson';
type Force = 'Strong' | 'Electromagnetic' | 'Weak';
app/app.component.ts
Agreguemos una clase
particle: Particle = {
id: 'u',
name: 'up',
mass: '2.3MeV/c²',
charge: '2/3',
spin: '1/2',
type: 'Quark',
forces: ['Electromagnetic', 'Strong', 'Weak']
};
app/app.component.ts
Agregamos la propiedad a AppComponent
template: '<h1>{{titulo}}</h1><p>Los detalles del {{particle.type}} <b>{{particle.name}}.</b></p>'
Y cambiamos el template
template: `
<h1>{{titulo}}</h1>
<h2>Detalles del {{particle.type}} {{particle.name}}.</h2>
<p>Masa: {{particle.mass}}</p>
<p>Carga: {{particle.charge}}</p>
<p>Spin: {{particle.spin}}</p>
<p>Type: {{particle.type}}</p>
<p>Fuerzas: {{particle.forces.join(', ')}}</p>
`
app/app.component.ts
El backtick ` nos permite un template más manejable
template: `
<h1>{{titulo}}</h1>
<h2>Detalles del {{particle.type}} {{particle.name}}.</h2>
<p>Masa: {{particle.mass}}</p>
<p>Carga: {{particle.charge}}</p>
<p>Spin: {{particle.spin}}</p>
<p>Type: {{particle.type}}</p>
<p>Fuerzas: {{particle.forces.join(', ')}}</p>
<div>
<label>Ajusta la masa: </label>
<input value="{{particle.mass}}" placeholder="masa">
</div>
`
app/app.component.ts
Ajustando la masa
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app/app.module.ts
Importamos el módulo de formularios
template: `
<h1>{{titulo}}</h1>
<h2>Detalles del {{particle.type}} {{particle.name}}.</h2>
<p>Masa: {{particle.mass}}</p>
<p>Carga: {{particle.charge}}</p>
<p>Spin: {{particle.spin}}</p>
<p>Type: {{particle.type}}</p>
<p>Fuerzas: {{particle.forces.join(', ')}}</p>
<div>
<label>Ajusta la masa: </label>
<input [(ngModel)]="particle.mass" placeholder="masa">
</div>
`
app/app.component.ts
Binding bidireccional
(la caja de platanos)
4. El listado de partículas fundamentales
y un master-detail con Angular
//...
var PARTICLES: Particle[] = [
{ id: 'u', name: 'up', mass: '2.3MeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'd', name: 'down', mass: '4.8MeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'c', name: 'charm', mass: '1.275GeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 's', name: 'strange', mass: '95MeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 't', name: 'top', mass: '173.07GeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'b', name: 'bottom', mass: '4.18GeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'e', name: 'electron', mass: '0.511MeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'μ', name: 'muon', mass: '105.7MeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'τ', name: 'tau', mass: '1.777GeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'νe', name: 'electron neutrino', mass: '<2.2eV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'νμ', name: 'muon neutrino', mass: '<0.17MeV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'ντ', name: 'tau neutrino', mass: '<15.5MeV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'g', name: 'gluon', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Strong'] },
{ id: 'γ', name: 'photon', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Electromagnetic'] },
{ id: 'Z', name: 'Z boson', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Weak'] },
{ id: 'W', name: 'W boson', mass: '0', charge: '0', spin: '±1', type: 'Boson', forces: ['Weak'] },
{ id: 'H', name: 'Higgs boson', mass: '0', charge: '0', spin: '0', type: 'Boson', forces: [] }
];
app/app.component.ts
El listado de partículas
app/app.component.ts
export class AppComponent {
titulo = 'Modelo estándar';
particles = PARTICLES;
}
Agregamos las partículas al componente
app/app.component.ts
<ul class="particles">
<li *ngFor="let particle of particles">
<span class="code">{{particle.id}}</span>
{{particle.name}}
</li>
</ul>
Y agregamos al template:
Si la página deja de funcionar, mira la consola. Lo que referencia a la propiedad 'particle' de AppComponent está fallando con un null pointer (por ejemplo, "Cannot read property 'type' of undefined").
app/app.component.ts
<li *ngFor="let particle of particles" (click)="onSelect(particle)">
Bind al evento click del li
La sintaxis (evento)="expresion", agrega un listener sobre evento, y evalúa expresion cuando se produce.
app/app.component.ts
selectedParticle: Particle;
Guardaremos la partícula seleccionada
onSelect(particle: Particle) { this.selectedParticle = particle; }
y definimos onSelect
app/app.component.ts
<h2>Detalles del {{selectedParticle.type}} {{selectedParticle.name}}.</h2>
<p>Masa: {{selectedParticle.mass}}</p>
<p>Carga: {{selectedParticle.charge}}</p>
<p>Spin: {{selectedParticle.spin}}</p>
<p>Type: {{selectedParticle.type}}</p>
<p>Fuerzas: {{selectedParticle.forces.join(', ')}}</p>
<div>
<label>Ajusta la masa: </label>
<input [(ngModel)]="selectedParticle.mass" placeholder="masa">
</div>
Mostramos los detalles de la partícula
app/app.component.ts
<div *ngIf="selectedParticle">
<h2>Detalles del {{selectedParticle.type}} {{selectedParticle.name}}.</h2>
<p>Masa: {{selectedParticle.mass}}</p>
<p>Carga: {{selectedParticle.charge}}</p>
<p>Spin: {{selectedParticle.spin}}</p>
<p>Type: {{selectedParticle.type}}</p>
<p>Fuerzas: {{selectedParticle.forces.join(', ')}}</p>
<div>
<label>Ajusta la masa: </label>
<input [(ngModel)]="selectedParticle.mass" placeholder="masa">
</div>
</div>
Evitemos el null pointer.
app/app.component.ts
styles:[`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.particles {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
float: left;
}
.particles li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 20px 4px 4px 20px;
}
.particles li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.particles li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.particles .text {
position: relative;
top: -3px;
}
.particles .code {
display: inline-block;
text-align: center;
font-size: small;
color: white;
width: 1.2em;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 20px;
}
`]
Agreguemos unos estilos en @Component
app/app.component.ts
[class.selected]="particle === selectedParticle"
Podemos marcar el elemento li seleccionado con
4. Partículas compuestas
o creando componentes con Angular
El código está quedando farragoso, por lo que el siguiente paso es cortarlo en partes más manejables, mantenibles y fáciles de entender.
app/particle-detail.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'my-particle-detail',
})
export class ParticleDetailComponent {
}
Nuevo componente en fichero nuevo.
app/particle-detail.component.ts
template:` <div *ngIf="particle">
<h2>Detalles del {{particle.type}} {{particle.name}}.</h2>
<p>Masa: {{particle.mass}}</p>
<p>Carga: {{particle.charge}}</p>
<p>Spin: {{particle.spin}}</p>
<p>Type: {{particle.type}}</p>
<p>Fuerzas: {{particle.forces.join(', ')}}</p>
<div>
<label>Ajusta la masa: </label>
<input [(ngModel)]="particle.mass" placeholder="masa">
</div>
</div>
`
Nos traemos su fragmento de template (cambiando selectedParticle por particle).
app/particle-detail.component.ts
particle: Particle;
Agregamos la propiedad
app/particle.ts
export class Particle {
id: string;
name: string;
mass: string;
charge: string;
spin: string;
type: ParticleType;
forces: Force[];
}
export type ParticleType = 'Quark' | 'Lepton' | 'Boson';
export type Force = 'Strong' | 'Electromagnetic' | 'Weak';
Definimos la partícula de forma centralizada
app/app.component.ts
import {Particle} from './particle';
y agregamos los correspondientes imports
app/particle-detail.component.ts
app/app.component.ts
<my-particle-detail [particle]="selectedParticle"></my-particle-detail>
Usamos detail "my-particle-detail" en la plantilla:
app/particle-detail.component.ts
@Input()
particle: Particle;
Anotamos una propiedad del componente para que pueda recibir datos.
(*cuidado con dejarse los paréntesis en la anotación)
import {Component, Input} from '@angular/core';
y hay que cambiar el import
app/app.module.ts
import {ParticleDetailComponent} from './particle-detail.component';
Importamos el componente particle-detail para poder registrarlo en el módulo
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
ParticleDetailComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
4. Fabricando partículas
desde servicios de Angular
app/particle.service.ts
import {Injectable} from '@angular/core';
@Injectable()
export class ParticleService {
}
Creamos un servicio que devuelva la información necesaria.
app/particle.service.ts
import { Injectable } from '@angular/core';
import {PARTICLES} from './mock-particles';
import {Particle} from './particle';
@Injectable()
export class ParticleService {
getParticles(): Particle[]{
return PARTICLES;
}
}
Le dotamos de contenido:
import {Particle} from './particle';
export var PARTICLES: Particle[] = [
{ id: 'u', name: 'up', mass: '2.3MeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'd', name: 'down', mass: '4.8MeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'c', name: 'charm', mass: '1.275GeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 's', name: 'strange', mass: '95MeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 't', name: 'top', mass: '173.07GeV/c²', charge: '2/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'b', name: 'bottom', mass: '4.18GeV/c²', charge: '1/3', spin: '1/2', type: 'Quark', forces: ['Electromagnetic', 'Strong', 'Weak'] },
{ id: 'e', name: 'electron', mass: '0.511MeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'μ', name: 'muon', mass: '105.7MeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'τ', name: 'tau', mass: '1.777GeV/c²', charge: '-1', spin: '1/2', type: 'Lepton', forces: ['Electromagnetic', 'Weak'] },
{ id: 'νe', name: 'electron neutrino', mass: '<2.2eV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'νμ', name: 'muon neutrino', mass: '<0.17MeV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'ντ', name: 'tau neutrino', mass: '<15.5MeV/c²', charge: '0', spin: '1/2', type: 'Lepton', forces: ['Weak'] },
{ id: 'g', name: 'gluon', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Strong'] },
{ id: 'γ', name: 'photon', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Electromagnetic'] },
{ id: 'Z', name: 'Z boson', mass: '0', charge: '0', spin: '1', type: 'Boson', forces: ['Weak'] },
{ id: 'W', name: 'W boson', mass: '0', charge: '0', spin: '±1', type: 'Boson', forces: ['Weak'] },
{ id: 'H', name: 'Higgs boson', mass: '0', charge: '0', spin: '0', type: 'Boson', forces: [] }
];
app/mock-particles.ts
app/app.module.ts
import { ParticleService } from './particle.service';
Registramos el servicio
//en @NgModule
providers: [ParticleService],
app/app.component.ts
Importamos el servicio
import { ParticleService } from './particle.service';
import { Particle } from './particle';
constructor(private particleService: ParticleService) { }
Agregamos la dependecias
El componente tiene un ciclo de vida: ngOnInit, ngOnChanges, ngDoCheck, ngOnDestroy
En ngOnInit inicializamos la propiedad particles
app/app.component.ts
import { OnInit } from '@angular/core';
export class AppComponent implements OnInit{
//...
particles:Particle[];
//...
ngOnInit() {
this.getParticles();
}
//...
getParticles() {
this.particles = this.particleService.getParticles();
}
}
El servicio debería ser asíncrono
app/particle.service.ts
getParticles(): Promise<Particle[]> {
return Promise.resolve(PARTICLES);
}
getParticlesSlowly(): Promise<Particle[]> {
return new Promise<Particle[]>(resolve =>
setTimeout(()=>resolve(PARTICLES), 2000)
);
}
Al resolver la promesa guardamos las partículas recibidas
app/app.component.ts
getParticles(){
this.particleService.getParticles().then(
particles => this.particles = particles
)
}
5. Navegando por la realidad
y las rutas con Angular
Vamos a agregar tres estados a la aplicación:
- Vista por familias
- Vista de listado
- Vista de detalle
app/app.component.ts
app/particles.component.ts
renombramos
como
cambiamos el nombre de la clase ParticlesComponent y el selector a my-particles
app/app.component.ts
creamos el fichero de nuevo con:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<my-particles></my-particles>
`
})
export class AppComponent {
titulo = 'Modelo estándar';
}
app/app.module.ts
y actualizamos el módulo
//...
import { ParticlesComponent } from './particles.component';
//...
declarations: [
AppComponent,
ParticleDetailComponent,
ParticlesComponent
],
index.html
agregamos la referencia al módulo de routing
<base href="/">
app/app.routes.ts
Definimos la primera ruta creando un nuevo módulo
import { RouterModule } from '@angular/router';
import { ParticlesComponent } from './particles.component';
export var RoutesModule = RouterModule.forRoot([
{
path: 'particles',
component: ParticlesComponent
}
]);
app/app.module.ts
Importamos el módulo de rutas
import {RoutesModule} from './app.routes';
@NgModule({
imports: [
BrowserModule,
FormsModule,
RoutesModule
],
app/app.component.ts
Agregamos un enlace para navegar y un lugar donde pintar el contenido
template: `
<h1>{{title}}</h1>
<a [routerLink]="['particles']">Particles</a>
<a [routerLink]="['families']">Families</a>
<router-outlet></router-outlet>
`,
Si hacemos click en el enlace "Particles" veremos el listado de nuevo
app/families.component.ts
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import {ParticleService} from './particle.service';
@Component({
selector: 'my-families',
template: `
<h3>Families</h3>
<ul>
<li *ngFor="let family of families">{{family}}</li>
</ul>
`,
providers: [ParticleService]
})
export class FamiliesComponent implements OnInit{
families: String[];
constructor(private particleService: ParticleService) { }
ngOnInit() {
this.getFamilies();
}
getFamilies() {
this.particleService.getParticles().then(
particles =>
this.families = particles.reduce((acc,{type}) => acc.includes(type)?acc:[type,...acc],[])
)
}
}
app/app.routes.ts
import { RouterModule } from '@angular/router';
import { ParticlesComponent } from './particles.component';
import { FamiliesComponent } from './families.component';
export var RoutesModule = RouterModule.forRoot([
{
path: 'particles',
component: ParticlesComponent
},
{
path: 'families',
component: FamiliesComponent
}
]);
app/app.module.ts
import { FamiliesComponent } from './families.component';
declarations: [
AppComponent,
ParticleDetailComponent,
ParticlesComponent,
FamiliesComponent
],
app/app.routes.ts
{
path: '',
redirectTo: '/particles',
pathMatch: 'full'
},
Redirección a particles por defecto
app/particles.component.ts
Organizamos un poco mejor el código fuente:
<h1>{{titulo}}</h1>
<ul class="particles">
<li *ngFor="let particle of particles"
(click)="onSelect(particle)"
[class.selected]="particle === selectedParticle">
<span class="code">{{particle.id}}</span>
{{particle.name}}
</li>
</ul>
<div *ngIf="selectedParticle">
<p>Selected particle: {{selectedParticle.type}} {{selectedParticle.name}}</p>
<button (click)="gotoDetail()">Ir al detalle</button>
</div>
templateUrl: 'app/particles.component.html',
styleUrls: ['app/particles.component.css']
app/particles.component.html
app/particles.component.css
Metemos lo que había en la propiedad styles
.selected {
background-color: #CFD8DC !important;
color: white;
}
.particles {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
float: left;
}
.particles li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 20px 4px 4px 20px;
}
.particles li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.particles li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.particles .text {
position: relative;
top: -3px;
}
.particles .code {
display: inline-block;
text-align: center;
font-size: small;
color: white;
width: 1.2em;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 20px;
}
app/app.routes.ts
Y agregamos una nueva ruta para los detalles
import { ParticleDetailComponent } from './particle-detail.component';
{
path: 'detail/:id',
component: ParticleDetailComponent
}
app/particle-detail.component.ts
Agregamos los imports
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ParticleService } from './particle.service';
Inyectamos los servicios
constructor(
private particleService: ParticleService,
private route: ActivatedRoute,
private location: Location
) {}
app/particle-detail.component.ts
Informamos de que tiene ciclo de vida
export class ParticleDetailComponent implements OnInit {
Y creamos la tarea de inicio
ngOnInit(): void {
this.route.params.forEach((params: Params) => {
let id = params['id'];
this.particleService.getParticle(id)
.then(particle => this.particle = particle);
});
}
Agregamos el método que falta
app/particle.service.ts
getParticle(id:string):Promise<Particle> {
return this.getParticles().then(
(particles:Particle[]) => particles.find(
particle => particle.id === id
)
)
}
Volver atrás
app/particle.component.ts
goBack() {
window.history.back();
}
<button (click)="goBack()">Back</button>
constructor(
private router: Router,
private particleService: ParticleService
) {}
app/particles.component.ts
gotoParticle(particle:Particle){
let link = ['/detail', particle.id];
this.router.navigate(link);
}
import {Router} from '@angular/router';
app/particles.component.html
<h1>{{titulo}}</h1>
<ul class="particles">
<li *ngFor="let particle of particles"
(click)="gotoParticle(particle)">
<span class="code">{{particle.id}}</span>
{{particle.name}}
</li>
</ul>
6. Teletransportación
y las peticiones $http con Angular
particles
[
{ "id": "u", "name": "up", "mass": "2.3MeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "d", "name": "down", "mass": "4.8MeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "c", "name": "charm", "mass": "1.275GeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "s", "name": "strange", "mass": "95MeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "t", "name": "top", "mass": "173.07GeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "b", "name": "bottom", "mass": "4.18GeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "e", "name": "electron", "mass": "0.511MeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "μ", "name": "muon", "mass": "105.7MeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "τ", "name": "tau", "mass": "1.777GeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "νe", "name": "electron neutrino", "mass": "<2.2eV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "νμ", "name": "muon neutrino", "mass": "<0.17MeV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "ντ", "name": "tau neutrino", "mass": "<15.5MeV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "g", "name": "gluon", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Strong"] },
{ "id": "γ", "name": "photon", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Electromagnetic"] },
{ "id": "Z", "name": "Z boson", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Weak"] },
{ "id": "W", "name": "W boson", "mass": "0", "charge": "0", "spin": "±1", "type": "Boson", "forces": ["Weak"] },
{ "id": "H", "name": "Higgs boson", "mass": "0", "charge": "0", "spin": "0", "type": "Boson", "forces": [] }
]
app/app.module.ts
import { HttpModule } from '@angular/http';
//...
imports: [
BrowserModule,
FormsModule,
RoutesModule,
HttpModule
],
app/app.module.ts
// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
//...
imports: [
BrowserModule,
FormsModule,
RoutesModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryDataService)
],
app/in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
let particles = [
{ "id": "u", "name": "up", "mass": "2.3MeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "d", "name": "down", "mass": "4.8MeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "c", "name": "charm", "mass": "1.275GeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "s", "name": "strange", "mass": "95MeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "t", "name": "top", "mass": "173.07GeV/c²", "charge": "2/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "b", "name": "bottom", "mass": "4.18GeV/c²", "charge": "1/3", "spin": "1/2", "type": "Quark", "forces": ["Electromagnetic", "Strong", "Weak"] },
{ "id": "e", "name": "electron", "mass": "0.511MeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "μ", "name": "muon", "mass": "105.7MeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "τ", "name": "tau", "mass": "1.777GeV/c²", "charge": "-1", "spin": "1/2", "type": "Lepton", "forces": ["Electromagnetic", "Weak"] },
{ "id": "νe", "name": "electron neutrino", "mass": "<2.2eV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "νμ", "name": "muon neutrino", "mass": "<0.17MeV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "ντ", "name": "tau neutrino", "mass": "<15.5MeV/c²", "charge": "0", "spin": "1/2", "type": "Lepton", "forces": ["Weak"] },
{ "id": "g", "name": "gluon", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Strong"] },
{ "id": "γ", "name": "photon", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Electromagnetic"] },
{ "id": "Z", "name": "Z boson", "mass": "0", "charge": "0", "spin": "1", "type": "Boson", "forces": ["Weak"] },
{ "id": "W", "name": "W boson", "mass": "0", "charge": "0", "spin": "±1", "type": "Boson", "forces": ["Weak"] },
{ "id": "H", "name": "Higgs boson", "mass": "0", "charge": "0", "spin": "0", "type": "Boson", "forces": [] }
];
return {particles};
}
}
app/particle.service.ts
Completamos el servicio con la llamada http
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import 'rxjs/add/operator/toPromise';
La importación de 'toPromise' agrega automáticamente el método a Observable
app/particle.service.ts
getParticles(): Promise<Particle[]> {
return this.http.get(this.particlesUrl)
.toPromise()
.then(response => response.json().data as Particle[])
.catch(this.handleError);
}
private handleError(error: any) {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
Completamos el servicio con la llamada http
private particlesUrl = 'app/particles';
constructor(private http: Http) { }
El servicio http devuelve un observable y no una promesa
7. Una pequeña reacción
app/app.module.ts
imports: [
BrowserModule,
FormsModule,
RoutesModule,
HttpModule,
BrowserAnimationsModule,
InMemoryWebApiModule.forRoot(InMemoryDataService)
],
Agregamos
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
app/families.component.ts
template: `
<h3>Families</h3>
<ul>
<li *ngFor="let family of families"
[@familyState]="family.state"
(click)="family.toggleState()"
>{{family.name}}</li>
</ul>
`,
styles:[`
ul {
width: 200px;
padding:0;
}
li {
padding: 10px;
border-bottom: 1px solid #ccc;
list-style-type: none;
}
li:last-child {
border-bottom: none;
}}
`],
Cambiamos la plantilla y los estilos
app/families.component.ts
import { trigger, state, style, animate, transition } from '@angular/animations';
animations: [
trigger('familyState', [
state('inactive', style({
backgroundColor: '#eee',
transform: 'scale(1)'
})),
state('active', style({
backgroundColor: '#cfd8dc',
transform: 'scale(1.1)'
})),
transition('inactive => active', animate('100ms ease-in')),
transition('active => inactive', animate('100ms ease-out'))
])
],
providers: [ParticleService]
})
export class FamiliesComponent implements OnInit{
Importamos el DSL de animaciones y configuramos las animaciones en el @Component
app/families.component.ts
export class FamiliesComponent implements OnInit{
families: Family[];
constructor(private particleService: ParticleService) { }
ngOnInit() {
this.getFamilies();
}
getFamilies() {
this.particleService.getParticles().then(
particles =>
this.families = particles.reduce(
(acc,{type}) => acc.includes(type)?acc:[type,...acc],
[]
).map(name => new Family(name)))
}
}
class Family {
constructor(public name:string){};
public state:string = 'inactive';
toggleState(){
this.state = this.state == 'active' ? 'inactive' : 'active';
}
}
Ampliamos el modelo con una clase
Mucha flexibilidad para configurar las animaciones.
¡Gracias!
Gonzalo Ruiz de Villa
@gruizdevilla
Angular Madrid
Angular workshop - Meetup Angular Madrid
By Gonzalo Ruiz de Villa
Angular workshop - Meetup Angular Madrid
Workshop introductorio de Angular del Meetup Angular Madrid
- 22,472