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:
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:
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:
//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
- Instala el addon ember-cli-star-rating.
- 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-idNested 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:
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
- 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
Workshop EmberJS
- 1,535
