Aplicaciones web con un hamster gafapasta

Text

quiénes somos

Juanan

juanantonio.gomez@beeva.com

Bea

beatriz.demiguel@beeva.com

¿qué neCESITAMOS ANTES DE EMPEZAR?

  • Tu editor de código HTML/JS favorito
  • Node (https://nodejs.org) instalado (versión mínima 4.0)
  • npm (2.x)
  • Git
  • Clonarte el repo:
         https://github.com/shokmaster/ember-workshops
  • `ember ` Todos los comandos de ember-cli empiezan por "ember"

 

  • `new` crea el directorio de tu proyecto, un repositiorio git local y hace el primer commit por ti

 

  • `foodme` será el nombre de nuestro proyecto

  $ ember new foodme

CREA TU NUEVO PROYECTO

Let's start!

  $ npm install -g ember-cli

  $ cd foodme
  $ npm install
  $ bower install

MVC (o algo así)

Templates
HTML + Handlebars = HTMLBars

Models
Manejo y/o persistencia de datos

Routes
Puntos de entrada de la app.
Definen los distintos estados.

OTROS ELEMENTOS DE EMBER

 

  • Controllers: contienen lógica (por poco tiempo)

 

  • Services: objetos globales con glamour

 

  • Helpers: funcioncillas reshulonas en templates

 

  • Mixins: no escribas JS dos veces

 

  • Partials: no escribas HTML dos veces

 

  • `serve` levanta la app en el servidor, por defecto en http://localhost:4200. Además, tenemos liveReload 

  $ cd foodme

  $ ember serve

ARRANCA EL SERVIDOR

rOUTEs

  $ ember generate route restaurants

genera una ruta

 // CREATE app/routes/restaurants.js  
 export default Ember.Route.extend({
    
 });
 // UPDATE app/router.js
 Router.map(function(){
     this.route('restaurants');
 });
 // CREATE app/templates/restaurants.hbs  
 {{outlet}}

AÑADE un MODELO

 // app/routes/restaurants.js  
 import Ember from 'ember'
 export default Ember.Route.extend({
     model(){
         return {
             foo: 'bar'
         }
     }
 });

 

 // app/templates/restaurants.hbs  
 <h1>{{model.foo}}</h1>

Abre http://localhost:4200/restaurants

 // RESULT  
 <h1>bar</h1>

Para evaluar variables en plantilla usamos llaves

{{ someVariable }}

Consume datos

// app/routes/restaurants.js

import Ember from 'ember';

const host = 'https://raw.githubusercontent.com/shokmaster/ember-workshops/master/';
const restaurantsUrl = `${host}resources/restaurants.json`;

export default Ember.Route.extend({

	model: function() {
		return Ember.$.getJSON(restaurantsUrl);
	}
});
// app/templates/restaurants.hbs

<ul>
  {{#each model as |restaurant| }}
        <h4>{{restaurant.name}}</h4>
        <p>{{restaurant.description}}</p>
  {{/each}}
</ul>

Vamos a mostrar un listado de restaurantes

Iteramos con operador de bloque each

{{#each someArray as |item| }}

       ... print this for each item...

 {{/each}}

Ember.$
Tenemos jquery disponible

application template

Es la plantilla por defecto, y se renderiza en todas las rutas

      <h1>Welcome to Ember</h1>
      {{outlet}}   <-- ~ "Lo que tengas, ponlo aquí"

AÑADE BOOTSTRAP

// En app/templates/restaurants.hbs damos forma al listado:

<div class="row-fluid">
    <div class="col-md-12">
        <div class="panel panel-default">
            <div class="panel-heading"><h3 class="panel-title">Restaurantes encontrados</h3></div>
            <div class="panel-body">

                {{! LISTA DE RESTAURANTES }}
                {{#each model as |restaurant|}}
                    <div class="media">
                        <a class="media-left"><img class="media-object" src="#"></a>
                        <div class="media-body">
                            <h4 class="media-heading">{{restaurant.name}}</h4>
                            {{restaurant.description}}
                        </div>
                    </div>
                {{/each}}

            </div>
        </div>
    </div>
</div>
// En app/index.html añadimos las CSS de Bootstrap:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">
// En templates/application.hbs añadimos un container de Bootstrap:

<div class="container-fluid">{{outlet}}</div>

¡no tenemos menú!

Vamos a añadir un menú de navegación a nuestra web

 

Utilizaremos el HTML de Bootstrap:

https://getbootstrap.com/components/#navbar

Application Controller

nav-menu Component

Datos:

  • Nombre de nuestra web
  • Lista de links del menú

Acciones:

  • Abrir/cerrar menú
  • Cambiar idioma

AÑADIENDO un menú (1) (2)

  $ ember g component nav-menu

  $ ember g controller application

2. Creamos el application controller:

1. Creamos el componente nav-menu:

AÑADIENDO UN MENÚ (3)

// En app/controllers/application.js ponemos los datos que le pasaremos al componente nav-menu:
//  * Nombre de nuestra aplicación
//  * Lista de enlaces para el menú

import Ember from 'ember';

export default Ember.Controller.extend({

    appName: 'FoodMe',

    menuLinks: [{
        label: 'restaurants',
        url: 'restaurants'
    }]

});

3. Rellenamos el application controller:

AÑADIENDO UN MENÚ (4)

// En app/templates/components/nav-menu.hbs ponemos el HTML del menú:

<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            {{! Collapsed menu for small screens }}
            <button type="button" {{action 'toggleMenu'}} class="navbar-toggle" data-toggle="collapse" data-target="#navbar" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>

            {{! Brand with link}}
            {{link-to appName "index" class="navbar-brand"}}
        </div>

        {{! Add class "in" to show items in small screen when menu is open }}
        <div class="collapse navbar-collapse {{if isMenuOpened 'in'}}" id="navbar">
            {{! Menu links}}
            <ul class="nav navbar-nav">
        	{{#each menuLinks as |link|}}
                    <li>{{link-to link.name link.url}}</li>
                {{/each}}
            </ul>
        </div>
         {{!@TODO cambio de idioma }}
    </div>
</nav>

4. Rellenamos el template del componente:

Utilizamos el helper action para que al hacer click lance la acción del controlador llamada 'toggleMenu'

{{action 'myActionName'}}

Utilizamos el componente link-to para crear un link a una ruta de nuestra app

{{link-to 'nameToShow' 'routeName'}}

Utilizamos el helper if para añadir una clase

{{if someBooleanProperty 'classToAdd'}}

AÑADIENDO UN MENÚ (5)

// En app/components/nav-menu.js
//  * Variable isMenuOpened para controlar el estado abierto/cerrado

import Ember from 'ember';

export default Ember.Component.extend({

    isMenuOpened: false,

    actions: {
        toggleMenu(){
            this.toggleProperty('isMenuOpened');
        }
    }
});

5. Rellenamos el JavaScript del componente:

Utilizamos la función toggleProperty que equivale a un set con el valor inverso:

this.toggleProperty('myboolean')

equivale a:

const myboolean = this.get('myboolean');

this.set('myboolean',  !myboolean)

La propiedad actions tiene todas las acciones:          actions: {

                   functionAction1(){..},

                   functionAction2(){..},

                    ..

              } 

Añade un helper

  $ ember generate helper restaurant-img

Mueve las imágenes que hay en la carpeta resources/img/restaurants

a la carpeta de tu proyecto public/img/restaurants

{{restaurant-img 'tofuparadise'}}

img/restaurants/tofuparadise.jpg

Escribiendo esto:

Queremos obtener esto:

Modifica el helper restaurant-img para que devuelva el path de la imagen de cada restaurante:

Ejercicio por parejas

Genera una ruta 'about' añadiendo algún contenido

Recuerda añadir un enlace en el menú

Los addons son complementos para tu app

addon ~ plugin

utiliza addons

Consulta los addons disponibles en http://emberobserver.com/

instala el addon i18n

  • `install` incluye un addon en tu proyecto como dependencia en el package.json

 

  • `ember-i18n` es el nombre del addon que vamos a instalar. Sirve para traducir tu app de manera sencilla.

  $ ember install ember-i18n

Documentación de este addon:

https://github.com/jamesarosen/ember-i18n/wiki

el Helper 't'

El helper t sirve para traducir textos. Recibe como parámetro una key, busca su traducción y la devuelve.

Ember.i18n.t ('restaurants')

    { "restaurants": "Restaurantes"}

{{t 'restaurants'}}  // Pinta "Restaurantes"

{{#some-component param=(t 'restaurants')}}

- Traducción en app/locales/es/translations.js

- Uso en los .hbs:

o, si estamos ya dentro de otro helper:

- Uso en .js:

el servicio i18n

El servicio 18n está disponible en toda la aplicación, solo tenemos que inyectarlo. Tiene una propiedad locale que indica el idioma actual.

- Inyecta el servicio en tu componente/controlador/ruta, etc:

i18n: Ember.inject.service()

- Accede al servicio y setea el idioma:

                                         const i18nService = this.get('i18n')

                                         i18nService.set('locale', 'es');

añadiendo traducciones

  $ ember generate locale en                    // Generamos ficheros de inglés

  $ ember generate locale es                    // Generamos ficheros de español

1. Genera los ficheros de traducciones:

2. Hemos preparado el contenido de los ficheros por ti.

 

    Copia el contenido de estos ficheros:

          resources/translations/english.js

          resources/translations/spanish.js

    en:

          app/locales/en/translations.js

          app/locales/es/translations.js

traduce textos

    // MODIFICA app/controllers/application.js

    ...
    menuLinks: [{
        label: 'restaurants',
        url: 'restaurants'
    }, {
        label: 'about',
        url: 'about'
    }]

    ...

    // MODIFICA app/templates/components/nav-menu.hbs

    ...

    {{#each menuLinks as |link|}}
            <li>{{link-to (t link.label) link.url}}</li>
    {{/each}}

    ...

Ejercicio por parejas

 

Crea un helper que reciba un array de números y devuelva el dia de la semana.

cambiando de idioma

//MODIFICA app/components/nav-menu.js
import Ember from 'ember';

const languages = [{
	name: 'Español',
	code: 'es'
}, {
	name: 'English',
	code: 'en'
}];

export default Ember.Component.extend({

        ...

	i18n: Ember.inject.service(),

	languages,

	selectedLanguage: languages[0],

	actions: {

                ....

		modifyLanguage(languageCode){
		    const i18nService = this.get('i18n');
                    i18nService.set('locale', languageCode);
 		}
                ....
	}
});

Definimos el array de idiomas

Definimos una propiedad para saber el idioma seleccionado y la inicializamos

Definimos una acción para cambiar el idioma que recibe como parámetro el código de idioma ('en' o 'es')

 

cambiando de idioma

  $ ember install ember-cli-sass

  $ ember install ember-power-select

3. Instala ember-power-select para utilizar su componente dropdown

Documentación de este addon:

http://www.ember-power-select.com

//Incluye en app/templates/components/nav-menu.hbs donde indica @TODO cambio de idioma
<ul class="nav navbar-nav navbar-right">
    <li>
        {{#power-select
            selected=selectedLanguage
            options=languages
            class='dropdown'
            onchange=(action 'modifyLanguage' selectedLanguage.code)
            as |language|}}
               {{language.name}}
        {{/power-select}}
    </li>
</ul>

segunda parte

ANTES DE EMPEZAR...

Cambiamos a la rama 'part-2':

  $ git checkout part-2

Propiedades computadas

Las propiedades computadas permiten declarar funciones como propiedades. Se declaran llamando a Ember.computed

Argumentos que recibe:

- La propiedad o propiedades de las que depende (se vuelve a calcular la función si estas dependencias cambian)

- La función que retorna el valor deseado (sólo se llama una vez y el resultado se almacenará en caché )

 

 

let Person = Ember.Object.extend({
  // these will be supplied by `create`
  firstName: null,
  lastName: null,

  fullName: Ember.computed('firstName', 'lastName', function() {
    const firstName = this.get('firstName');
    const lastName  = this.get('lastName');

    return firstName + ' ' + lastName;
  })
});

let tom = Person.create({
  firstName: 'Tom',
  lastName: 'Dale'
});

tom.get('fullName') // 'Tom Dale'

Helpers de ember (1)

- concat concatena strings (concat firstName ' ' lastName)}}

- get propiedades de un objeto (get person 'name')

- hash crea objetos {{(hash name='Sarah' title=office)}} 

- Helpers para debugear: debugger (breakpoints), log (logs de variables)

- each-in itera sobre las propiedades de un objeto

- with alias de una propiedad a un nuevo nombre {{#with user.posts as |blogPosts|}}

- query-params pasa parámetros cuando se transiciona a una ruta:

                this.transitionTo('myRoute', { queryParams: { foo: 'bar'}})

Helpers de ember (2)

- mut permite especificar que un componente secundario puede actualizar el valor (mutable) que se le pasa. Cambia el valor del componente principal.

 

 

 

 

 ha

 

{{my-component updateAction=(action 'updateValue')}}
export default Ember.Controller.extend({ 
    actions: { 
        updateValue(newValue) { 
            this.set('model.value', newValue); 
        }
    } 
});
{{ my-component 
  updateAction=(action (mut model.value))
}}

Filtra los restaurantes (1)

  $ ember g controller restaurants

import Ember from 'ember';

const CUISINE_OPTIONS = [
    { name: 'african', title: 'African' },
    { name: 'american', title: 'American' },
    { name: 'barbecue', title: 'Barbecue' },
    { name: 'cafe', title: 'Cafe' },
    { name: 'chinese', title: 'Chinese' },
    { name: 'czech/slovak', title: 'Czech / Slovak' },
    { name: 'german', title: 'German' },
    { name: 'indian', title: 'Indian' },
    { name: 'japanese', title: 'Japanese' },
    { name: 'mexican', title: 'Mexican' },
    { name: 'pizza', title: 'Pizza' },
    { name: 'thai', title: 'Thai' },
    { name: 'vegetarian', title: 'Vegetarian' }
];

export default Ember.Controller.extend({

    CUISINE_OPTIONS,

    /**
     * Filter criterias.
     */
    filterCuisins: [],

    /**
     * Filter function.
     */
    filteredRestaurants: Ember.computed('model', 'filterCuisins', function() {
        let filteredRestaurants = this.get('model');

        const filterCuisins = this.get('filterCuisins');

        // Filter by cuisine
        if (Ember.isPresent(filterCuisins)) {
            filteredRestaurants = filteredRestaurants.filter((item) =>
                filterCuisins.mapBy('name').includes(item.cuisine)
            );
        }

        return filteredRestaurants;
    })

});

Definimos una propiedad que contiene el array con todos los tipos de cocina

Definimos una propiedad computada que depende de los tipos de cocina seleccionados y retorna los restaurantes filtrados

FILTRA los restaurantes (2)

{{! app/templates/restaurants.hbs }}
<div class="row-fluid">
  <div class="col-md-4">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title">{{t 'filterRestaurants'}}</h3>
      </div>

      <div class="panel-body">
        {{! @TODO filter rating }}
      </div>

      <div class="panel-body">
        <label>{{t 'cuisine'}}</label>
        {{#power-select-multiple
          options=CUISINE_OPTIONS
          selected=filterCuisins
          placeholder=(t 'selectCuisins')
          onchange=(action (mut filterCuisins))
          as |cuisine|}}
          {{cuisine.title}}
        {{/power-select-multiple}}
      </div>

      <div class="panel-body">
        {{! @TODO filter name }}
      </div>

      <div class="panel-footer">
        {{! @TODO button clear }}
      </div>
    </div>
  </div>

  <div class="col-md-8">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title">{{t 'foundRestaurants' count=filteredRestaurants.length}}</h3>
      </div>

      <div class="panel-body">
        {{#each filteredRestaurants as |restaurant| }}
          <div class="media">
            <a class="media-left">
              <img class="media-object" src={{restaurant-img restaurant.id}} width="64" height="64">
            </a>
            <div class="media-body">
              <h4 class="media-heading">
                {{restaurant.name}} <span class="label label-info">{{restaurant.cuisine}}</span>
              </h4>
              {{restaurant.description}}
            </div>
          </div>
        {{/each}}
      </div>
    </div>
  </div>
</div>

Utilizamos el selector múltiple del addon ember-power-select

Mostramos los restaurantes filtrados en lugar del modelo

Utilizamos el helper action combinado con el helper mut :

El componente invocaría la acción con el nuevo valor seleccionado. cambia el valor selectedCuisins a lo que se proporcionó como argumento de acción.

Ejercicio por parejas

  1. Instala el addon  ember-cli-star-rating.
  2. Muestra en el listado de restaurantes la valoración de cada uno de ellos. y añade un filtro para buscar los restaurantes por su valoración.

NESTED ROUTES

Genera rutas anidadas a partir de la ruta restaurants.

- restaurants/index mostrará todos los restaurantes

 

Si abres el router (app/router) nada se ha actualizado. Esto es porque funciona de forma similar a la ruta del índice base. Es la ruta predeterminada que se procesa cuando no se proporciona ninguna ruta.

- restaurants/detail mostrará el detalle de un restaurante.

 

Para indicarle a la aplicación a qué restaurante queremos acceder, necesitamos reemplazar el path de la ruta detail con la ID del restaurante.

 

Router.map(function() {
  ...
  this.route('restaurants', function() {
    this.route('detail', { path: 'detail/:id' });
  });
});

  $ ember g route restaurants/index

  $ ember g route restaurants/detail

http://localhost:4200/restaurants/detail/restaurant-id

Nested Index Route

 - Mueve también la plantilla de la ruta principal a la plantilla de la ruta restaurants. Deja la ruta principal con un outlet.

 - Elimina el controlador padre y genera el controlador de la ruta hija con lo que tenía el padre

  $ ember g controller restaurants/index

 - Si no definimos modelo, la ruta anidada hereda el modelo de la ruta padre.

Nested detail Route

 Genera el modelo buscando por id:

Añade el restaurante a la plantilla:

// app/routes/restaurants/detail.js
import Ember from 'ember';

export default Ember.Route.extend({

	model({id}){
		const parentModel = this.modelFor('restaurants') || [];
		return parentModel.findBy('id', id);
	}
});
{{! app/templates/restaurants/detail.hbs }}
{{model.description}}

Comprueba el resultado abriendo en el navegador:

http://localhost:4200/#/restaurants/esthers

Nested detail Route

Plantilla con estilo para la ruta de detalle:

<div class="row-fluid">
   <div class="page-header">
      <h1>{{model.name}}</h1>
   </div>

   <div class="col-md-3">
      <img src="{{restaurant-img model.id}}" width="200" height="200" class="img-responsive" alt="Generic placeholder thumbnail">
      <h4><i class="glyphicon glyphicon-star"></i> Rating: {{model.rating}}</h4>
      <h4><i class="glyphicon glyphicon-usd"></i> Price: {{model.price}}</h4>
      <span class="text-muted"><i class="glyphicon glyphicon-tag"></i> {{model.cuisine}}</span>
   </div>

   <div class="col-md-9">
      <h3>{{t 'description'}}</h3>
      {{model.description}}

      <h3>{{t 'location'}}</h3>
      {{model.location}}

      <h3>{{t 'openingHours'}}</h3>
      <ul>
         <li>Opens/Closes: {{model.opens}} - {{model.closes}}</li>
         <li>
            Days opened:
            <ul>
               {{#each model.days as |day|}}
                  <li>{{capitalize (t (concat 'weekDays.' (weekday day)))}}</li>
               {{/each}}
            </ul>
         </li>
      </ul>

      <h3>{{t 'menu'}}</h3>
      <ul>
         {{#each model.menuItems as |item|}}
            <li>{{capitalize item.name}} <span class="text-muted">{{item.price}} €</span></li>
         {{/each}}
      </ul>
   </div>
</div>
import Ember from 'ember';

export function capitalize([input] /*, hash*/ ) {
    let r = input;

    if (typeof input === 'string') {
        r = input.capitalize();
    }

    if (typeof input === 'object' && typeof input.toString === 'function') {
        r = input.toString().capitalize();
    }

    return Ember.String.htmlSafe(r);
}

export default Ember.Helper.helper(capitalize);

Enlazando a un restaurante

Utiliza link-to para ir a la ruta de detalle pasando como parámetro el restaurante (así cogerá el id).

// MODIFICA app/templates/restaurants/index.hbs
...
{{#each filteredRestaurants as |restaurant| }}
   <div class="media">
      <a class="media-left">
         <img class="media-object" src={{restaurant-img restaurant.id}} width="64" height="64">
      </a>
      <div class="media-body">
         <h4 class="media-heading">
            {{link-to restaurant.name 'restaurants.detail' restaurant}}
            <span class="label label-info">{{capitalize restaurant.cuisine}}</span>
         </h4>
         {{restaurant.description}}
      </div>
   </div>
{{/each}}
...

Helpers de ember (3)

- InputHelpers:  textarea +  input son la forma más fácil de crear controles de formulario comunes.

Utilizándolos, puedes crear estos elementos con declaraciones casi idénticas a cómo se crearía un <input> o <textarea> tradicional.

 

https://guides.emberjs.com/v2.10.0/templates/input-helpers/

 

https://ember-twiddle.com/a67797d151288c1c0324f4da7289ccc4

 h

 

Ejercicio por parejas

  1. Añade un input en el panel del buscardor para que filtre buscando por nombre

¿Dudas?

 

juanantonio.gomez@beeva.com

 

beatriz.demiguel@beeva.com

¡¡GRACIAS!!

Workshop EmberJS

By Beatriz de Miguel Pérez