Text
Juanan
juanantonio.gomez@beeva.com
Bea
beatriz.demiguel@beeva.com
$ ember new foodme
Si clonas el repo de ejemplo no tendrás que hacerlo, pero recuérdalo para tu próximo proyecto :)
$ npm install -g ember-cli
$ cd foodme
$ npm install
> Recomendación del chef: <
instala Ember Inspector
Templates
HTML + Handlebars = HTMLBars
Models
Manejo y/o persistencia de datos
Routes
Puntos de entrada de la app.
Definen los distintos estados.
./foodme$ ember serve
$ ember generate route restaurants
// app/routes/restaurants.js
export default Route.extend({ });
// app/router.js Router.map(function() {
this.route('restaurants'); });
// app/templates/restaurants.hbs
{{outlet}}
ember-cli añade la ruta 'restaurants' al router:
y crea dos ficheros:
la ruta
la plantilla
// app/routes/restaurants.js
import Route from '@ember/routing/route'; export default Route.extend({ model() { return { foo: 'bar' }
}
});
// app/templates/restaurants.hbs
<h1>{{model.foo}}</h1>
// RESULT
<h1>bar</h1>
Para evaluar variables en plantilla usamos llaves
{{ someVariable }}
// app/routes/restaurants.js
import Route from '@ember/routing/route';
import $ from 'jquery';
const host = 'https://raw.githubusercontent.com/shokmaster/ember-workshops-3/master/';
export default Route.extend({
model: function() {
return $.getJSON(`${host}resources/restaurants.json`);
}
});
// 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}}
Tenemos jquery disponible
Es la plantilla por defecto, y se renderiza en todas las rutas
<h1>Welcome to Ember</h1>
{{outlet}} <-- ~ "Lo que tengas, ponlo aquí"
// 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>Nos ocuparemos de las imágenes más adelante
Vamos a añadir un menú de navegación a nuestra web
Utilizaremos el HTML de Bootstrap:
Application Controller
nav-menu Component
Datos:
Acciones:
$ ember g component nav-menu
$ ember g controller application
2. Creamos el application controller:
1. Creamos el componente nav-menu:
// En app/controllers/application.js definimos los datos que le pasaremos al componente nav-menu:
// * Nombre de nuestra aplicación
// * Lista de enlaces para el menú
import Controller from '@ember/controller';
export default Controller.extend({
appName: 'FoodMe',
menuLinks: [{
name: 'Restaurantes',
url: 'restaurants'
}]
});
3. Rellenamos el application controller:
// En app/components/nav-menu.js
// * Variable isMenuOpened para controlar el estado abierto/cerrado
import Component from '@ember/component';
export default Component.extend({
isMenuOpened: false,
actions: {
toggleMenu(){
this.toggleProperty('isMenuOpened');
}
}
});4. 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(){..},
..
}
// 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>5. 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'}}
// En app/templates/application.hbs añadimos el componente y le pasamos
// las propiedades que definimos en el controller/application.js
{{nav-menu
appName=appName
menuLinks=menuLinks}}
<div class="container-fluid">
{{outlet}}
</div>6. Añadimos el componente al template application:
Y este es el resultado:
$ 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:
Pista: el parámetro que recibirá nuestro helper es la id del restaurante
Y este es el resultado:
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
Consulta los addons disponibles en http://emberobserver.com/
$ ember install ember-i18n
Documentación de este addon:
El helper t sirve para traducir textos. Recibe como parámetro una key, busca su traducción y la devuelve.
this.get('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 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: inject()
- Accede al servicio y setea el idioma:
const i18nService = this.get('i18n');
i18nService.set('locale', 'es');
$ 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/en/translations.js
resources/translations/es/translations.js
en:
app/locales/en/translations.js
app/locales/es/translations.js
// 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}}
...
Crea un helper llamado weekday que reciba un
número y devuelva el dia de la semana. Por ejemplo:
{{weekday 3}} // pinta 'wednesday'
//MODIFICA app/components/nav-menu.js
import Component from '@ember/component';
import { inject } from '@ember/service';
import { observer } from '@ember/object';
const languages = [{
name: 'Español',
code: 'es'
}, {
name: 'English',
code: 'en'
}];
export default Component.extend({
...
i18n: inject(),
languages,
selectedLanguage: languages[1],
onSelectedLanguageChange: observer('selectedLanguage', function() {
this.set('i18n.locale', this.get('selectedLanguage.code'));
}),
...
});
Definimos el array de idiomas
Definimos una propiedad para saber el idioma seleccionado y la inicializamos
Definimos un observer que cambiará el idioma cuando el selector cambie
- 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))
}}$ 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 pone "@TODO cambio de idioma"
<ul class="nav navbar-nav navbar-right">
<li>
{{#power-select
selected=selectedLanguage
options=languages
class='dropdown'
onchange=(action (mut selectedLanguage))
as |language|}}
{{language.name}}
{{/power-select}}
</li>
</ul>Y este es el resultado:
$ ember g helper-test weekday --test-type unit
Creamos un tet unitario para el helper 'weekday'
Documentación oficial:
import { module, test } from 'qunit';
import { weekday, names } from 'foodme/helpers/weekday';
module('Unit | Helper | weekday', function() {
test('it returns the day number', async function(assert) {
...
});
test('it returns an empty string when receives an incorrect day number', async function(assert) {
...
});
});
Cambiamos a la rama '06-filter-restaurants':
$ git fetch --all
$ git checkout 06-filter-restaurants
Las propiedades computadas permiten declarar funciones como propiedades. Se declaran llamando a Ember.computed.
import EmberObject, { computed } from '@ember/object';
let Person = EmberObject.extend({
firstName: null,
lastName: null,
fullName: 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'Argumentos que recibe:
- Las propiedades de las que depende: si cambian, se ejecuta la función.
- La función que retorna el valor deseado: sólo se llama una vez y el resultado se almacena en caché hasta que las propiedades dependientes cambien.
- 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:
{{transition-to 'my-route' (query-params foo='bar')}}
equivale a: this.transitionTo('myRoute', { queryParams: { foo: 'bar'}})
$ ember g controller restaurants
import Controller from '@ember/controller';
import { A } from '@ember/array';
import { computed } from '@ember/object';
import { isPresent } from '@ember/utils';
export default Controller.extend({
cuisineOptions: computed('model', function() {
return this.get('model').mapBy('cuisine').filter((elem, pos, arr) => arr.indexOf(elem) === pos).map((cuisine) => {
return {
name: cuisine,
title: cuisine.replace('/', ' / ').capitalize()
};
});
}),
// Filter criterias
filterCuisins: A(),
// Filter function
filteredRestaurants: computed('model', 'filterCuisins', function() {
let filteredRestaurants = this.get('model');
const filterCuisins = this.get('filterCuisins');
// Filter by cuisine
if (isPresent(filterCuisins)) {
filteredRestaurants = filteredRestaurants.filter((item) =>
filterCuisins.mapBy('name').includes(item.cuisine)
);
}
return filteredRestaurants;
})
});Definimos una propiedad que genera 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
{{! 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=cuisineOptions
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.
Vamos a generar rutas anidadas a partir de la ruta 'restaurants'.
- restaurants/index mostrará todos los restaurantes:
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/[:id]Si abrimos el router (app/router) nada ha cambiado. Esto es porque 'index' funciona como ruta predeterminada y, si no tenemos nada que añadir al comportamiento por defecto, no hace falta definirla.
- 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.
Ver esquema aquí
- 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.
Genera el modelo buscando por id:
Añade el restaurante a la plantilla:
// app/routes/restaurants/detail.js
import Route from '@ember/routing/route';
export default 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:
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 { helper } from '@ember/component/helper';
import { htmlSafe } from '@ember/template';
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 htmlSafe(r);
}
export default helper(capitalize);Utiliza link-to para ir a la ruta 'detail', pasando como parámetro el restaurante. Ember extraerá automáticamente la popiedad '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}}
...Son la forma más fácil de crear controles de formulario comunes. Con ellos podemos crear estos elementos con una sintáxis casi idéntica al HTML tradicional.
La docu:
https://guides.emberjs.com/release/templates/input-helpers/
Un ejemplo básico de input con binding de propiedades:
https://ember-twiddle.com/a67797d151288c1c0324f4da7289ccc4
h
Añade un input en el panel del buscador para que filtre buscando por nombre
Añade un botón en el panel del buscador para reiniciar los filtros de búsqueda
Son la forma más fácil de crear controles de formulario comunes. Con ellos podemos crear estos elementos con una sintáxis casi idéntica al HTML tradicional.
La docu:
https://guides.emberjs.com/release/templates/input-helpers/
Un ejemplo básico de input con binding de propiedades:
https://ember-twiddle.com/a67797d151288c1c0324f4da7289ccc4
h
Añade un botón a cada restaurante para buscarlo en Google Maps:
Pista: click aquí para buscar la documentación técnica
Oops! no tenemos controller para 'detail'. ¿Crees que es necesario crearlo?
Son test a alto nivel, y comprueban una funcionalidad desde el punto de vista de un usuario.
$ ember g acceptance-test restaurants
// tests/acceptance/restaurants-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
module('Acceptance | restaurants', function(hooks) {
setupApplicationTest(hooks);
test('visiting /restaurants', async function(assert) {
await visit('/restaurants');
assert.equal(currentURL(), '/restaurants');
});
});
ember-cli nos crea un test básico
Vamos a definir qué tiene que hacer nuestra funcionalidad
import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
module('Acceptance | restaurants', function(hooks) {
setupApplicationTest(hooks);
test('visiting /restaurants', async function(assert) {
await visit('/restaurants');
assert.equal(currentURL(), '/restaurants');
});
test('should link to information about the company.', async function (assert) {
});
test('should show restaurants list at home page', async function(assert) {
});
test('should filter the list of restaurants by name.', async function (assert) {
});
test('should transition to the selected restaurant details', async function (assert) {
});
});
Cosas que necesitarás:
Ahora tienes que implementarlos ;)
this.element.querySelector('SELECTOR CSS')
this.element.querySelectorAll('SELECTOR CSS')
assert.ok() // 1 arg (evalúa true/false)
assert.equal() // 2 args (evalúa si son iguales)
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
juanantonio.gomez@beeva.com
beatriz.demiguel@beeva.com