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,242