Nothing Special   »   [go: up one dir, main page]

Discover Meteor - Building Real-Time JavaScript Web Apps - Sacha Greif & Tom Coleman

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 325

DISCOVER METEOR

Building Real-Time JavaScript Web Apps


Versin 1.9 (updated 27 de junio de 2015)

Tom Coleman & Sacha Greif


Cover photo credit: Perseid Hunting by Darren Blackburn, licensed under a Creative Commons
Attribution 2.0 Generic license.

www.discovermeteor.com

Introduccin

Hagamos un pequeo experimento mental. Imaginemos que abrimos dos ventanas del explorador de
archivos de nuestro ordenador mostrando la misma carpeta.
Ahora borramos un archivo en una de las dos ventanas. Habr desaparecido en la otra?
Cuando modificamos algo en nuestro sistema de archivos local, el cambio se aplica en todas partes
sin necesidad de refrescos o callbacks. Simplemente sucede.
Ahora, vamos a pensar qu pasara en la web en esta misma situacin. Por ejemplo, digamos que
abrimos el mismo WordPress en dos ventanas del navegador y creamos un post en una de ellas. A
diferencia del escritorio, la otra ventana no reflejar el cambio a menos que la recargues.
Nos hemos acostumbrado a la idea de que un sitio web es algo con lo que solo te comunicas como a
rfagas separadas.
Meteor es parte de una nueva ola de frameworks y tecnologas que buscan desafiar el statu quo
haciendo webs reactivas y en tiempo real.

Qu es Meteor?
Meteor es una plataforma para crear aplicaciones web en tiempo real construida sobre Node.js.
Meteor se localiza entre la base de datos de la aplicacin y su interfaz de usuario y se encarga que las
dos partes estn sincronizadas.
Como Meteor usa Node.js, se utiliza JavaScript en el cliente y en el servidor. Y ms an, Meteor es
capaz de compartir cdigo entre ambos entornos.
El resultado es una plataforma muy potente y muy sencilla ya que Meteor abstrae muchas de las
molestias y dificultades que nos encontramos habitualmente en el desarrollo de aplicaciones web.

Por qu Meteor?
Por qu dedicar tiempo a aprender Meteor en lugar de cualquier otro framework web? Dejando a un
lado las caractersticas de Meteor, creemos que todo se reduce a una sola cosa: Meteor es full stack y
es fcil de aprender.
Meteor permite crear una aplicacin web en tiempo real en cuestin de horas. Y si ya hemos hecho
desarrollo web, estaremos familiarizados con JavaScript, y ni siquiera tendremos que aprender un
nuevo lenguaje.
Meteor podra ser la plataforma ideal para nuestras necesidades, o, quizs no. Pero por qu no
probarlo y descubrirlo por nosotros mismos?

Por qu este libro?


Durante los ltimos aos, hemos estado trabajando en numerosos proyectos con Meteor, desde
aplicaciones web hasta aplicaciones mviles, y desde proyectos comerciales hasta proyectos de
cdigo abierto.
Hemos aprendido un montn, pero no siempre ha sido fcil encontrar respuestas a todas nuestras
preguntas. Tuvimos que encajar piezas de muchas fuentes diferentes, y en muchos casos incluso
inventamos nuestras propias soluciones. Con este libro, queremos compartir todas estas lecciones, y
crear una sencilla gua para construir una aplicacin desde cero con Meteor.
La aplicacin que construiremos es una versin simplificada de una red social como Hacker News o
Reddit, a la que llamaremos Microscope (por analoga con su hermana mayor, la aplicacin de cdigo
abierto, Telescope). Durante su construccin, veremos todos los elementos que intervienen en una
aplicacin desarrollada con Meteor, tales como cuentas de usuario, colecciones, enrutamiento, y
mucho ms.

Para quin es este libro?

Uno de nuestros objetivos al escribir este libro es mantener las cosas accesibles y fciles de entender,
as que cualquiera debera ser capaz de seguirlo, aunque no tenga experiencia con Meteor, Node.js, o
frameworks MVC, o incluso con la programacin en general en el lado del servidor.
Por otro lado, se asume cierta familiaridad con los conceptos y la sintaxis bsica de JavaScript. Si
alguna vez has hackeado algo de cdigo jQuery o jugado un poco con la consola de desarrollo del
navegador, vers como no tendrs problemas en seguirlo.
Si an no te sientes cmodo usando JavaScript, te sugerimos que eches un vistazo a nuestra entrada
JavaScript primer for Meteor de nuestro blog, antes de seguir con el libro.

Sobre los autores


Si te ests preguntando quienes somos y por qu deberas confiar en nosotros, a continuacin tienes
algo ms de informacin sobre nosotros dos.
Tom Coleman forma parte de Percolate Studio, una tienda de desarrollo web centrada en la calidad
y la experiencia de usuario. Adems, es uno de los mantenedores del repositorio de paquetes
Atmosphere, y est detrs de otros proyectos dentro de Meteor (como el Iron Router).
Sacha Greif ha trabajado como diseador en startups como Hipmunk y Ruby Motion. Es el creador
de Telescope y Sidebar (basada en Telescope), y es tambin el fundador de Folyo.

Captulos y barras laterales


Para que este libro sea de utilidad tanto para el principiante como para el programador avanzado, sus
captulos estn divididos en dos categoras: los captulos normales (numerados del 1 al 14) y las
barras laterales o sidebars (nmeros .5).
Los captulos normales son la gua para construir la aplicacin, y su objetivo es conseguir que
funcione de la forma ms rpida posible, explicando los pasos ms importantes sin entrar en
demasiados detalles.

Por otro lado, las barras laterales profundizan en los entresijos de Meteor, y nos ayudarn a
comprender mejor lo que realmente ocurre entre bastidores.
As que, si nos consideramos principiantes, deberamos de saltarnos las barras laterales en una
primera lectura, y volver a ellas ms tarde una vez que hayamos jugado un poco con Meteor.

Commits e instancias on-line


No hay nada peor que estar siguiendo un libro de programacin y de repente darnos cuenta de que
nuestro cdigo se ha roto y que nada funciona como debera.
Para evitarlo, hemos creado un repositorio en GitHub para Microscope, ofreciendo enlaces a
commits de git cada pocos cambios de cdigo. Adems, cada commit se enlaza con una instancia online de la aplicacin, por lo que se puede comparar con nuestra copia local. He aqu un ejemplo de lo
que podrs ver:

Commit 11-2
Mostrar las notificaciones en la cabecera.
Ver en GitHub

Lanzar instancia

Solo una cosa, ten en cuenta que el hecho de que ofrezcamos estos commits, no significa que tengas
que ir de un checkout al siguiente. Aprenders mucho ms si dedicas el tiempo necesario a escribir el
cdigo de tu aplicacin!

Otros recursos
Si quieres aprender ms acerca de un aspecto particular de Meteor, la documentacin oficial de
Meteor es el mejor sitio al que ir para empezar.

Tambin te recomendamos Stack Overflow para solucionar problemas y dudas, y el canal IRC
#meteor si necesitas ayuda directa.

Necesito Git?
Estar familiarizado con el control de versiones Git no es estrictamente necesario para seguir
este libro, pero lo recomendamos encarecidamente.
Si quieres ponerte al da, te recomendamos Git Is Simpler Than You Think de Nick Farina.
Si eres principiante, tambin te recomendamos la app GitHub for Mac, que te permite
administrar repositorios sin utilizar la lnea de comandos. O SourceTree (Mac OS &
Windows), los dos gratuitos.

Contacto
Si deseas ponerte en contacto con nosotros, puedes enviarnos un correo electrnico a
hello@discovermeteor.com.
Adems, si encuentras un error tipogrfico o cualquier otro error en el contenido del libro,
puedes reportarlo en este repositorio de GitHub.
Si encuentras un problema en el cdigo de Microscope, puedes enviarlo al repositorio de
Microscope.
Por ltimo, para cualquier otra pregunta, puedes dejarnos un comentario en el panel lateral de
esta aplicacin.

Empezando

Las primeras impresiones son las que cuentan. La instalacin de Meteor debera ser muy sencilla y, en
la mayora de los casos, slo cuesta 5 minutos ponerlo en marcha.
Para empezar, si estamos usando Mac OS o GNU/Linux, podemos instalar Meteor con el siguiente
comando desde la consola:

curl https://install.meteor.com | sh

Si ests usando Windows, echa un vistazo a la guia oficial de instalacin: install instructions en la
web de Meteor.
Se instalar el ejecutable meteor en nuestro sistema y lo dejar listo para empezar a usar Meteor.

Sin instalar Meteor


Si no podemos (o no queremos) instalar Meteor de forma local, recomendamos usar
Nitrous.io.
Nitrous.io es un servicio que te permite ejecutar aplicaciones y editar el cdigo directamente
en tu navegador, y hemos escrito una breve gua para ayudarte a ponerte en marcha.
Slo tienes que seguir esta gua hasta completar la seccin Installing Meteor, y luego
seguir con este captulo a partir de la seccin Crear y ejecutar una aplicacin.

Creando una simple aplicacin


Ahora que tenemos instalado Meteor, vamos a crear nuestra aplicacin. Para ello, utilizaremos la

herramienta de lnea de comandos meteor :

meteor create microscope

Este comando crea un proyecto bsico listo para usar. Cuando termina, deberamos ver un directorio
llamado microscope/ , que contiene lo siguiente:

.meteor
microscope.css
microscope.html
microscope.js

La aplicacin que se ha creado es una aplicacin bsica que demuestra slo algunas sencillas pautas.
A pesar de que nuestra aplicacin no hace casi nada, ya podemos ejecutarla. Para hacerlo, volvemos
al terminal y escribimos:

cd microscope
meteor

Ahora abrimos http://localhost:3000/ (o su equivalente http://0.0.0.0:3000/ ) en el


navegador y deberamos ver algo como esto:

Hello World de Meteor

Commit 2-1
Un proyecto bsico.
Ver en GitHub

Lanzar instancia

Enhorabuena! ya tenemos nuestra primera aplicacin Meteor funcionando. Por cierto, para parar la
aplicacin, todo lo que hay que hacer es abrir la pestaa terminal donde se ejecuta y pulsar ctrl+c .
Si ests utilizando Git, este sera un buen momento para iniciar el repositorio con git init .

Bye Bye Meteorite


Hubo un tiempo en el que Meteor utilizaba un gestor de paquetes externo llamado Meteorite.
Desde la versin 0.9.0 de Meteor, Meteorite ya no es necesario, ya que sus caractersticas se
han incorporado a Meteor.
As que si encuentras referencias a mrt de Meteorite a lo largo de este libro o mientras lees
material relacionado con Meteor, puedes reemplazarlo con total seguridad por el habitual
meteor

Aadir un paquete
Ahora vamos a usar el sistema de paquetes de Meteor para incluir el framework Bootstrap en nuestro
proyecto:
Esto no es distinto de aadir Bootstrap de la forma habitual, incluyendo manualmente los ficheros
CSS y JavaScript, excepto en que confiamos en el mantenedor del paquete para que lo mantenga
actualizado para nosotros.
Ya que estamos, aadiremos tambin el paquete Underscore. Underscore es una librera de utilidades
JavaScript, y es muy til cuando necesitemos manipular estructuras de datos.
El paquete bootstrap lo mantiene el usuario twbs , por lo que el nombre completo del paquete es
twbs:bootstrap

El paquete underscore forma parte de los paquetes oficiales incluidos en Meteor, lo que quiere
decir que no hay que incluir el nombre del autor:

meteor add twbs:bootstrap


meteor add underscore

Fjate que estamos aadiendo Bootstrap 3. Algunas de las capturas de este libro estn tomadas de
una versin antigua de Microscope con Bootstrap 2, por lo que podran parecer ligeramente
diferentes.

Commit 2-2
Aadido el paquete bootstrap.
Ver en GitHub

Lanzar instancia

Tan pronto como agregues el paquete Bootstrap, deberas notar un cambio en el aspecto de nuestra
aplicacin:

Con Boostrap.

Al contrario de la forma tradicional en la que incluimos recursos externos, no tenemos que agregar
enlaces a ningn fichero CSS o JavaScript, porque Meteor lo hace por nosotros! Esta es slo una de

las muchas ventajas de los paquetes Meteor.

Una nota sobre los paquetes


Al hablar acerca de los paquetes en el contexto de Meteor, vale la pena ser especfico. Meteor
puede usar cinco tipos bsicos de paquetes:
El mismo ncleo de Meteor est dividido en diferentes paquetes de la plataforma
Meteor. Estn incluidos en cada app y probablemente nunca tengas que preocuparte
por ellos.
Los paquetes ordinarios se conocen como isopacks, o paquetes isomrficos
(paquetes que funcionan en los dos lados, cliente y servidor). Los paquetes Firstparty, como accounts-ui o appcache , estn mantenidos por los desarrolladores de
Meteor y vienen incluidos en Meteor.
Los paquetes de terceros son paquetes isopacks que otros usuarios han subido al
servidor de paquetes de Meteor. Puedes echarles un vistazo en Atmosphere o con el
comando meteor search .
Los paquetes locales son paquetes personalizados que puedes crear tu mismo y
colocarlos en el directorio /packages .
Los paquetes NPM (Node.js Packaged Modules) son paquetes de Node.js. A pesar de
que no funcionan por defecto con Meteor, pueden ser utilizados por los tipos de
paquete anteriores.

La estructura de una aplicacin Meteor


Antes de empezar a escribir cdigo debemos estructurar de forma adecuada nuestro proyecto. Para
asegurarte de que dispones de un entorno limpio y claro, abre el directorio microscope y borra los
archivos microscope.html , microscope.js , y microscope.css .
A continuacin, crea cuatro directorios dentro de /microscope : /client , /server , /public y
/lib

Ahora, crearemos dos archivos vacos main.html y main.js dentro de /client . Por ahora, no te
preocupes si esto rompe completamente la app, empezaremos a rellenar los nuevos ficheros en el
siguiente captulo.
Debemos mencionar que algunos de los directorios que hemos creado son especiales y Meteor tiene
reglas para ellos:
El cdigo de /server se ejecuta en el servidor.
El cdigo de /client se ejecuta en el cliente.
Todo lo dems se ejecuta en las dos partes, cliente y servidor.
Las cosas estticas (fuentes, imgenes, etc.) van en el directorio /public .
Y tambin es til saber como Meteor decide en que orden cargan los ficheros:
Los archivos de /lib se cargan antes que nada.
Los archivos con nombre main.* se cargan despus que todos los dems.
Todo se carga por orden alfabtico segn el nombre del fichero.
Ten en cuenta que aunque Meteor tiene todas estas reglas, en realidad no nos obliga a utilizar una
estructura de archivos predefinida. As que la estructura que sugerimos es slo nuestra forma de
hacer las cosas, no son reglas inamovibles.
Os animamos a echar un vistazo a la documentacin oficial Meteor para conocer ms detalles acerca
de la estructura de las aplicaciones.

Meteor es MVC?
Si hemos usado otros frameworks, como Ruby on Rails, puede que nos preguntemos si las
aplicaciones de Meteor adoptan el patrn MVC (Model View Controller).
La respuesta corta es no. A diferencia de Rails, Meteor no impone ninguna estructura
predefinida para su aplicacin. As que en este libro vamos a exponer cdigo de la forma que
ms sentido tenga para nosotros, sin preocuparnos demasiado por las siglas.

Pblico?
Bueno, mentimos. En realidad no vamos a necesitar public/ por la sencilla razn de que Microscope
no utiliza ningn archivo esttico. Pero como en la mayora de aplicaciones se van a incluir al menos
un par de imgenes, pensamos que era importante incluirlo.
Por cierto, te puedes haber dado cuenta de que se ha creado un directorio oculto llamado .meteor .
Aqu es donde Meteor almacena su propio cdigo. Cambiar cosas aqu dentro es, en general, una muy
mala idea con las nicas excepciones de los archivos .meteor/packages y .meteor/release , que se
utilizan, respectivamente, para listar nuestros paquetes y para establecer la versin de Meteor que
queremos utilizar.

Underscores vs CamelCases
Lo nico que vamos a decir sobre el viejo debate del guin bajo ( my_variable ) contra el
camelCase ( myVariable ) es que en realidad no importa el que elijas, siempre y cuando lo
adoptes en todo el proyecto.
En este libro, utilizamos camelCase porque es la forma habitual en JavaScript (despus de
todo, es JavaScript no java_script!).
Las nicas excepciones a esta regla son los nombres de los archivos, para los que se van a
utilizar guiones bajos ( my_file.js ), y las clases CSS, para las que usaremos guiones ( .myclass

). La razn es que en el sistema de archivos, el subrayado es lo ms comn, mientras

que en la propia sintaxis, CSS ya utiliza guiones ( font-family , text-align , etc).

El CSS de nuestra aplicacin


Este libro no trata sobre CSS. As que para evitar entrar en detalles de estilo, hemos decidido que la
hoja de estilos est disponible desde el principio, as, no ser necesario preocuparse por ella nunca
ms.
Meteor carga el CSS minimizado y de forma automtica, por lo que, a diferencia de otros recursos
estticos, va en /client , no en /public . Vamos a crear el archivo
client/stylesheets/style.css

y a aadirle este CSS:

.grid-block, .main, .post, .comments li, .comment-form {


background: #fff;
border-radius: 3px;
padding: 10px;
margin-bottom: 10px;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); }
body {
background: #eee;

color: #666666; }
#main {
position: relative;
}
.page {
position: absolute;
top: 0px;
width: 100%;
}
.navbar {
margin-bottom: 10px; }
/* line 32, ../sass/style.scss */
.navbar .navbar-inner {
border-radius: 0px 0px 3px 3px; }
#spinner {
height: 300px; }
.post {
/* For modern browsers */
/* For IE 6/7 (trigger hasLayout) */
*zoom: 1;
position: relative;
opacity: 1; }
.post:before, .post:after {
content: "";
display: table; }
.post:after {
clear: both; }
.post.invisible {
opacity: 0; }
.post.instant {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none; }
.post.animate{
-webkit-transition: all 300ms 0ms;
-moz-transition: all 300ms 0ms ease-in;
-o-transition: all 300ms 0ms ease-in;
transition: all 300ms 0ms ease-in; }
.post .upvote {
display: block;
margin: 7px 12px 0 0;
float: left; }
.post .post-content {

float: left; }
.post .post-content h3 {
margin: 0;
line-height: 1.4;
font-size: 18px; }
.post .post-content h3 a {
display: inline-block;
margin-right: 5px; }
.post .post-content h3 span {
font-weight: normal;
font-size: 14px;
display: inline-block;
color: #aaaaaa; }
.post .post-content p {
margin: 0; }
.post .discuss {
display: block;
float: right;
margin-top: 7px; }
.comments {
list-style-type: none;
margin: 0; }
.comments li h4 {
font-size: 16px;
margin: 0; }
.comments li h4 .date {
font-size: 12px;
font-weight: normal; }
.comments li h4 a {
font-size: 12px; }
.comments li p:last-child {
margin-bottom: 0; }
.dropdown-menu span {
display: block;
padding: 3px 20px;
clear: both;
line-height: 20px;
color: #bbb;
white-space: nowrap; }
.load-more {
display: block;
border-radius: 3px;
background: rgba(0, 0, 0, 0.05);
text-align: center;
height: 60px;

line-height: 60px;
margin-bottom: 10px; }
.load-more:hover {
text-decoration: none;
background: rgba(0, 0, 0, 0.1); }
.posts .spinner-container{
position: relative;
height: 100px;
}
.jumbotron{
text-align: center;
}
.jumbotron h2{
font-size: 60px;
font-weight: 100;
}
@-webkit-keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
.errors{
position: fixed;
z-index: 10000;
padding: 10px;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
pointer-events: none;
}
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
-webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards;
-moz-animation: fadeOut 2700ms ease-in 0s 1 forwards;
width: 250px;

float: right;
clear: both;
margin-bottom: 5px;
pointer-events: auto;
}

client/stylesheets/style.css

Commit 2-3
Estructura de ficheros reorganizada.
Ver en GitHub

Lanzar instancia

Una nota sobre CoeeScript


En este libro vamos usar JavaScript puro. Pero si quisiramos usar CoeeScript, bastara con
aadir el paquete CoeeScript y estaramos listos para continuar:
meteor add coffeescript

Despliegue

SIDEBAR

2.5

A algunos les gusta trabajar silenciosamente en un proyecto hasta que queda perfecto. Otros quieren
mostrarlo al mundo lo ms pronto posible.
Si eres de los primeros y prefieres desarrollar a nivel local, no dudes en saltarte este captulo. Pero si
prefieres aprender a desplegar tu aplicacin Meteor en la Web, ahora te explicamos cmo hacerlo.
Vamos a aprender a desplegar una aplicacin Meteor de diferentes formas. Eres libre de utilizar
cualquiera de ellas en cualquier etapa del desarrollo, ya sea trabajando en Microscope o en otra
aplicacin. Vamos a empezar!

Introduciendo las barras laterales.


Este captulo es una barra lateral (sidebar). En estos captulos se adopta una mirada ms
profunda a temas ms generales sobre Meteor, de forma independiente al resto del libro.
As que si prefieres seguir construyendo Microscope, puedes saltarse estos captulos y volver
a ellos ms tarde.

Despliegue de aplicaciones en Meteor.com


Desplegar en un subdominio de Meteor (por ejemplo http://myapp.meteor.com ) es la opcin ms
sencilla, y ser lo primero que probaremos. Es muy til para mostrar la aplicacin durante las
primeras etapas del desarrollo o para configurar rpidamente un servidor de prueba.
Desplegar en Meteor es muy simple. Solo tienes que abrir el terminal, ir al directorio de la aplicacin y
escribir:

meteor deploy myapp.meteor.com

Por supuesto que tienes que tener cuidado de reemplazar myapp con un nombre de tu eleccin, y
preferiblemente uno que no est en uso.
Si es la primera vez que despliegas una aplicacin, te pedir crear una cuenta en Meteor. Y si todo va
bien, despus de unos segundos podrs acceder a la aplicacin desde http://myapp.meteor.com .
Puedes mirar la documentacin oficial para obtener ms informacin sobre cosas como el acceso a
la base de datos de la instancia, o cmo configurar un dominio personalizado.

Despliegue de aplicaciones en Modulus


Modulus es una gran opcin para desplegar aplicaciones basadas en Node.js. Es uno de los pocos
proveedores de PaaS (plataforma-como-servicio) que apoyan oficialmente a Meteor, y ya hay un buen
nmero de gente corriendo aplicaciones en produccin all.
Puedes aprender ms sobre Modulus, leyendo su gua de despliegue de aplicaciones Meteor:
deployment guide for Meteor apps.

Meteor Up
Aunque todos los das aparecen nuevas soluciones en la nube, a menudo vienen con su propia cuota
de problemas y limitaciones. As que, actualmente, el despliegue en tu propio servidor sigue siendo la
mejor manera de poner una aplicacin Meteor en produccin. El problema es que, hacerlo uno mismo
no es tan sencillo, especialmente si lo que ests buscando es un despliegue de calidad.
Meteor Up (o mup , para abreviar) es un intento de solucionar este problema con una utilidad de lnea
de comandos que se encarga por nosotros de la instalacin y el despliegue. As que veamos cmo
desplegar Microscope utilizando Meteor Up.

Antes de nada, vamos a necesitar un servidor. Recomendamos o bien Digital Ocean, desde $5 al mes,
o bien AWS, que proporciona Micro instancias gratuitas (con las que rpidamente tendremos
problemas de escala, aunque deberan ser suficientes si solo buscamos empezar a jugar con Meteor).
Sea cual sea el servicio que elijas, debes obtener tres cosas: la direccin IP del servidor, un inicio de
sesin (normalmente root o ubuntu ), y una contrasea. Guarda estas cosas en un lugar seguro,
pronto las necesitaremos!.

Inicializando Meteor Up
Para empezar, necesitamos instalar Meteor Up va npm :

npm install -g mup

A continuacin, crearemos un directorio especial donde pondremos la configuracin de Meteor Up


para un despliegue en particular. Vamos a utilizar un directorio independiente por dos razones:
primero, porque, por lo general, es la mejor manera de evitar incluir credenciales privadas en tu
repositorio Git, especialmente si ests trabajando en un repositorio pblico.
En segundo lugar, mediante el uso de directorios separados, podremos manejar mltiples
configuraciones en paralelo. Esto ser muy til para, por ejemplo, desplegar instancias de desarrollo y
produccin.
As que vamos a crear este nuevo directorio y lo utilizaremos para iniciar un nuevo proyecto Meteor
Up:

mkdir ~/microscope-deploy
cd ~/microscope-deploy
mup init

Compartir con Dropbox


Una manera de asegurarse de que todo el equipo de desarrollo utilice la misma
configuracin de despliegue es crear la carpeta de configuracin de Meteor Up dentro de
Dropbox, o cualquier servicio similar.

Configuracin de Meteor Up
Cuando inicializamos un nuevo proyecto, Meteor Up crea dos archivos: mup.json y settings.json .

mup.json

contendr los ajustes relacionados con el despliegue, mientras que settings.json

contendr todos los ajustes relacionados con la aplicacin (tokens OAuth, tokens para anlisis, etc.)
El siguiente paso es configurar el archivo mup.json . Aqu est el archivo mup.json que mup init
genera por defecto. Todo lo que hay que hacer es rellenar los espacios en blanco:

//server authentication info


"servers": [{
"host": "hostname",
"username": "root",
"password": "password"
//or pem file (ssh based authentication)
//"pem": "~/.ssh/id_rsa"
}],
//install MongoDB in the server
"setupMongo": true,
//location of app (local directory)
"app": "/path/to/the/app",
//configure environmental
"env": {
"ROOT_URL": "http://supersite.com"
}
}

mup.json

Vamos a repasar cada uno de estos valores.


Autenticacin del servidor
Te habrs dado cuenta de que con Meteor Up puedes usar SSH con usuario y contrasea o una clave
privada (PEM), por lo que lo podemos usar con casi cualquier proveedor de la nube.
Nota importante: si eliges utilizar la autenticacin basada en contrasea, asegrate de instalar
primero sshpass . (Echa un vistazo a esta guia).
Configuracin de MongoDB
El siguiente paso es configurar una base de datos MongoDB. Recomendamos usar Compose o
cualquier otro proveedor en la nube porque ofrecen un apoyo profesional y mejores herramientas de

gestin.
Si has decidido usar Compose, configura setupMongo como false y aade la variable de entorno
MONGO_URL

en un bloque env en mup.json . Si por el contrario decides usar MongoDB en el propio

servidor, configura setupMongo como true y Meteor Up se har cargo de todo.


El path de la aplicacin Meteor
Debido a que la configuracin de Meteor Up reside en un directorio diferente. Mediante la propiedad
app

le decimos cmo llegar hasta nuestra aplicacin. Slo hay que introducir la ruta local completa,

que se puede obtener con el comando pwd de la terminal, cuando estamos dentro del directorio de
la aplicacin.
Environment Variables
Dentro del bloque env podemos especificar todas las variables de entorno de nuestra la aplicacin
(por ejemplo, ROOT_URL , MAIL_URL , MONGO_URL , etc.).

Configuracin y despliegue
Antes de que podamos desplegar, tendremos que configurar el servidor para que est listo para alojar
aplicaciones Meteor. La magia de Meteor Up encapsula este complejo proceso en un solo comando!

mup setup

Esto llevar un tiempo dependiendo del rendimiento del servidor y la conectividad de la red. Despus
de que la instalacin termine correctamente, por fin podemos desplegar nuestra aplicacin con:

mup deploy

Esto empaqueta la aplicacin, y la despliega en el servidor que acabamos de configurar.

Mostrando logs
Los registros son muy importantes y Meteor Up proporciona una forma fcil de manejarlos, emulando
el comando tail -f . Solo tienes que escribir:

mup logs -f

Aqu termina nuestro resumen de lo que puede hacer Meteor Up. Para ms informacin, le sugerimos
visitar el repositorio GitHub de Meteor Up.
Estas tres formas de desplegar aplicaciones Meteor deberan ser suficiente para la mayora de los
casos de uso. Por supuesto, sabemos que algunos de vosotros preferirais tener el control y configurar
un servidor Meteor desde cero. Eso, lo dejaremos para otro da o tal vez para otro libro!

Plantillas

Para introducirnos de manera sencilla en el desarrollo con Meteor, adoptaremos un enfoque de afuera
hacia adentro, es decir, primero construiremos el envoltorio exterior y luego lo conectaremos al
funcionamiento interno de la aplicacin.
Esto implica que, en este captulo, solo utilizaremos el directorio /client .
Si todava no lo has hecho, crea un nuevo archivo main.html dentro del directorio client ,
rellenndolo con el siguiente cdigo:

<head>
<title>Microscope</title>
</head>
<body>
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="/">Microscope</a>
</div>
</header>
<div id="main">
{{> postsList}}
</div>
</div>
</body>

client/main.html

Esta ser la plantilla principal de la aplicacin. Como se puede ver, todo es HTML excepto la etiqueta
{{> postsList}}

, que es un punto de insercin de la plantilla postsList . Ahora, vamos a crear un

par de plantillas ms.

Las plantillas en Meteor

La aplicacin que estamos construyendo va a ser una red social de noticias que estar compuesta de
mensajes (en adelante, posts) organizados en listas, y as es como organizaremos nuestras plantillas.
Vamos a crear el directorio /templates dentro de /client . Aqu pondremos todas nuestras
plantillas, pero adems, para mantener las cosas ordenadas creamos el directorio /posts dentro de
/templates

para las plantillas relacionadas con los posts.

Cmo encuentra nuestros archivos Meteor?


Meteor es bueno encontrando archivos. No importa en qu lugar pongamos el cdigo dentro
de /client , Meteor lo encontrar y lo compilar correctamente. Esto significa que no hay
que escribir manualmente rutas para los archivos CSS o JavaScript.
Tambin significa que se podran poner todos los archivos en el mismo directorio, o incluso
todo el cdigo en el mismo archivo. Pero como, de todas formas, Meteor lo va a compilar
todo en un solo archivo minimizado, preferimos mantener las cosas bien organizadas y
utilizar una estructura de archivos lo ms limpia posible.

Ya estamos listos para nuestra segunda plantilla. Dentro de client/templates/posts , crea el fichero
posts_list.html

<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
</template>

client/templates/posts/posts_list.html

Y post_item.html :

<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
</div>
</div>
</template>

client/templates/posts/post_item.html

Fjate en el atributo name="postsList" del elemento template. Este ser el nombre que Meteor
usar para saber donde va cada plantilla (fjate que el nombre del fichero no es importante).
Es el momento de introducir Spacebars el sistema de plantillas de Meteor. Spacebars es simplemente
HTML mas tres cosas: inclusiones (tambin llamadas partials o plantillas parciales), expresiones
(expressions) y bloques de ayuda (block helpers).
Las inclusiones usan la sintaxis {{> templateName}} y simplemente le dicen a Meteor que reemplace
la inclusin por la plantilla del mismo nombre (en nuestro caso postItem ).
Las expresiones como {{title}} pueden, o bien llamar a una propiedad del objeto actual o bien, al
valor de retorno de un ayudante (helper) como el que definiremos ms adelante en nuestro gestor de
plantilla.
Los bloques de ayuda son tags especiales para mantener el control del flujo de la plantilla, por
ejemplo {{#each}}{{/each}} o {{#if}}{{/if}} .

Ir ms lejos
Si quieres saber ms sobre Spacebars puedes consultar la documentacin oficial.

Con estos conocimientos, ya podemos entender cmo van a funcionar nuestras plantillas:
Primero, en la plantilla postsList iteramos sobre un objeto posts usando un bloque {{#each}}
{{/each}}

, y para cada iteracin, incluimos la plantilla postItem .

Pero, de dnde viene el objeto posts ?. Buena pregunta. Es un ayudante de plantilla, y puedes
pensar en ellos como un cajn o hueco para valores dinmicos.
La plantilla postItem es bastante sencilla. Solo usa tres expresiones: {{url}} y {{title}}
devuelven propiedades, y {{domain}} llama a un ayudante.

Ayudantes de plantillas
Hasta ahora hemos estado tratando con Spacebars, que es poco ms que HTML con algunas etiquetas
extra. A diferencia de otros lenguajes como PHP (o pginas HTML con JavaScript), Meteor mantiene
las plantillas y su lgica separadas, de forma que nuestras plantillas por s mismas no hacen casi
nada.
Para que una plantilla tenga vida, necesita ayudantes. Puedes pensar en ellos como los cocineros
que toman los ingredientes (tus datos) y los preparan, antes de entregar el plato terminado (las
plantillas) al camarero, que, finalmente, los entrega.
En otras palabras, mientras la funcin de las plantillas es mostrar o iterar sobre variables, los
ayudantes son los que hacen el trabajo pesado asignando un valor a cada variable.

Controladores?
Puede ser tentador pensar que los ficheros que contienen estos ayudantes son una especie
de controlador. Pero eso sera ambiguo, ya que los controladores (al menos en el sentido
de controladores MVC) normalmente tienen un papel diferente.
As que hemos decido apartarnos de esa terminologa, y simplemente nos referimos a
ayudantes de plantillas o lgica de plantilla cuando hablamos del cdigo JavaScript que
acompaa a las plantillas.

Para mantener las cosas ordenadas, adoptaremos la convencin de nombrar al fichero que contiene
la plantilla con el mismo nombre, pero con la extensin .js. As que vamos a crear un fichero
posts_list.js

dentro de /client/templates/posts para construir nuestro primer ayudante:

var postsData = [
{
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
},
{
title: 'Meteor',
url: 'http://meteor.com'
},
{
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
}
];
Template.postsList.helpers({
posts: postsData
});

client/templates/posts/posts_list.js

Si todo est bien, ya se pueden ver los datos en el navegador:

Nuestra primera plantilla con datos estticos

Estamos haciendo dos cosas. Primero, creamos algunos datos prototipo en postsData .
Normalmente, estos datos vienen de la base de datos, pero como no hemos visto cmo hacerlo
todava (espera al siguiente captulo), hacemos trampa mediante el uso de datos estticos.
Segundo, usamos la funcin Template.postsList.helpers() para definir un ayudante de plantilla
llamado posts que, sencillamente devuelve nuestros datos creados en postsData .
Y si recuerdas, estamos usando el ayudante posts en nuestra plantilla postsList :

<template name="postsList">
<div class="posts page">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
</template>

client/templates/posts/posts_list.html

Al definir el ayudante posts , conseguimos que est disponible para usarlo en la plantilla, as que
nuestra plantilla ser capaz de recorrer el array postData pasando la plantilla postItem para cada
uno de sus elementos.

Commit 3-1
Aadimos una plantilla bsica para recorrer los posts y d
Ver en GitHub

El ayudante

Lanzar instancia

domain

De forma similar, crearemos un fichero post_item.js para albergar la lgica de la plantilla


postItem

Template.postItem.helpers({
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});

client/templates/posts/post_item.js

Esta vez el valor de nuestro ayudante domain , no son datos sino una funcin annima. Este patrn es
mucho ms comn (y ms til) en comparacin con nuestros ejemplos de datos ficticios.

Mostrando el dominio para cada enlace.

El ayudante domain coge una URL y devuelve su dominio a travs de un poco de magia JavaScript.
Pero, de dnde saca esa url la primera vez?.
Para responder a esta pregunta tenemos que volver a nuestra plantilla posts_list.html . El bloque
{{#each}}

no solo itera nuestros datos, sino que tambin establece el valor de this dentro del

bloque al objeto siendo iterado.


Esto significa que entre las dos etiquetas {{each}} , el valor de this es asignado a cada post
sucesivamente, y esto se hace extensivo al gestor de la plantilla ( post_item.js ).
Ahora entendemos porqu this.url devuelve la URL del post actual. Y ms an, como utilizamos
{{title}}

y {{url}} dentro de nuestra plantilla post_item.html , Meteor sabe que lo que

queremos es this.title y this.url y devuelve los valores correctos.

Commit 3-2
Establecemos un ayudante `domain` en `postItem`.
Ver en GitHub

Lanzar instancia

Magia JavaScript
Aunque esto no es especfico de Meteor, he aqu una breve explicacin de la magia de
JavaScript. En primer lugar, estamos creando un elemento HTML ancla ( a ) vaco y lo
almacenamos en la memoria.
A continuacin, establecemos el atributo href para que sea igual a la URL del post actual
(como acabamos de ver, en un ayudante, this es el objeto que se est usando en este
momento).
Por ltimo, aprovechamos de la propiedad hostname del elemento a para devolver el
nombre de dominio del post sin el resto de la URL.

Si todo ha ido bien deberamos ver una lista de posts en el navegador. Esta lista son solo datos
estticos, lo que, por el momento, no nos permite aprovechar las caractersticas de tiempo real de
Meteor. Aprenderemos cmo hacerlo en el prximo captulo!

Recarga automtica
Probablemente ya habrs notado que no es necesario recargar la ventana del navegador
cada vez que cambiamos un archivo de cdigo.
Esto se debe a que Meteor hace un seguimiento de todos los archivos en el directorio del
proyecto, y los actualiza automticamente en el navegador cada vez que detecta un cambio
en alguno de ellos.
La recarga automtica es bastante inteligente, ya que incluso, conserva el estado de la
aplicacin entre dos refrescos!

Usando Git y GitHub

SIDEBAR

3.5

GitHub es un repositorio para proyectos de cdigo abierto basado en el sistema de control de


versiones Git, y su funcin es hacer que sea fcil compartir cdigo y colaborar en proyectos. Pero
tambin es una gran herramienta de aprendizaje. En este captulo, vamos a ver cmo usar GitHub
para seguir Descubriendo Meteor.
Esta barra lateral asume que no ests familiarizado con Git ni con Github. Si ya manejas las dos cosas,
no dudes en pasar al siguiente captulo!

Commits
El bloque bsico de trabajo de un repositorio git es el commit o confirmacin de cdigo. Puedes pensar
en un commit como en una instantnea del estado de tu cdigo en un momento dado.
En lugar de darte el cdigo final de microscope, hemos ido tomando instantneas en cada paso del
proceso de construccin, y las hemos compartido todas en GitHub.
Por ejemplo, este es el ltimo commit del captulo anterior y se ve as:

Un commit visto en GitHub.

Lo que ves es el di (de diferencia) del archivo post_item.js , es decir, los cambios introducidos
por este commit. En este caso, hemos creado el archivo post_item.js a partir de cero, por lo que
todo su contenido se destaca en verde.
Vamos a compararlo con un ejemplo de ms adelante:

Modificando cdigo.

Esta vez, solo se resaltan en verde las lneas modificadas.


Y, por supuesto, a veces no ests aadiendo o modificando cdigo, tambin borras cosas:

Codigo eliminado.

Ya hemos visto el primer uso de GitHub: ver de un vistazo lo que ha cambiado.

Navegando entre commits de cdigo


La vista de commits muestra los cambios incluidos en este commit en particular, pero a veces, puede
que quieras ver los archivos que no han cambiado, solo para asegurarnos cul es el estado del cdigo
en esta etapa del proceso.
Una vez ms GitHub nos echa una mano. Cuando ests en una pgina de un commit, haz clic en el
cdigo Browse code:

El botn browse code.

Como ves, tienes acceso al repo tal y como estaba en ese commit especfico:

El repositorio en el commit 3-2.

GitHub no nos da muchas pistas visuales de que estamos viendo un commit en particular, pero si lo
comparramos con la rama mster, veremos que la estructura de archivos es muy diferente:

El repositorio en el commit 14-2.

Acceder a un commit de forma local


Acabamos de ver cmo explorar todo un repositorio desde la web de GitHub. Pero, cmo lo hacemos
a nivel local? Por ejemplo, para ejecutar la aplicacin y ver cmo se comporta en un commit en
concreto.
Para verlo, empezaremos a usar la utilidad de lnea de comandos de git. Para empezar,
asegrate de que tienes instalado Git y entonces clona (o, descarga una copia local) el repositorio
Microscope:

git clone https://github.com/DiscoverMeteor/Microscope.git github_microscope

github_microscope

es el nombre del directorio local dnde se le clonar la aplicacin. Si ya existe

el directorio microscope , solo tienes que elegir cualquier otro nombre (no tiene que tener el mismo
nombre que el repositorio GitHub).
Ahora entramos con cd en el repositorio recin descargado y estamos listos para usar la utilidad
de lnea de comando

de git:

cd github_microscope

Cuando clonamos el repositorio de GitHub, descargamos todo el cdigo, lo que significa que lo que
vemos es el ltimo commit (HEAD) de la aplicacin.
Afortunadamente, hay una forma de volver atrs en el tiempo y revisar (check out) un commit
especfico sin afectar a los dems. Vamos a probarlo:

git checkout chapter3-1


Nota: revisando el commit 'chapter3-1'.
Ahora, nuestro repositorio se ha "aislado" del commit HEAD. Podemos revisarlo,
hacer cambios, experimentar con ellos y hacer commits y descartarlos sin que
esto afecte a otras ramas cuando hagamos otros checkouts.
Si quieres mantener los cambios que has hecho, puedes crear una nueva rama
usando la opcin -b con el comando checkout. Por ejemplo:
git checkout -b new_branch_name
HEAD is now at a004b56... Added basic posts list template and static data.

Git nos est informando de que estamos aislados de HEAD, lo que significa que ahora podremos ver

commits del pasado pero no podremos cambiarlos, como si los viramos a travs de una bola de
cristal.
(Ten en cuenta que Git tiene comandos que permiten cambiar commits del pasado. Esto sera como
viajar en el tiempo y pisar una mariposa, pero queda fuera del alcance de esta breve introduccin).
Hemos sido capaces de escribir tan solo chapter3-1 porque todos los commits de Microscope estn
etiquetados con el nmero de captulo correcto. Si este no fuera el caso, tendras que encontrar el
hash o identificador nico del commit que quieres ver.
Una vez ms, GitHub nos lo pone fcil. Podemos encontrar el hash de cada commit en la esquina
inferior derecha de la cabecera azul como se puede ver a continuacin:

Encontrar el hash de un commit.

As que vamos a intentarlo con el hash en vez de con la etiqueta:

git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932


Previous HEAD position was a004b56... Added basic posts list template and static d
ata.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

Y por ltimo, qu pasa si queremos dejar de mirar la bola mgica y volver al presente? Pues le
decimos a Git que queremos volver a la rama master:

git checkout master

Recuerda que, en cualquier momento del proceso, puedes ejecutar la aplicacin usando el comando
meteor

, aunque el repositorio no est en HEAD. Puede ser necesario ejecutar un meteor update

primero si Meteor se queja de que faltan paquetes, ya que el cdigo de los paquetes no est incluido
en el repositorio.

Una perspectiva histrica


Este es otro escenario muy comn: ests mirando un archivo y ves cambios que no habas visto antes
y no recuerdas cundo se han hecho. Podramos revisar comit a commit hasta encontrar el que nos
interesa pero, hay una forma ms fcil gracias a la caracterstica History de GitHub.
En primer lugar, accede a uno de los archivos del repositorio en GitHub, y a continuacin, busca el
botn History:

El botn History de GitHub.

Ahora, dispones de una lista ordenada de todos los commits que afectaron a este archivo en
particular:

Mostrando el historial de un archivo.

El juego de los culpables


Vamos a echar un vistazo a la vista Blame:

El botn Blame de GitHub.

Esta vista ordenada nos muestra lnea por lnea quin modific el archivo en cada commit (en otras
palabras, a quin hay que echar la culpa si las cosas ya no funcionan):

La vista Blame de GitHub.

Git, al igual que GitHub, son herramientas complejas, por lo que no podemos pretender conocerlas
con profundidad en un solo captulo. De hecho, apenas hemos araado la superficie de lo que es
posible hacer con ellas. Pero, lo poco que hemos visto te va a permitir seguir el resto del libro sin
problemas.

Colecciones

En el Captulo uno hablamos sobre la caracterstica central de Meteor: la sincronizacin automtica


de datos entre cliente y servidor.
En este captulo miraremos ms de cerca todo esto y observaremos cmo funciona la tecnologa que
lo hace posible, las Colecciones Meteor.
Una coleccin es una estructura de datos especial que se encarga de almacenar los datos de forma
permanente, en una base de datos MongoDB en el servidor, y de la sincronizacin de datos en tiempo
real con el navegador de cada usuario conectado.
Queremos que nuestros posts sean permanentes y los podamos compartir con otros usuarios, as que
vamos a empezar creando una coleccin llamada Posts para poder almacenarlos.
Las colecciones son el eje central de cualquier aplicacin, as que para asegurarnos de que se definen
primero, las pondremos en el directorio lib . Si todava no lo has hecho, crea un directorio llamado
collections/

dentro de lib , crea un archivo llamado posts.js y aade lo siguiente:

Posts = new Mongo.Collection('posts');

lib/collections/posts.js

Commit 4-1
Coleccin Posts
Ver en GitHub

Lanzar instancia

Con var o sin var


En Meteor, la palabra clave var limita el alcance del objeto al archivo actual. Nosotros
queremos que los Posts estn disponibles para toda nuestra aplicacin, por eso no usamos
var

Almacenando datos
Las aplicaciones web tienen a su disposicin bsicamente tres formas de almacenar datos, cada una
desempeando un rol diferente:
La memoria del navegador: cosas como las variables JavaScript son almacenadas en la
memoria del navegador, lo que implica que no son permanentes: son datos locales a la pestaa
del navegador y desaparecern tan pronto como la cierres.
El almacn del navegador: los navegadores tambin pueden almacenar datos de forma
permanente usando cookies o Local Storage. Aunque estos datos sean permanentes de una
sesin a otra, son locales al usuario actual (pero disponible entre las pestaas) y no se puede
compartir de forma sencilla con otros usuarios.
La base de datos del servidor: el mejor lugar para almacenar datos de forma permanente para
que puedan estar disponibles para ms de un usuario es una base de datos (Siendo MongoDB la
solucin por defecto para las aplicaciones Meteor).
Meteor hace uso de estas tres formas, y a veces sincroniza los datos de un lugar a otro (como veremos
pronto). Dicho esto, la base de datos permanece como la fuente de datos cannica que contiene la
copia maestra de los datos.

Cliente y Servidor
El cdigo dentro de las carpetas que no sean client/ ni server/ se ejecutar en ambos contextos.
Por lo que la coleccin Posts estar disponible en el lado cliente y servidor. Sin embargo, lo que

hace la coleccin en cada entorno es bastante diferente.


En el servidor, la coleccin tiene la tarea de hablar con la base de datos MongoDB y leer y escribir
cualquier cambio. En este sentido, se puede comparar con una librera de base de datos estndar.
En el cliente sin embargo, la coleccin es una copia de un subconjunto de la coleccin cannica. La
coleccin del lado del cliente se mantiene actualizada, de forma constante y (normalmente)
trasparente con ese subconjunto de datos en tiempo real.

Consola vs. Consola vs. Consola


En este captulo hemos empezado a usar la consola del navegador, que no debemos
confundir con la terminal, con la Shell de Meteor o con la Shell de Mongo. A continuacin,
explicamos brevemente cada una de ellas.
Terminal

La Terminal

Se abre desde el sistema operativo.


Las llamadas a console.log() en el lado del servidor se muestran por aqu.

Prompt: $ .
Tambin se conoce como: Shell, Bash
Consola del navegador

La consola del navegador

Se abre desde dentro del navegador, ejecuta cdigo JavaScript.


Las llamadas a console.log() en el lado del cliente se muestran por aqu.
Prompt: .
Tambin se conoce como: JavaScript Console, DevTools Console
La consola de Meteor

La consola de Meteor

Se abre desde la Terminal con meteor shell .


Te da acceso directo al cdigo de la parte del servidor de tu aplicacin.
Prompt: > .
La consola de Mongo

La consola de Mongo

Se abre desde la Terminal con meteor mongo .


Te da acceso directo a la base de datos de tu aplicacin.
Prompt: > .
Tambin se conoce como: Mongo Console
Ten en cuenta que no hay que escribir el carcter prompt ( $ , , or > ) como parte de un
comando. Y que puedes asumir como salida, todo lo que no empiece con el prompt.

Colecciones en el lado del servidor


Volviendo al servidor, la coleccin acta como una API de nuestra base de datos Mongo. En el cdigo
del lado del servidor, esto nos permite escribir comandos Mongo como Posts.insert() o
Posts.update()

, que harn cambios en la coleccin posts almacenada dentro de Mongo.

Para mirar el interior de la base de datos Mongo, abrimos una segunda ventana de terminal (mientras
Meteor se est ejecutando en la primera), vamos al directorio de la aplicacin y ejecutamos el
comando meteor mongo para iniciar una shell de Mongo, en la que podemos escribir los comandos
estndares de Mongo (y como de costumbre, salir con ctrl+c ). Por ejemplo, vamos a insertar un
nuevo post:

meteor mongo
> db.posts.insert({title: "A new post"});
> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};

Consola de mongo

Mongo en Meteor.com
Debemos saber que cuando alojamos nuestra aplicacin en *.meteor.com, tambin
podemos acceder a la consola de Mongo usando meteor mongo myApp .
Y ya que estamos, tambin podemos obtener los logs de nuestra aplicacin escribiendo
meteor logs myApp

La sintaxis de Mongo es familiar, ya que utiliza una interfaz JavaScript. No vamos a hacer ningn tipo
de manipulacin de datos adicional en la consola de Mongo, pero podemos echar un vistazo de vez
en cuando solo para ver lo que pasa por ah.

Colecciones en el lado del cliente


Las colecciones son ms interesantes en el lado del cliente. Cuando se declara Posts = new
Mongo.Collection('posts');

en el cliente, lo que se est creando es una cach local dentro del

navegador de la coleccin real de Mongo. Cuando decimos que las colecciones del lado del cliente son
una cach, queremos decir que contiene un subconjunto de los datos, y ofrece un acceso muy
rpido.
Es importante entender este punto, ya que es fundamental para comprender la forma en la que
funciona Meteor. En general, una coleccin del lado del cliente consiste en un subconjunto de todos
los documentos almacenados en la coleccin de Mongo (por lo general, nunca querremos enviar toda
nuestra base de datos al cliente).
En segundo lugar, los documentos se almacenan en la memoria del navegador, lo que significa que el
acceso a ellos es prcticamente instantneo. As que, cuando se llama, por ejemplo, a Posts.find()
desde el cliente, no hay caminos lentos hasta el servidor o a la base de datos, ya que los datos ya
estn precargados.

Introduciendo MiniMongo
La implementacin de Mongo en el lado del cliente de Meteor se llama MiniMongo. Todava
no est implementada por completo y es posible que podamos encontrar algunas
caractersticas de Mongo que no funcionan en MiniMongo. Sin embargo, todas las que
cubrimos en este libro funcionan de manera similar.

Comunicacin cliente-servidor
La parte ms importante de todo esto es cmo se sincronizan los datos de la coleccin del cliente con
la coleccin del mismo nombre (en nuestro caso posts ) del servidor.
Mejor que explicarlo en detalle, vamos a verlo.
Empezaremos abriendo dos ventanas del navegador, y accediendo a la consola en cada uno de ellos.
A continuacin, abrimos la consola de Mongo en la lnea de comandos.
En este punto, deberamos ser capaces de encontrar el nico documento que hemos creado antes
desde la consola de Mongo (ten en cuenta que el interfaz de nuestra aplicacin estar mostrando
todava los tres posts de prueba anteriores. Ignralos por ahora).

> db.posts.find();
{title: "A new post", _id: ObjectId("..")};

Consola de Mongo

Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};

Consola del primer navegador

Creemos un nuevo post en una de las ventanas del navegador ejecutando un insert:

Posts.find().count();
1
Posts.insert({title: "A second post"});
'xxx'
Posts.find().count();
2

Consola del primer navegador

Como era de esperar, el post aparece en la coleccin local. Ahora vamos a comprobar Mongo:

db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};

Consola de Mongo

Como puedes ver, el post ha viajado hasta la base de datos sin escribir una sola lnea de cdigo para
enlazar nuestro cliente hasta el servidor (bueno, en sentido estricto, hemos escrito una sola lnea de
cdigo: new Mongo.Collection("posts") ). Pero eso no es todo!
Escribamos esto en la consola del segundo navegador:

Posts.find().count();
2

Consola del segundo navegador

El post est ah tambin! A pesar de que no hemos refrescado ni interactuado con el segundo
navegador, y desde luego no hemos escrito cdigo para insertar actualizaciones. Todo ha sucedido

por arte de magia e instantneamente. Todo esto se har ms evidente ms adelante.


Lo que ha pasado es que la coleccin del cliente ha informado de un nuevo post a la coleccin del
servidor, que inmediatamente se pone a distribuirlo en la base de datos Mongo y a todos los clientes
conectados a la coleccin post .
Ver los posts en la consola del navegador no es muy til. Vamos a aprender cmo conectar estos datos
a nuestras plantillas, y de esta forma, convertir nuestro prototipo HTML en una aplicacin web en
tiempo real.

Rellenando la base de datos


Ver el contenido de nuestras colecciones en la consola del navegador es una cosa, pero lo que
realmente nos gustara es mostrar los datos y sus cambios en la pantalla. Cuando esto ocurra,
habremos convertido nuestra sencilla pgina web que muestra datos estticos, en una aplicacin web
en tiempo real en la que los datos cambian de forma dinmica.
Lo primero que vamos a hacer es meter unos cuantos datos en la base de datos. Lo haremos
mediante un archivo que carga un conjunto de datos estructurados en la coleccin de Posts cuando
el servidor se inicia por primera vez.
En primer lugar, vamos a asegurarnos de que no hay nada en la base de datos. Para borrar la base de
datos y restablecer el proyecto usaremos meteor reset . Por supuesto, hay que ser muy cuidadoso
con este comando una vez que se empieza a trabajar en proyectos del mundo-real.
Paramos el servidor Meteor (pulsando ctrl-c ) y, a continuacin, en la lnea de comandos,
ejecutamos:

meteor reset

El comando reset borra completamente la base de datos Mongo. Es til en el desarrollo cundo hay
bastantes posibilidades de que nuestra base de datos caiga en un estado inconsistente.
Vamos a inciar nuestra aplicacin Meteor de nuevo:

meteor

Ahora que la base de datos est vaca, podemos aadir lo siguiente a server/fixtures.js para
cargar tres posts cuando el servidor arranca y encuentra la coleccin Posts vaca:

if (Posts.find().count() === 0) {
Posts.insert({
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
});
Posts.insert({
title: 'Meteor',
url: 'http://meteor.com'
});
Posts.insert({
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
});
}

server/fixtures.js

Commit 4-2
Datos para la coleccin de posts.
Ver en GitHub

Lanzar instancia

Hemos ubicado este archivo en el directorio /server , por lo que no se cargar en el navegador de
ningn usuario. El cdigo se ejecutar inmediatamente cuando se inicia el servidor, y har tres
llamadas a insert para agregar tres sencillos posts en la coleccin de Posts.
Ahora ejecutamos nuevamente el servidor con meteor , y estos tres posts se cargarn en la base de
datos.

Datos dinmicos
Si abrimos una consola de navegador, veremos los tres mensajes cargados desde MiniMongo:

Posts.find().fetch();

Consola del navegador

Para ver estos mensajes renderizados en HTML, podemos utilizar un ayudante de plantilla.
En el Captulo 3 vimos cmo Meteor nos permite enlazar un contexto de datos a nuestras plantillas
Spacebars para construir vistas HTML a partir de estructuras de datos simples. Bien, pues, de la
misma forma vamos a enlazar los datos de nuestra coleccin. Simplemente reemplazamos el objeto
JavaScript esttico postsData por una coleccin dinmica.
A propsito, no dudes en borrar el cdigo de postsData . As es cmo debe quedar
client/templates/posts/posts_list.js

Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});

client/templates/posts/posts_list.js

Commit 4-3
Conexin entre la coleccin Posts y la plantilla `postList`.
Ver en GitHub

Lanzar instancia

Find & Fetch


En Meteor, find() devuelve un cursor que es una fuente de datos reactiva. Cuando
queramos usar los contenidos a los que apunta el cursor, podemos usar fetch() sobre l
para trasformarlo en un array.
Dentro de una aplicacin, Meteor es lo suficientemente inteligente para saber cmo iterar
sobre cursores sin tener que convertirlos de forma explcita en arrays. Por eso no veremos a
menudo fetch() en el cdigo Meteor (y por eso no lo hemos usado en el ejemplo anterior).

Ahora, en lugar de cargar una lista de mensajes como un array esttico desde una variable, ahora
estamos devolviendo un cursor a nuestro ayudante posts (aunque la cosa no parece muy diferente
puesto que estamos devolviendo exactamente los mismos datos):

Usando datos en vivo

Nuestro ayudante {{#each}} ha recorrido todos nuestros Posts , y los ha mostrado en la pantalla.
La coleccin del lado del servidor ha tomado los posts de Mongo, los ha pasado a nuestra coleccin
del lado del cliente, y nuestro ayudante Spacebars los ha pasado a la plantilla.
Ahora iremos un paso ms all, y vamos a aadir otro post a travs de la consola del navegador:

Posts.insert({
title: 'Meteor Docs',
author: 'Tom Coleman',
url: 'http://docs.meteor.com'
});

Consola del navegador

Vuelve a mirar el navegador deberas ver esto:

Aadiendo un post desde la consola

Acabas de ver la reactividad en accin por primera vez. Cuando le pedimos a Spacebars que recorra el
cursor Posts.find() , l ya sabe cmo monitorizar este cursor en busca de cambios, y de esa forma,
alterar el cdigo HTML para mostrar los datos correctos en la pantalla.

Inspeccionando cambios en el DOM


En este caso, el cambio ms simple posible es aadir otro <div class="post"> ...
</div>

. Si queremos asegurarnos de que esto es realmente lo que ocurre, solo tenemos que

abrir el inspector DOM del navegador y seleccionar el <div> correspondiente a uno de los
posts existentes.
Ahora, desde la consola, insertamos otro post. Cuando volvemos de nuevo al inspector,
podremos ver un <div> , correspondiente al nuevo post, pero seguirs teniendo el mismo
<div>

seleccionado. Esta es una manera til de saber cundo han vuelto a ser renderizados

los elementos y cundo no.

Conectando colecciones: Publicaciones y suscripciones


Meteor tiene habilitado por defecto el paquete autopublish , algo que no es conveniente para
aplicaciones en produccin. Este paquete indica que las colecciones son compartidas en su totalidad
con cada cliente conectado. Esto no es lo que realmente queremos, as que vamos a deshabilitarlo.
Abrimos una nueva ventana de terminal y escribimos:

meteor remove autopublish

Esto tiene un efecto instantneo. Si miramos ahora el navegador, veremos que todos nuestros posts
han desaparecido! Esto se debe a que confibamos en autopublish para asegurarnos de que
nuestra coleccin del lado del cliente era una rplica de todos los posts de la base de datos.
Con el tiempo vamos a necesitar asegurarnos de que solo trasferimos los posts que el usuario
realmente necesita ver (teniendo en cuenta cosas como la paginacin). Pero, por ahora, lo vamos a
configurar para que la coleccin Posts se publique en su totalidad (tal y como lo tenamos hasta
ahora).
Para ello, creamos una funcin publish() que devuelve un cursor que referencia a todos los posts:

Meteor.publish('posts', function() {
return Posts.find();
});

server/publications.js

En el cliente, hay que suscribirse a la publicacin. Aadimos la siguiente lnea a main.js :

Meteor.subscribe('posts');

client/main.js

Commit 4-4
`autopublish` eliminado y configurada una publicacin bs
Ver en GitHub

Lanzar instancia

Si comprobamos el navegador de nuevo, veremos que nuestros posts estn de vuelta. Uf!

Conclusin
Entonces, qu hemos logrado? Bueno, a pesar de que no tenemos interfaz de usuario, lo que
tenemos es una aplicacin web completamente funcional. Podramos desplegar esta aplicacin en
Internet, y (mediante la consola del navegador) empezar a publicar nuevas historias y verlas aparecer
en los navegadores de otros usuarios de todo el mundo.

Publicaciones y suscripciones

SIDEBAR

4.5

Las publicaciones y las suscripciones son dos de los conceptos ms importantes de Meteor, pero
puede que sean difciles de comprender si acabas de empezar.
Esto ha acarreado una gran cantidad de malentendidos, como la creencia de que Meteor es inseguro,
o que las aplicaciones no pueden manejar grandes cantidades de datos.
La magia que hace Meteor es la razn ms importante de que ocurra esto al principio. Aunque la
magia es, en ltima instancia muy til, puede ocultar lo que realmente est pasando entre bastidores
(como suele pasar con la magia). As que vamos a desenvolver las capas de dicha magia para tratar de
entender lo que est pasando.

Los viejos tiempos


Pero primero, vamos a echar una mirada a los buenos tiempos all por 2011, cuando todava no
exista Meteor. Digamos que ests haciendo una sencilla aplicacin con Rails. Cuando un usuario llega
a tu sitio, el cliente (es decir, su navegador) enva una solicitud a tu aplicacin, que reside en el
servidor.
Lo primero que hace la App es averiguar qu datos necesita ver el usuario. Estos podran ser la pgina
12 de resultados de bsqueda, la informacin del perfil de usuario de Mary, los 20 tweets ms
recientes de Bob, y as sucesivamente. Bsicamente, podramos verlo como un dependiente de una
librera buscando por los pasillos el libro que has pedido.
Una vez que tiene los datos correctos, el segundo trabajo de la aplicacin es traducir esos datos a un
formato HTML agradable y legible (o JSON en el caso de una API).
En la metfora de la librera, estaramos envolviendo el libro y metindolo en una bolsa bonita. Esta
es la Vista, del famoso modelo Model-View-Controller.

Por ltimo, la aplicacin coge el cdigo HTML y lo enva hacia el navegador. El trabajo de la aplicacin
ha terminado, y puede relajarse tomando una cerveza mientras espera la siguiente solicitud.

Cmo lo hace Meteor?


Veamos lo que hace Meteor tan especial. Como hemos visto, la principal innovacin de Meteor es que,
mientras que una aplicacin Rails solo vive en el servidor, una aplicacin Meteor tambin incluye
componentes que se ejecutarn en el cliente (el navegador).

Enviar un subconjunto de la base de datos al cliente.

As que lo que ocurre es que el empleado de la tienda, no solo encuentra el libro, sino que adems te
sigue a casa para lertelo por la noche (admitiremos que suena un poco raro).
Esta arquitectura permite a Meteor hacer cosas muy interesantes, la ms importante es lo que Meteor
llama base de datos en todas partes. En pocas palabras, Meteor tomar un subconjunto de la base
de datos y la copiar en el cliente.
Esto tiene dos grandes implicaciones: la primera es que en lugar de enviar cdigo HTML, una
aplicacin Meteor enva datos actuales en bruto al cliente y deja que el cliente se ocupe de ellos
(data on the wire). Lo segundo es que sers capaz de acceder, e incluso modificar esos datos
instantneamente sin tener que esperar al servidor (latency compensation).

Publicaciones
Una base de datos de una aplicacin puede contener decenas de miles de documentos, algunos de
los cuales podran ser privados o confidenciales. As que, obviamente, por razones de seguridad y
escalabilidad, no deberamos sincronizar toda la base de datos en el cliente.
Por lo tanto, vamos a necesitar una forma de decirle a Meteor qu subconjunto de los datos se
pueden enviar al cliente, y lo podremos hacer utilizando las publicaciones.
Volvamos a Microscope. Aqu estn todos los posts de nuestra aplicacin que hay en la base de datos:

Todos los posts que contiene la base de datos.

Aunque esta funcin no exista realmente en Microscope, imaginemos que algunos de nuestros posts
se han marcado como entradas con lenguaje abusivo. Aunque queramos mantenerlos en nuestra
base de datos, no deben ponerse a disposicin de los usuarios (es decir, enviarse a los clientes).
Nuestra primera tarea ser decirle a Meteor qu datos queremos enviar. Le diremos que solo
queremos publicar posts sin marcar:

Excluyendo posts marcados.

Este sera el cdigo correspondiente, que estara en el servidor:

// on the server
Meteor.publish('posts', function() {
return Posts.find({flagged: false});
});

Esto asegura que no hay manera posible de que el cliente pueda acceder a un post marcado. Esta es
la forma de hacer una aplicacin segura con Meteor: basta con asegurarse de que solo publicas los
datos a los que el cliente actual debe tener acceso.

DDP
Puedes pensar en el sistema de publicaciones/suscripciones como un embudo que trasfiere
datos desde una coleccin en el servidor (origen) a una en el cliente (destino).
El protocolo que se habla dentro del embudo se llama DDP (que significa Protocolo de Datos
Distribuidos). Para aprender ms sobre DDP, puedes ver esta charla de la conferencia en
Real-time de Matt DeBergalis (uno de los fundadores de Meteor), o este screencast de Chris
Mather que te gua a travs de este concepto con un poco ms de detalle.

Suscripciones
A pesar de que no vamos a poner a disposicin de los clientes los posts marcados, pueden quedar
miles que no debemos enviar de una sola vez. Necesitamos una forma de que los clientes
especifiquen qu subconjunto necesitan en un momento determinado, y aqu es exactamente donde
entran las suscripciones.
Cualquier dato que se suscribe, se refleja en el cliente gracias a Minimongo, la implementacin de
MongoDB en el lado del cliente que provee Meteor.
Por ejemplo, digamos que estamos viendo la pgina del perfil de Bob Smith, y que solo queremos ver
sus posts.

La suscripcin a los posts de Bob ser un reflejo de ellos en el cliente.

En primer lugar, podramos modificar nuestra publicacin para que acepte un parmetro:

// on the server
Meteor.publish('posts', function(author) {
return Posts.find({flagged: false, author: author});
});

Entonces podramos definir ese parmetro cuando nos suscribimos a esa publicacin desde el cliente:

// on the client
Meteor.subscribe('posts', 'bob-smith');

Esta es la forma de hacer escalable una aplicacin Meteor: en lugar de suscribirse a todos los datos
disponibles, solo escogemos las piezas que necesitamos en un momento dado. De esta manera,

evitaremos sobrecargar la memoria del navegador, sin importar si el tamao de la base de datos del
servidor es enorme.

Bsquedas
Ahora resulta que los mensajes de Bob tienen varias categoras (por ejemplo: JavaScript, Ruby, y
Python). Tal vez todava queremos cargar todos los Mensajes de Bob en la memoria, pero en este
momento solo queremos mostrar los de la categora JavaScript. Aqu es donde la bsqueda entra
en juego

Seleccin de un subconjunto de documentos en el cliente.

Al igual que hicimos en el servidor, vamos a utilizar la funcin Posts.find () para seleccionar un
subconjunto de estos datos:

// on the client
Template.posts.helpers({
posts: function(){
return Posts.find({author: 'bob-smith', category: 'JavaScript'});
}
});

Ahora que tenemos una buena comprensin de cmo funcionan las publicaciones y suscripciones,
vamos a profundizar un poco ms, repasando los patrones de diseo ms comunes.

Autopublicacin
Si creas un proyecto Meteor desde cero (es decir, usando meteor create ), el paquete autopublish
se habilitar automticamente. Como punto de partida, vamos a hablar acerca de lo que hace
exactamente este paquete.
El objetivo de autopublish es que sea muy fcil empezar a desarrollar y lo hace reflejando todos los
datos del servidor en el cliente, lo que permite olvidarse de publicaciones y suscripciones y empezar
directamente a escribir el cdigo de la aplicacin.

Autopublish

Y cmo funciona? Supn que tienes una coleccin en el servidor llamada 'posts' . Entonces
autopublish

buscar todos los posts que haya en la base de datos Mongo y los enviar

automticamente a una coleccin llamada 'posts' en el cliente.


As que si usas autopublish , no necesitas pensar en publicaciones. Los datos son omnipresentes, y
todo es ms sencillo. Por supuesto, aparecen problemas obvios al tener una copia completa de la
base de datos en la cach de cada usuario.

Por esta razn, autopublish solo es apropiado cuando estamos empezando, cundo todava no se
ha pensado en las publicaciones.

Publicando colecciones completas


Si eliminamos autopublish , te dars cuenta de que todos los datos han desaparecido del cliente.
Una forma fcil de traerlos de vuelta es, simplemente, replicar lo que hace autopublish publicando
una coleccin en su totalidad. Por ejemplo:

Meteor.publish('allPosts', function(){
return Posts.find();
});

publicando una coleccin completa

Todava publicamos colecciones completas, pero al menos ahora tenemos control sobre qu
colecciones publicamos. En este caso, publicamos la coleccin Posts pero no Comments .

Publicando colecciones parciales


El siguiente nivel de control es publicar solo una parte de una coleccin. Por ejemplo, solo los posts
que pertenecen a un determinado autor:

Meteor.publish('somePosts', function(){
return Posts.find({'author':'Tom'});
});

Publicando una parte de una coleccin

Entre bastidores
Si has ledo la documentacin de Meteor sobre publicaciones, tal vez te habrs sentido
abrumado al or hablar de added() y ready() para establecer los atributos de los registros
en el cliente, y te habr costado cuadrarlo con aplicaciones basadas en Meteor que hayas
podido ver y que nunca usan esos mtodos.
La razn es que Meteor ofrece una comodidad muy importante: el mtodo
_publishCursor()

. Todava no lo has utilizado? Tal vez no directamente, pero eso es

exactamente lo que Meteor utiliza cuando devuelve un cursor (por ejemplo,


Posts.find({'author':'Tom'})

) desde una funcin publish .

Cuando Meteor comprueba que la publicacin somePosts ha devuelto un cursor,


automticamente llama a _publishCursor () para publicar ese cursor.
Esto es lo que hace _publishCursor() :
Se comprueba el nombre de la coleccin en el servidor.
Toma todos los documentos que coinciden con el cursor y los enva al cliente en una
coleccin del mismo nombre. (Para hacer esto, utiliza .added() ).
Cada vez que se aade, elimina o modifica un documento, enva esos cambios a la
coleccin del lado del cliente. (Para hacerlo, utiliza .observe() en el cursor y
.added()

, .changed() y .removed() ).

As, en el ejemplo anterior, podemos asegurar que el usuario solo tiene en la cach, los posts
en los que est interesado (los escritos por Tom).

Publicando propiedades parciales


Hemos visto cmo publicar solo algunos de nuestros posts, pero todava podemos seguir recortando!
Vamos a ver cmo publicar slo algunas propiedades.
Al igual que antes, vamos a utilizar find() para devolver un cursor, pero esta vez vamos a excluir
ciertos campos:

Meteor.publish('allPosts', function(){
return Posts.find({}, {fields: {
date: false
}});
});

Publicando propiedades parciales

Por supuesto, tambin podemos combinar ambas tcnicas. Por ejemplo, si quisiramos devolver
todos los posts de Tom, dejando de lado sus fechas, escribiramos:

Meteor.publish('allPosts', function(){
return Posts.find({'author':'Tom'}, {fields: {
date: false
}});
});

Recapitulando
Hemos visto cmo pasar de publicar todas las propiedades de todos los documentos de todas las
colecciones (con autopublish ) a publicar solo algunas propiedades de algunos documentos de
algunas colecciones.
Esto cubre los fundamentos de lo que se puede hacer con las publicaciones en Meteor, y estas
tcnicas tan sencillas deberan servir para la gran mayora de casos de uso.
An as, en ocasiones, tendrs que ir ms all combinando, vinculando, o fusionando publicaciones.
Todo esto lo veremos ms adelante en uno de los captulos del libro!

Enrutando

Ahora que tenemos una lista de posts (que eventualmente sern enviados por los usuarios),
necesitamos una pgina individual donde nuestros usuarios puedan discutir sobre cada post.
Nos gustara que estas pginas fueran accesibles a travs de un enlace con una URL permanente de la
forma http://myapp.com/posts/xyz (donde xyz es un identificador _id de MongoDB) que sea
nica para cada post.
Esto significa que necesitaremos algn tipo de enrutamiento o routing para analizar lo que hay dentro
de la barra de direcciones del navegador y mostrar el contenido correcto.

Aadiendo el paquete Iron Router


Iron Router es un paquete de enrutado que ha sido concebido especficamente para aplicaciones
Meteor.
No solo ayuda con el enrutamiento (creacin de rutas), sino tambin puede hacerse cargo de filtros
(asignar acciones a algunas de estas rutas) e incluso administrar suscripciones (control de qu ruta
tiene acceso a qu datos). (Nota: Iron Router ha sido desarrollado en parte por Tom Coleman, coautor
de este libro).
En primer lugar, vamos a instalar el paquete desde Atmosphere:

meteor add iron:router

Terminal

Este comando descarga e instala el paquete iron-router dentro de nuestra aplicacin. Hay que tener
en cuenta que a veces puede ser necesario reiniciar la aplicacin (con ctrl+c para parar y meteor

para iniciar de nuevo) antes de poder usar algunos paquetes.

Vocaulario del Router


En este captulo vamos a tocar un montn de caractersticas del Router. Si tienes experiencia
con un framework como Rails, ya estars familiarizado con la mayora de estos conceptos. Si
no, aqu hay un glosario para ponerte al da:
Routes: Una ruta es la pieza de construccin bsica del enrutamiento. Es bsicamente
el conjunto de instrucciones que le dicen a la aplicacin a dnde ir y qu hacer cuando
se encuentra con una URL.
Paths: Un path es una direccin URL dentro de la aplicacin. Puede ser esttica
( /terms_of_service ) o dinmica ( /posts/xyz ), e incluso puede incluir parmetros
de consulta ( /search?Keyword=meteor ).
Segments: Las diferentes partes de un Path, delimitadas por barras inclinadas ( / ).
Hooks: Son acciones que nos gustara realizar antes, despus o incluso durante el
proceso de enrutamiento. Un ejemplo tpico sera comprobar si el usuario tiene las
credenciales adecuadas antes de mostrar una pgina.
Filters: Son simplemente Hooks o acciones que se definen de forma global para una o
ms rutas.
Route Templates: Cada ruta debe apuntar a una plantilla. Si no se especifica una, el
router buscar una plantilla con el mismo nombre que la ruta por defecto.
Layouts: Puedes pensar en los layouts como si fueran marcos para tu contenido.
Contienen todo el cdigo HTML que envuelve la plantilla actual, y seguir siendo el
mismo, aunque la plantilla cambie.
Controllers: Algunas veces, nos daremos cuenta de que muchas de nuestras plantillas
utilizan los mismos parmetros. En lugar de duplicar el cdigo, podemos dejar que
todas estas rutas se hereden desde un solo controlador de enrutamiento que contendr
toda la lgica necesaria.
Para obtener ms informacin acerca de Iron Router, echa un vistazo a la documentacin
completa en GitHub.

Enrutando: Mapeando URLs a plantillas


Hasta ahora, hemos construido nuestro diseo usando una plantilla fija (como {{> postsList}} ).
As que, aunque el contenido de nuestra aplicacin puede cambiar, la estructura bsica de la pgina
es siempre la misma: una cabecera, con una lista de posts debajo de ella.
Iron Router nos permite romper este molde al tomar el control de lo que se muestra en el interior de
la etiqueta HTML <body> . Por eso no vamos a definir el contenido como lo haramos con una pgina
HTML normal. En vez de eso, vamos a indicar al router que apunte a una plantilla especial que
contiene un ayudante {{> yield}} .
El ayudante {{> yield}} definir una zona dinmica especial que mostrar automticamente lo
que corresponde a la ruta actual (a modo de convencin, llamaremos a esta plantilla especial route
template o plantilla de ruta):

Plantillas y layouts.

Empezaremos creando nuestro layout y aadiendo el ayudante {{> yield}}. En primer lugar, vamos a
eliminar la etiqueta <body> del fichero main.html , y movemos su contenido a su propia plantilla,
layout.html

(que colocaremos dentro del directorio client/templates/application ).

Iron Router se ocupar de insertar nuestro layout en nuestro main.html adelgazado, que ahora
quedar as:

<head>
<title>Microscope</title>
</head>

client/main.html

Mientras que el nuevo fichero layout.html , contendr ahora el diseo exterior de la aplicacin:

<template name="layout">
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="/">Microscope</a>
</div>
</header>
<div id="main">
{{> yield}}
</div>
</div>
</template>

client/templates/application/layout.html

Te habrs dado cuenta de que hemos cambiado la inclusin de la plantilla postsList con una
llamada al ayudante yield .
Despus de este cambio, nuestra pestaa del navegador mostrar la pgina de ayuda de Iron Router.
Esto es debido a que no le hemos dicho al router qu debe hacer con la URL / , por lo que
simplemente sirve una plantilla vaca.
Para comenzar, podemos recuperar el comportamiento anterior mapeando la URL raz / a la
plantilla postsList . Vamos a crear un nuevo fichero router.js dentro del directorio /lib en la
raz de nuestro proyecto:

Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});

lib/router.js

Hemos hecho dos cosas importantes. En primer lugar, le hemos dicho al router que utilice el layout
que hemos creado como diseo predeterminado para todas las rutas.
En segundo lugar, hemos definido una nueva ruta llamada postsList y la hemos mapeado a / .

El directorio /lib
Meteor garantiza que cualquier cosa que pongamos dentro de la carpeta /lib se cargar
antes que cualquier otra cosa de la aplicacin (con la posible excepcin de los smart
packages). Esto hace que sea un gran lugar para poner cualquier cdigo auxiliar que debe
estar disponible en todo momento.
Solo una pequea advertencia: ten en cuenta que, dado que la carpeta /lib no est dentro
ni de /client ni de /server , sus contenidos estarn disponibles para ambos entornos.

Rutas con nombre


Vamos a aclarar un poco las cosas. Hemos llamado a nuestra ruta postsList , pero tambin tenemos
una plantilla llamada postsList . Entonces, qu est pasando?
De forma predeterminada, Iron Router buscar una plantilla con el mismo nombre que la ruta. De
hecho, incluso intentar buscar un camino basado en el nombre de la url que proporciones. Aunque
no funcionar en este caso particular (ya que nuestra ruta es / ), Iron Router podra encontrar la
plantilla correcta si usamos http://localhost:3000/postsList como nuestra url.
Te estars preguntando por qu necesitamos nombrar nuestras rutas. Hacerlo nos permite utilizar
algunas caractersticas del Iron Router que hacen que sea ms fcil construir enlaces dentro de
nuestra aplicacin. La ms til es el ayudante de Spacebars {{pathFor}} , que devuelve los

componentes del path de cualquier ruta.


Queremos que el enlace principal apunte de nuevo a la lista de mensajes, as que en vez de especificar
una URL esttica / , podemos utilizar el ayudante Spacebars. El resultado final ser el mismo, pero
tendremos ms flexibilidad porque el ayudante siempre obtendr la direccin URL correcta, incluso si
posteriormente cambiamos el path de la ruta en la configuracin del router.

<header class="navbar navbar-default" role="navigation">


<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...

client/templates/application/layout.html

Commit 5-1
Enrutado bsico.
Ver en GitHub

Lanzar instancia

Esperando a los datos


Si despliegas la versin actual de la aplicacin (o lanzas la instancia mediante el enlace anterior), te
dars cuenta de que la lista aparece vaca durante unos instantes antes de que aparezcan los posts.
Esto es porque cuando la pgina se carga por primera vez, no hay posts para mostrar hasta que se
completa la suscripcin posts obteniendo los datos enviados desde el servidor.
Tendramos una mejor experiencia de usuario si proporcionramos alguna informacin visual de que
algo est pasando, y que el usuario debe esperar un poco.

Por suerte, Iron Router proporciona una forma fcil de hacerlo: podemos decirle que espere ( waitOn )
a la suscripcin.
Empezaremos moviendo nuestra suscripcin posts desde main.js hasta el router:

Router.configure({
layoutTemplate: 'layout',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});

lib/router.js

Lo que estamos diciendo aqu es que para cualquier ruta del sitio (ahora mismo solo tenemos una,
pero pronto vendrn ms!), queremos suscribirnos a la subscripcin posts .
La diferencia clave entre esto y lo que tenamos antes (cuando la suscripcin estaba en main.js , que
ahora debera estar vaco y lo podemos eliminar), es que ahora Iron Router sabe cuando la ruta
est preparada (ready) esto es, cuando la ruta tiene los datos que necesita para renderizarse.

Cargando cosas
Saber cuando la ruta postsList est lista no nos sirve de mucho si de todas formas vamos a estar
mostrando una plantilla vaca. Afortunadamente, Iron Router proporciona una forma de retrasar el
renderizado de una plantilla hasta que la ruta est preparada, y mostrar una plantilla de cargando en
su lugar ( loading ):

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});

lib/router.js

Fjate que como hemos definido nuestra funcin waitOn de forma global a nivel del router, esto solo
ocurrir una sola vez cuando el usuario acceda por primera vez a la aplicacin. Despus de esto, los
datos ya estarn cargados en la memoria del navegador y el router no necesitar volver a esperar de
nuevo.
La pieza final del rompecabezas es la plantilla de carga. Vamos a utilizar el paquete spin para crear
un buen efecto de carga animada. Lo aadimos con meteor add sacha:spin , y luego creamos la
plantilla loading de carga en el directorio client/templates/includes :

<template name="loading">
{{>spinner}}
</template>

client/templates/includes/loading.html

Ten en cuenta que {{>spinner}} est contenido en el paquete spin . A pesar de que proviene de
fuera de nuestra aplicacin, podemos incluirlo como cualquier otra plantilla.
Por lo general es una buena idea esperar a las suscripciones, no solo por la experiencia de usuario,
sino tambin porque significa que podemos asumir con seguridad que los datos estarn siempre
disponibles dentro de una plantilla. Esto elimina la necesidad de enredarse con plantillas que se
muestran antes de que los datos que usan estn disponibles, cosa que a menudo requiere soluciones
difciles.

Commit 5-2
Esperando a la suscripcin.
Ver en GitHub

Lanzar instancia

Un primer vistazo a la reactividad


La reactividad es una parte fundamental de Meteor, y aunque todava queda un poco para
conocerla, nuestra plantilla de carga nos da un primer vistazo a este concepto.
Redireccionar a una plantilla de carga de datos si no se ha cargado todava est muy bien,
pero cmo sabe el router cundo redirigir al usuario una vez han llegado los datos?
Por ahora, solo diremos que aqu es exactamente donde entra en juego la reactividad. Pero
no te preocupes, aprenders ms sobre ella muy pronto!

Enrutando a un post especfico


Ahora que hemos visto cmo enrutar hacia la plantilla postsList , vamos a configurar una ruta para
mostrar los detalles de un solo post.
Solo hay un problema: no podemos continuar definiendo rutas una por una para cada post, ya que
podra haber cientos de ellos. As que tendremos que crear una ruta dinmica y hacer que se vea esta
nos muestre cualquier post que queremos.
Para empezar, vamos a crear una nueva plantilla post_page.html que simplemente muestra la
misma plantilla para un post que hemos utilizado anteriormente en la lista de posts.

<template name="postPage">
<div class="post-page page">
{{> postItem}}
</div>
</template>

client/templates/posts/post_page.html

Ms adelante aadiremos ms elementos a esta plantilla (como los comentarios), pero, por ahora,
solo la vamos a usar para mostrar {{> PostItem}} .
Ahora vamos a crear otra ruta con nombre, esta vez, mapeando URLs de la forma /posts/<ID> hacia
la plantilla postPage :

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage'
});

lib/router.js

La sintaxis especial :_id le dice dos cosas al router: primero, que encuentre cualquier ruta de la
forma /posts/xyz/ , donde xyz puede ser cualquier cadena. En segundo lugar, poner lo que
encuentra dentro de una propiedad _id en el vector de parmetros del router.
Ten en cuenta que usamos el _id como cadena porque as lo queremos. El router no tiene manera
de saber si le pasamos un _id real, o simplemente una cadena de caracteres al azar.
Ya enrutamos a la plantilla correcta, pero todava nos falta algo: el router conoce el _id del post que

nos gustara ver, pero la plantilla todava no tiene ni idea. Entonces, cmo solucionamos este
problema?
Afortunadamente, el router integra una solucin inteligente: permite especificar el contexto de datos
de una plantilla. Puedes pensar en el contexto de datos como lo que rellena un delicioso pastel hecho
de plantillas y diseos. En pocas palabras, son los datos con los que rellenamos la plantilla:

El contexto de datos.

En nuestro caso, podemos obtener el contexto de datos correcto mediante la bsqueda de nuestro
post basado en el _id que recibimos de la URL:

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});

lib/router.js

De esta forma, cada vez que un usuario accede a esta ruta, encontraremos el post adecuado y lo
pasaremos a la plantilla. Recuerda que findOne devuelve un solo post, el que coincide con la
consulta, y que proporcionar solo un id como argumento es una abreviatura de {_id: id} .
Dentro de la funcin data de una ruta, this se corresponde con la ruta actual, y podemos usar
this.params

para acceder a las propiedades de la ruta (que habamos indicado con el prefijo :

dentro de nuestro path ).

Ms acerca de los contextos de datos


Al establecer el contexto de datos de una plantilla, se puede controlar el valor de this
dentro de los ayudantes de la plantilla.
Esto se hace implcitamente con el iterador {{#each}} , que ajusta automticamente el
contexto de datos de cada iteracin para el elemento que se est iterando:

{{#each widgets}}
{{> widgetItem}}
{{/each}}

Pero tambin podemos hacerlo explcitamente utilizando {{#with}} , que simplemente


dice toma este objeto, y le aplicas la siguiente plantilla. Por ejemplo, se puede escribir:

{{#with myWidget}}
{{> widgetPage}}
{{/with}}

Resulta que se consigue el mismo resultado pasando el contexto como un argumento en la


llamada a la plantilla. As que el bloque de cdigo anterior se puede reescribir como:

{{> widgetPage myWidget}}

Para una exploracin con profundidad sobre los contextos de datos sugerimos leer nuestro
blog sobre este tema.

Usando nuestro enrutador dinmico


Por ltimo, crearemos un nuevo botn Discuss que enlazar a nuestra pgina invidual del post. De
nuevo, podramos hacer algo como <a href="/posts/{{_id}}"> , pero es mucho ms fiable utilizar
un ayudante de ruta.

Hemos llamado a la ruta al post postPage , as que podemos usar el ayudante {{pathFor
'postPage'}}

<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>

client/templates/posts/post_item.html

Commit 5-3
Ruta para un nico post.
Ver en GitHub

Lanzar instancia

Pero espera, cmo sabe el router dnde conseguir la parte xyz en /posts/xyz ? Despus de todo,
no le hemos pasado ninguna _id .
Resulta que Iron Router es lo suficientemente inteligente como para averiguarlo por s mismo. Le
estamos diciendo que use la ruta postPage , y el router sabe que esta ruta requiere un _id de algn
tipo (as es como hemos definido nuestro path ).
As que el router buscar este _id en el lugar ms lgico: el contexto de datos del ayudante
{{pathFor 'postPage'}}

, en otras palabras: this . Y da la casualidad de que nuestro this

corresponde a un post, que (sorpresa!) tiene una propiedad _id .


De forma alternativa, se puede especificar el lugar donde tiene que buscar el _id , pasando un
segundo argumento al ayudante (es decir, {{pathFor 'postPage' someOtherPost}} ). Un uso

prctico sera, por ejemplo, conseguir los enlaces a los posts anterior y siguiente en una lista.
Para ver si todo funciona correctamente, navega a la lista de posts y haz clic en uno de los enlaces
Discuss. Deberas ver algo como esto:

La pgina para un slo post.

HTML5 pushState
Una cosa que hay que tener en cuenta es que estos cambios en las URLs suceden gracias a
HTML5 pushState.
El router recoge los clics en URL internas, y evita que el navegador salga fuera de la
aplicacin haciendo los cambios necesarios en su estado.
Si todo funciona correctamente la pgina debera cambiar instantneamente. De hecho, a
veces las cosas cambian tan rpido que podra ser necesario aadir algn tipo de transicin.
Esto est fuera del alcance de este captulo, aunque, no obstante, es un tema interesante.

Post no encontrado
No olvidemos que el enrutamiento funciona de ambas formas: podemos cambiar la URL cuando
visitamos una pgina, pero tambin podemos mostrar una pgina cuando cambiemos la URL. Por lo
que tenemos que pensar que pasa si alguien introduce una URL errnea.
Menos mal que Iron Router se preocupa por esto a travs de la opcin notFoundTemplate .
Primero, crearemos una plantilla que muestre un simple error 404:

<template name="notFound">
<div class="not-found page jumbotron">
<h2>404</h2>
<p>Sorry, we couldn't find a page at this address.</p>
</div>
</template>

client/templates/application/not_found.html

Despus, sencillamente le decimos a Iron Router que use esta plantilla:

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});

//...

lib/router.js

Para probar la nueva pgina de error, puedes intentar introducir una URL aleatoria como
http://localhost:3000/nothing-here

Pero un momento, qu pasa si alguien introduce una URL de la forma


http://localhost:3000/posts/xyz

, donde xyz no es un identificador _id de post vlido? Esto es

una ruta vlida, pero no apunta a ningn dato.


Afortunadamente, Iron Router es lo suficientemente inteligente para saber esto si definimos un hook
especial dataNotFound al final de router.js :

//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});

lib/router.js

Esto le dice a Iron Router que muestre la pgina de no encontrado, no solo cuando la ruta sea
invlida, si no tambin para la ruta postPage cuando la funcin data devuelva un objeto falso (o
null

, false , undefined o vaci).

Commit 5-4
Aadida la plantilla de no encontrado.
Ver en GitHub

Lanzar instancia

Por qu Iron?
Te sorprenderas sobre la historia detrs del nombre Iron Router. Segn el autor Chris
Mather, viene del hecho de que los meteoritos estn compuestos principalmente de hierro.

La sesin

SIDEBAR

5.5

Meteor es un framework reactivo. Esto significa que cuando cambian los datos, cambian cosas de la
aplicacin sin tener que hacer nada de forma explcita.
Ya lo hemos visto en accin viendo cmo cambian nuestras plantillas cuando cambian los datos y las
rutas.
En captulos posteriores veremos ms en profundidad cmo funciona todo esto, pero ahora, nos
gustara introducir algunas caractersticas bsicas de la reactividad, que son muy tiles en todas las
aplicaciones.

La sesin en Meteor
Ahora mismo, en Microscope, el estado actual de la aplicacin est contenido en su totalidad en la
URL que se est mostrando (y en la base de datos).
Pero en muchos casos, necesitars almacenar valores de estado que solo son relevantes en la versin
de la aplicacin para usuario actual (por ejemplo, si algn elemento se muestra o est oculto). Usar la
Sesin es una forma conveniente de hacerlo.
La sesin es un almacn global de datos reactivo. Es global en el sentido de que es un objeto global:
solo hay una sesin, y esta es accesible desde todas partes. Las variables globales se suelen ver como
algo malo, pero en este caso la sesin se utiliza como un bus de comunicacin central para diferentes
partes de la aplicacin.

Modificando la Sesin
La sesin est disponible en todas partes en el cliente como el objeto Session . Para establecer un
valor en la sesin, puedes llamar a:

Session.set('pageTitle', 'A different title');

Consola del navegador

Puedes leer los datos de nuevo con Session.get('mySessionProperty'); . Hemos dicho que la
Sesin es una fuente de datos reactiva, lo que significa que si lo pones en un ayudante, veras un
cambio en el navegador cuando cambia la variable de sesin.
Para probarlo, aade el siguiente cdigo a la plantilla layout:

<header class="navbar navbar-default" role="navigation">


<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
</div>
</header>

client/templates/application/layout.html

Template.layout.helpers({
pageTitle: function() { return Session.get('pageTitle'); }
});

client/templates/application/layout.js

Una nota sobre el cdigo de las barras laterales


Ten en cuenta que el cdigo mostrado en las barras laterales no forma parte del flujo
principal del libro. Por lo que debera crear una nueva rama (si ests usando Git), o si no,
asegurarte de deshacer los cambios al final del captulo.

La recarga automtica de Meteor (recarga de cdigo en caliente o HCR, de Hot Code Reload)
preserva las variables de sesin, por lo que ahora debes ver A dierent title en la barra de
navegacin. Si no es as, solo tienes que escribir el comando Session.set() de nuevo.
Si lo cambiamos de nuevo (desde la consola del navegador), debemos ver que se visualiza otro ttulo:

Session.set('pageTitle', 'A brand new title');

Consola del navegador

La sesin est disponible a nivel global, por lo que los cambios se pueden hacer desde cualquier lugar
de la aplicacin. Esto nos da una gran cantidad de potencia y flexibilidad, pero tambin puede ser una
trampa si se usa demasiado.
De todas formas, es importante apuntar que el objeto de Session no es compartido entre distintos
usuarios, ni siquiera entre distintas pestaas del navegador. Esto es por lo que si abres tu aplicacin
en una nueva pestaa, te encontrars con una pgina en blanco.

Cambios idnticos
Si modificas una variable de sesin con Session.set() pero la estableces al mismo valor,
Meteor es lo suficientemente inteligente como para eludir la cadena reactiva, y evitar las
llamadas a mtodos innecesarios.

Presentamos a Autorun
Hemos visto un ejemplo de una fuente de datos reactiva, y la hemos visto en accin dentro de un
ayudante de plantilla. Pero mientras que algunos contextos en Meteor (como ayudantes de plantilla)
son inherentemente reactivos, la mayora de cdigo de la aplicacin sigue siendo el viejo y simple
JavaScript.

Supongamos que tenemos el siguiente fragmento de cdigo en algn lugar de nuestra aplicacin:

helloWorld = function() {
alert(Session.get('message'));
}

A pesar de que llamamos a una variable de sesin, el contexto en el que se hace no es reactivo, lo que
significa que no vamos a ver alerts cada vez que cambia su valor.
Aqu es dnde entra en juego Autorun. Como su nombre indica, el cdigo dentro de un bloque
autorun

se ejecutar automticamente cada vez que cambien las fuentes de datos reactivas

utilizadas dentro.
Prueba a escribir esto en la consola del navegador:

Tracker.autorun( function() { console.log('Value is: ' + Session.get('pageTitle


')); } );
Value is: A brand new title

Consola del navegador

Como era de esperar, el bloque de cdigo situado en el interior de autorun se ejecuta una vez,
mostrando los datos por la consola. Ahora, vamos a intentar cambiar el ttulo:

Session.set('pageTitle', 'Yet another value');


Value is: Yet another value

Consola del navegador

Magia! Al cambiar el valor, autorun sabe que tiene que ejecutar su contenido de nuevo, volviendo a
mostrar el nuevo valor por la consola.

Volviendo a nuestro ejemplo anterior, si queremos activar una nueva alerta cada vez que cambien
variables de sesin, lo nico que tenemos que hacer es envolver nuestro cdigo en un bloque
autorun

Tracker.autorun(function() {
alert(Session.get('message'));
});

Como acabamos de ver, los autorun pueden ser muy tiles para rastrear fuentes de datos reactivas y
reaccionar inmediatamente ante ellos.

Recarga automtica de cdigo


Durante el desarrollo de Microscope, hemos estado aprovechando una de las caractersticas de
ahorro de tiempo que proporciona Meteor: La recarga de cdigo en caliente (HCR). Cada vez que
guardamos uno de nuestros archivos fuente, Meteor detecta los cambios y reinicia el servidor de
forma trasparente, informando a todos los clientes para que recarguen la pgina.
Es similar a una recarga automtica de pgina, pero con una diferencia importante.
Para entenderlo, vamos a restablecer la variable de sesin que hemos estado utilizando:

Session.set('pageTitle', 'A brand new title');


Session.get('pageTitle');
'A brand new title'

Consola del navegador

Si recargramos la pgina manualmente se perderan las variables de sesin (porque que estaramos
creando una nueva). Pero si provocamos una recarga en caliente (por ejemplo, guardando uno de
nuestros archivos de cdigo), la pgina volver a cargar, pero todava tendremos el valor de la

variable de sesin. Prubalo!

Session.get('pageTitle');
'A brand new title'

Consola del navegador

As que si utilizamos variables de sesin para hacer un seguimiento de lo que est haciendo el
usuario, el HCR debe ser prcticamente trasparente para el usuario, ya que preserva el valor de todas
las variables de sesin. Esto nos permite desplegar nuevas versiones de nuestra aplicacin, ya en
produccin, con la seguridad de que no molestaremos mucho a nuestros usuarios.
Considera esto un momento. Si podemos llegar a mantener el estado entre la URL y la sesin,
podemos cambiar de forma trasparente el cdigo que est corriendo por debajo con una mnima
interrupcin.
Ahora vamos a comprobar lo que pasa cuando refrescamos la pgina de forma manual:

Session.get('pageTitle');
null

Browser console

Hemos perdido la sesin. En un HCR, Meteor guarda la sesin en el almacenamiento local del
navegador y lo carga de nuevo tras la recarga. Esto no significa que el comportamiento de recarga
explcita no tenga sentido: si un usuario recarga la pgina, es como si navega de nuevo a la misma
URL, y el estado, debe restablecerse al que vera cualquier usuario que visita esa URL.
Las lecciones ms importantes en todo esto son:
1. Guarda siempre el estado en la sesin o en la URL para no molestar mucho a los usuarios

cuando ocurre una recarga en caliente.


2. Almacena cualquier estado que deba estar compartido entre usuarios dentro de la propia URL.
Con esto concluye nuestra exploracin de la sesin, uno de las caractersticas ms tiles de Meteor.
No olvides deshacer cualquier cambio en el cdigo antes de pasar al siguiente captulo.

Aadiendo usuarios

Hasta el momento, hemos logrado crear y mostrar algunos datos estticos y conectarlo todo en un
prototipo simple.
Incluso hemos visto cmo nuestra interfaz de usuario es sensible a los cambios en los datos, y que los
cambios aparecen inmediatamente cuando se insertan o cambian los datos. An as, nuestro sitio
est limitado por el hecho de que no podemos introducir datos. De hecho, ni siquiera tenemos
usuarios todava!
Veamos cmo arreglamos esto.

Cuentas de usuario de forma sencilla


En la mayora de los frameworks web, agregar cuentas de usuario es un problema familiar. Hay que
hacerlo en casi todos los proyectos, pero nunca resulta tan fcil como quisiramos. Y si adems hay
que tratar con OAuth u otros esquemas de autenticacin de terceros, las cosas tienden a ponerse feas
rpidamente.
Por suerte, Meteor nos ampara. Gracias a la forma en la que los paquetes Meteor pueden contribuir al
cdigo del servidor (JavaScript) y al del cliente (JavaScript, HTML y CSS), podemos obtener un
sistema de cuentas casi sin esfuerzo.
Podramos usar el paquete para cuentas de usuario ( meteor add accounts-ui ), pero como hemos
construido toda nuestra aplicacin con Bootstrap, vamos a utilizar ian:accounts-ui-bootstrap-3
(la nica diferencia es el estilo). En la lnea de comandos, tecleamos:

meteor add ian:accounts-ui-bootstrap-3


meteor add accounts-password

Terminal

Estos dos comandos hacen accesibles las plantillas para cuentas de forma que podemos incluirlas en
nuestro sitio usando el ayudante {{> loginButtons}} . Un consejo muy til: se puede controlar en
qu lado aparece el desplegable de inicio de sesin con el atributo align (por ejemplo: {{>
loginButtons align="right"}})

Vamos a aadir los botones a nuestra cabecera. Y puesto que la cabecera est empezando a crecer,
vamos a darle ms espacio en su propia plantilla (la pondremos en el directorio
client/templates/includes/

). Estamos utilizando cdigo y clases como se indica en Bootstrap

para que todo se vea mejor:

<template name="layout">
<div class="container">
{{> header}}
<div id="main">
{{> yield}}
</div>
</div>
</template>

client/templates/application/layout.html

<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>

client/templates/includes/header.html

Ahora ya podemos ver los botones de acceso en la esquina superior derecha.

Interfaz de usuario de inicio de sesin en Meteor

Podemos usarlos para iniciar sesin, solicitar un cambio de contrasea, y todo lo que se necesita para
gestionar cuentas basadas en contraseas.
Para decirle a nuestro sistema de cuentas que queremos que los usuarios accedan al sistema a travs
de solo un nombre de usuario, simplemente aadimos un bloque de configuracin en un nuevo
archivo config.js dentro de client/helpers/

Accounts.ui.config({
passwordSignupFields: 'USERNAME_ONLY'
});

client/helpers/config.js

Commit 6-1
Aadidas cuentas de usuario y una plantilla en la cabecera
Ver en GitHub

Lanzar instancia

Creando nuestro primer usuario


Adelante, regstrate para obtener una cuenta: el botn Sign In cambiar para mostrar el nombre de
usuario. Esto confirma que has creado una cuenta. Pero, de dnde vienen los datos de la cuenta?
Cuando aadimos el paquete accounts , Meteor crea una nueva coleccin, que se puede acceder
desde Meteor.users . Para verlo, abre la consola del navegador y escribe:

Meteor.users.findOne();

Consola del navegador

La consola devuelve un objeto que representa el usuario. Si lo inspeccionamos un poco, veremos que
contiene nuestro nombre de usuario, as como un identificador nico _id . Tambin se puede
obtener el usuario que ha iniciado sesin con Meteor.user() .
Ahora, nos registramos con otro usuario y ejecutamos lo siguiente en la consola del navegador:

Meteor.users.find().count();
1

Consola del navegador

La consola devuelve 1. No debera haber 2? Se ha borrado el primero? Si intentas acceder con ese
usuario, vers que no es as.
Vamos a mirar en la base de datos del servidor ( meteor mongo en la terminal):

> db.users.count()
2

Consola de Mongo

Definitivamente hay 2, pero, porqu slo se ve uno en el navegador?

Una publicacin misteriosa!


Si pensamos de nuevo en el captulo 4, recordars que deshabilitamos la auto-publicacin
autopublish

, dejamos de enviar todos los datos procedentes del servidor a las colecciones locales

de cada cliente conectado. Tuvimos que crear parejas de publicaciones y suscripciones para

intercambiar datos.
Sin embargo, no hemos creado ninguna clase de publicacin para usuarios. As que cmo es que
podemos ver esos datos?
La respuesta es que el paquete accounts , autopublica los datos bsicos de la cuenta del usuario
actual, de otra forma, el usuario no podra acceder nunca al sitio.
El hecho de que el paquete accounts slo publica el usuario actual explica porqu un usuario no
puede ver los detalles de la cuenta de los otros usuarios.
As que solo se publica un objeto de usuario por usuario conectado (y ninguno cuando no est
autenticado).
Es ms, los documentos en el navegador no parecen contener los mismos campos que en el servidor.
En Mongo, un usuario tiene un montn de datos. Para verlo, vuelve a la terminal de Mongo y escribe:

> db.users.find()
{
"_id": "H5kKyxtbkLhmPgtqs",
"createdAt": ISODate("2015-02-10T08:26:48.196Z"),
"profile": {},
"services": {
"password": {
"bcrypt": "$2a$10$yGPywo3/53IHsdffdwe766roZviT03YBGltJ0UG"
},
"resume": {
"loginTokens": [{
"when": ISODate("2015-02-10T08:26:48.203Z"),
"hashedToken": "npxGH7Rmkuxcv098wzz+qR0/jHl0EAGWr0D9ZpOw="
}]
}
},
"username": "sacha"
}

Consola de Mongo

Por otro lado, en el navegador el objeto de usuario tiene muchos menos campos, como se puede ver
escribiendo el comando equivalente:

Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}

Consola del navegador

Este ejemplo nos muestra como una coleccin local puede ser un subconjunto seguro de la base de
datos real. El usuario conectado solo ve lo necesario para poder hacer el trabajo (en este caso, el
nombre de usuario). Este es un patrn que debemos aprender porque nos ser muy til ms adelante.
Eso no significa que no podamos hacer pblicos ms datos de usuario. Si lo necesitamos, podemos
consultar la documentacin de Meteor para ver cmo se hace.

Reactividad

SIDEBAR

6.5

Si las colecciones son la caracterstica principal de Meteor, podemos decir que la reactividad es lo
que hace til esta caracterstica.
Las colecciones trasforman radicalmente la forma en que la aplicacin maneja cambios en los datos.
En lugar de tener que comprobarlo manualmente (por ejemplo, con una llamada AJAX) y actualizar
los cambios en el cdigo HTML, con Meteor, los cambios pueden llegar en cualquier momento y
aplicarse a la interfaz de usuario sin ms complicaciones.
Vamos a verlo con detenimiento: entre bastidores, Meteor es capaz de cambiar cualquier parte de la
interfaz de usuario cuando se actualiza una coleccin subyacente.
La forma imperativa (o a mano) de hacerlo sera utilizar .observe() , una funcin del cursor que
dispara callbacks cuando los documentos seleccionados cambian. De esta forma, podramos hacer
cambios en el DOM (el HTML de nuestra pgina web) a travs de esos callbacks. El cdigo resultante
sera algo como esto:

Posts.find().observe({
added: function(post) {
// when 'added' callback fires, add HTML element
$('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
},
changed: function(post) {
// when 'changed' callback fires, modify HTML element's text
$('ul li#' + post._id).text(post.title);
},
removed: function(post) {
// when 'removed' callback fires, remove HTML element
$('ul li#' + post._id).remove();
}
});

Es probable que te des cuentas de que este tipo de cdigo va a tender a hacerse muy complejo muy

rpidamente. Imagnate lo que sera tratar con cambios en cada uno de los atributos del post, y tener
que cambiar HTML complejo dentro de <li> . Por no hablar de casos ms extremos cuando
empecemos a depender de mltiples fuentes de informacin que pueden cambiar en tiempo real.

Cundo deberamos usar observe() ?


A veces es necesario usar el patrn anterior, sobre todo cuando se trabaja con widgets de
terceros. Por ejemplo, imaginemos que queremos aadir o quitar puntos en un mapa en
tiempo real dentro de una Coleccin (por ejemplo, para mostrar las localizaciones de los
usuarios actualmente conectados).
En tal caso, tendrs que usar callbacks observe() para conseguir que el mapa hable con
la coleccin Meteor y saber cmo reaccionar a los cambios en los datos. Por ejemplo,
tendras que confiar en los callbacks added y removed para llamar a los mtodos dropPin
o removePin() de la API del mapa.

Un enfoque declarativo
Meteor nos proporciona una forma fcil de hacer todo esto: la reactividad, que, en esencia es un
enfoque declarativo. De esta forma, podemos definir la relacin entre los objetos una sola vez y
podemos estar seguros de que se mantendrn siempre sincronizados, en vez de tener que especificar
el comportamiento de cada uno de los posibles cambios.
Este es un concepto realmente poderoso, ya que, en un sistema de tiempo real puede haber muchas
entradas y todas pueden cambiar de forma impredecible. Con declarativo, queremos decir que,
cuando se renderiza HTML basado en una o varias fuentes de datos reactivos, Meteor se hace cargo de
la tarea de sincronizar las fuentes y, de forma trasparente, asumir el trabajo sucio de mantener la
interfaz de usuario actualizada.
Y todo esto, slo para acabar diciendo que, en lugar de tener que pensar en callbacks tipo observe ,
Meteor nos permite escribir:

<template name="postsList">
<ul>
{{#each posts}}
<li>{{title}}</li>
{{/each}}
</ul>
</template>

Y obtener la lista de posts con:

Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});

Cuando cambian los datos reactivos, es Meteor el que, entre bastidores, est creando callbacks
observe()

, y redibujando las secciones pertinentes del HTML.

Seguimiento de dependencias en Meteor: Computaciones


Meteor es un framework reactivo y en tiempo real, pero no todo el cdigo incluido en una aplicacin
Meteor es reactivo. Si as fuera, la aplicacin entera se volvera a ejecutar cada vez que cambia algo.
En vez de eso, la reactividad se limita a reas especficas, que llamamos computaciones.
En otras palabras, una computacin es un bloque de cdigo que se ejecuta cada vez que cambia una
de las fuentes de datos reactivos de las que depende. Si tienes una fuente reactiva (por ejemplo, una
variable de sesin) y quieres responder reactivamente a ella, tendrs que crear una computacin.
Ten en cuenta que, por lo general, no es necesario hacerlo de forma explcita, ya que, Meteor provee
de una computacin especial a cada una de nuestras plantillas (lo que significa que puedes estar
seguro de que tus plantillas reflejarn los datos de los que dependen).

Todas las fuentes de datos reactivas hacen un seguimiento de todas las computaciones que las usan
para poder avisarles de que su valor ha cambiado y deben volver a computarse. Para hacer esto, se
llama a la funcin invalidate() .
Generalmente, las computaciones se establecen para volver a evaluar su contenido, y esto es lo que
ocurre con las computaciones que Meteor crea para las plantillas (aunque, adems, se aade un poco
ms de magia para redibujar la pgina de manera ms eficiente). Aunque, si es necesario, se puede
tener ms control sobre lo que hace una de estas computaciones, en la prctica, no va a ser necesario
porque Meteor nos va a dar justo el comportamiento que vamos a necesitar.

Configuracin de una computacin


Ahora que entendemos la teora de las computaciones, configurar una te va a parecer
desproporcionadamente fcil. Usaremos la funcin Tracker.autorun para encerrar un bloque de
cdigo en una computacin y hacerlo reactivo:

Meteor.startup(function() {
Tracker.autorun(function() {
console.log('There are ' + Posts.find().count() + ' posts');
});
});

Ten en cuenta que tenemos que envolver el bloque Tracker dentro de un bloque
Meteor.startup()

para asegurarnos que solo se ejecute una vez cuando Meteor ha terminado de

cargar la coleccin Posts .


Entre bastidores, autorun crea una computacin, y la configura para que se vuelva a evaluar cada
vez que cambian las fuentes de datos de las que depende. El ejemplo es muy sencillo, simplemente
muestra el nmero actual de posts en la consola. Como Posts.find() es una fuente de datos de
reactiva, ser esta la que se encargue de hacer que se vuelva a ejecutar la computacin cada vez que
cambie el nmero de posts.

> Posts.insert({title: 'New Post'});


There are 4 posts.

El resultado es que, de forma fcil y natural, podemos escribir cdigo que usa datos reactivos,
sabiendo que, por detrs, el sistema de dependencias se encargar de todo en todo momento.

Creando posts
Hemos visto lo fcil que es crear posts llamando a Posts.insert a travs de la consola pero, no
podemos esperar que nuestros usuarios hagan lo mismo.
Necesitamos construir algn tipo de interfaz de usuario para que los usuarios creen nuevas entradas
en la aplicacin.

Creando la pgina de envo


Empezaremos definiendo una ruta para nuestra nueva pgina en lib/router.js :

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.onBeforeAction('dataNotFound', {only: 'postPage'});

lib/router.js

Aadiendo un enlace en la cabecera


Con la ruta definida, ahora podemos aadir un enlace a la cabecera de nuestra pgina:

<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>

client/templates/includes/header.html

Configurar una ruta implica que si un usuario navega a /submit , Meteor mostrar la plantilla
postSubmit

. As que vamos a escribir esa plantilla:

<template name="postSubmit">
<form class="main form page">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" c
lass="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name yo
ur post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>

client/templates/posts/post_submit.html

Aqu hay un montn de markup, pero es solo porque usamos el CSS de Twitter Bootstrap. Aunque slo
son esenciales los elementos del formulario, el marcado adicional ayudar a que nuestra aplicacin
se vea un poco mejor. Ahora debera tener un aspecto similar a este:

El formulario de creacin de posts

Es un simple formulario. No tenemos que preocuparnos de programar una accin para l, porque
interceptaremos su evento submit y actualizaremos los datos va JavaScript. (No tiene sentido
proporcionar un fallback no-JS si tenemos en cuenta que Meteor no funciona con JavaScript
desactivado).

Creando posts
Vamos a enlazar un controlador de eventos al evento submit del formulario. Es mejor usar el evento
submit

(en lugar de un click en un botn), ya que cubrir todas las posibles formas de envo (como

por ejemplo pulsar intro).

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});

client/templates/posts/post_submit.js

Commit 7-1
Nueva pgina de envo y enlace a ella desde la cabecera.
Ver en GitHub

Lanzar instancia

Esta funcin utiliza jQuery para analizar los valores de los distintos campos del formulario y rellenar
un objeto post con los resultados. Tenemos que asegurarnos de usar preventDefault para que el
navegador no intente enviar el formulario si volvemos atrs o adelante despus.
Al final, podemos dirigirnos a la pgina de nuestro nuevo post. La funcin insert() devuelve el
identificador _id del objeto que se ha insertado en la base de datos, que podemos pasar a la funcin
go()

del router para que nos lleve a la pgina correcta.

El resultado es que el usuario pulsa en submit , se crea un nuevo post, y vamos inmediatamente a la
pgina de discusin de ese nuevo post.

Aadiendo algo de seguridad

Tal como est ahora, cualquiera que visite la web puede crear posts. Para evitarlo, debemos hacer que
los usuarios inicien sesin. Podramos ocultar el nuevo formulario, pero an as, se podra seguir
haciendo desde la consola.
Afortunadamente, Meteor gestiona la seguridad de las colecciones de la forma adecuada, lo que
ocurre es que, por defecto, esta caracterstica viene desactivada. Esto es as para permitirnos empezar
con facilidad a construir la aplicacin, dejando las cosas aburridas para ms tarde.
Es el momento de eliminar el paquete insecure :

meteor remove insecure

Terminal

Despus de hacerlo, nos damos cuenta de que el formulario de posts ya no funciona. Esto es as,
porque sin el paquete insecure , no se permiten inserciones en la coleccin de posts desde el lado del
cliente.
Necesitamos escribir reglas explcitas para decirle a Meteor qu usuarios pueden insertar posts o
hacer que las inserciones se hagan en el lado del servidor.

Permitir insertar posts


Para que nuestro formulario funcione de nuevo, vamos a ver cmo permitir posts del lado del cliente.
Como veremos, al final usaremos una tcnica diferente, pero por ahora, lo pondremos todo a
funcionar de nuevo, de una forma sencilla: en collections/posts.js :

Posts = new Mongo.Collection('posts');


Posts.allow({
insert: function(userId, doc) {
// only allow posting if you are logged in
return !! userId;
}
});

lib/collections/posts.js

Commit 7-2
Eliminado el paquete `insecure` y permitido aadir posts
Ver en GitHub

Lanzar instancia

Llamamos a Posts.allow , que le dice a Meteor que se trata de un conjunto de circunstancias en las
que a los clientes se les permite hacer cosas en la coleccin de Posts . En este caso, estamos
diciendo: a los clientes se les permite insertar posts siempre y cuando tengan un userId .
El userId que realiza la modificacin se pasa a las funciones allow y deny (o devuelve null si no
hay ningn usuario conectado). Como las cuentas de usuario forman parte del ncleo de Meteor,
podemos confiar en que el userId siempre ser el correcto.
Nos las hemos arreglado para asegurarnos de que un usuario tiene que estar registrado para crear un
mensaje. Salimos de la sesin e intentamos crear un post para ver lo que sale por la consola del
navegador:

Error en la insercin: Acceso denegado.

Sin embargo, todava tenemos que tratar con unas cuantas cosas:
Los usuarios que no han iniciado sesin an pueden ver el formulario.
El post no est vinculado al usuario de ninguna forma.
Se pueden crear mltiples posts que apunten a la misma URL.
Vamos a corregir estos problemas.

Asegurar el acceso al formulario


Vamos a empezar por evitar que los usuarios no registrados puedan ver el formulario de envo de
posts. Lo haremos a nivel de router, definiendo una accin (hook) del router.
Una accin intercepta el proceso de enrutamiento y, potencialmente, cambia la accin que lleva
acabo el router. Puedes pensar en l como en un guardia de seguridad que verifica tus credenciales

antes de dejarte entrar.


Lo que tenemos que hacer es comprobar si el usuario est conectado. Si no lo est, mostramos la
plantilla accessDenied en lugar de la plantilla postSubmit (en este momento le diremos al router
que no haga nada ms). As que vamos a modificar router.js :

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
this.render('accessDenied');
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});

lib/router.js

Adems, tenemos que crear una plantilla para la pgina de error:

<template name="accessDenied">
<div class="access-denied page jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>

client/templates/includes/access_denied.html

Commit 7-3
Acceso denegado al envo de posts a usuarios no registrados.
Ver en GitHub

Lanzar instancia

Si ahora nos dirigimos a http://localhost:3000/submit/ sin estar registrados, veremos el mensaje de


error:

Plantilla de error de acceso

Lo bueno de las acciones del router es que son reactivas. Esto significa que no necesitamos pensar en
funciones de retorno cuando el usuario se autentica: cuando el estado de autenticacin del usuario
cambia, la plantilla del Router cambia instantneamente de accessDenied a postSubmit sin tener
que escribir explcitamente cdigo para manejarlo (y adems, esto funciona incluso en las otras
pestaas del navegador).
Iniciemos sesin, y vayamos a la pgina para crear un nuevo post. Ahora actualizar la pgina en el
navegador. Veremos que, por un instante, se ve la plantilla accessDenied antes de que aparezca el
formulario. Esto es porque Meteor empieza a mostrar las plantillas tan pronto como sea posible, antes
de haber hablado con el servidor y comprobado si el usuario existe.
Para evitar este problema (que es uno de los ms comunes que nos podemos encontrar cuando
tratamos de lidiar con la latencia entre el cliente y el servidor), solo mostraremos una pantalla de
espera durante un instante en el que esperamos para ver si el usuario tiene acceso o no.
Despus de todo en este momento no sabemos si el usuario tiene acceso y no podemos mostrar
ninguna de las plantillas, ya sea la de accessDenied o la de postSubmit hasta que lo sepamos.
As que vamos a modificar nuestra accin para aadir la plantilla de espera mientras
Meteor.loggingIn()

sea verdadero en:

//...
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});

lib/router.js

Commit 7-4
Mostrar la pantalla de carga mientras esperamos al login.
Ver en GitHub

Lanzar instancia

Ocultando el enlace
La forma fcil de evitar que los usuarios lleguen al formulario es esconder el enlace. Podemos hacerlo
fcilmente desde header.html :

//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{
/if}}
</ul>
//...

client/templates/includes/header.html

Commit 7-5
No mostrar el enlace a la pgina de envo si el usuario n
Ver en GitHub

Lanzar instancia

El paquete accounts nos ofrece el ayudante currentUser que es el equivalente a Meteor.user()


en Spacebars. Puesto que es reactivo, el enlace aparecer o desaparecer segn el estado del usuario.

Meteor.methods para mejorar la seguridad y la abstraccin


Nos las hemos arreglado para asegurar el acceso a la pgina de entrada de posts, y no permitir crear
posts a usuarios no registrados incluso si intentan hacerlo desde la consola. Sin embargo, todava
quedan cosas que debemos mejorar:
Aadir el timestamp de los posts.
Asegurarse de que no hay URLs duplicadas.
Aadir detalles sobre el autor del post (ID, nombre de usuario, etc.)
Podramos pensar en hacer todo esto en nuestro controlador submit . Pero, hacindolo de esta
forma, nos encontraramos con un montn de problemas.

Para el timestamp, tendramos que confiar en la hora de la mquina del usuario.


Los clientes no conocern todas las URL publicadas. Solo conocen los posts que pueden ver
en ese momento (veremos porqu), as que no podemos asegurar desde el lado del cliente que
las URLs sean nicas.
Por ltimo, aunque podramos aadir la informacin de usuario en el lado del cliente,
estaramos abriendo nuestra aplicacin a posibles ataques de usuarios usando la consola del
navegador.
Por todas estas razones, es mejor mantener nuestros controladores de eventos simples y, si queremos
hacer ms inserciones o actualizaciones en las colecciones, debemos usar mtodos.
Un mtodo en Meteor es una funcin del lado del servidor que se llama desde el lado del cliente. Ya
estamos familiarizados con ellos de hecho, entre bastidores, la insercin, la actualizacin y el
borrado de datos de la coleccin, son mtodos. Vamos a ver cmo crear el nuestro.
Volvamos a post_submit.js . En lugar de insertar directamente en la coleccin Posts , vamos a
llamar a un mtodo llamado postInsert :

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});

client/templates/posts/post_submit.js

La funcin Meteor.call llama a un mtodo nombrado por su primer argumento. Se pueden


proporcionar argumentos a la llamada (en este caso, pasamos el objeto post que hemos construido
del formulario), y, finalmente, habilitamos un callback, que se ejecutar cuando el mtodo del lado
del servidor finalice.
Las funciones de retorno de los mtodos Meteor siempre tienen dos argumentos, error y result .
Si por cualquier razn el argumento error existe, avisaremos al usuario (usando return para
finalizar la funcin). Si todo ha funcionado bien, redirigiremos al usuario a la pgina de discusin del
post recin creado.

Comprobaciones de seguridad
Aprovecharemos esta oportunidad para aadir algo de seguridad a nuestros mtodos usando el
paquete audit-argument-checks .
Este paquete nos permite realizar comprobaciones sobre un objeto JavaScript usando patrones
predefinidos. En nuestro caso, lo usaremos para comprobar que el usuario que est invocando el
mtodo est correctamente autenticado (asegurndonos que Meteor.userId() es de tipo String ),
y que el objeto postAttributes pasado como argumento al mtodo contiene las cadenas title y
url

, para no terminar insertando cualquier dato extrao en nuestra base de datos.

Vamos a definir el mtodo postInsert en nuestro fichero collections/posts.js . Eliminaremos el


bloque allow() del fichero posts.js porque usando mtodos, Meteor no lo evala.
Extenderemos ( extend ) el objeto postAttributes con tres propiedades ms: el identificador del
usuario _id y el username , adems de la fecha y hora submitted , antes de insertarlos en nuestra
base de datos y devolver el _id al cliente (en otras palabras, a la funcin original que llam a este
mtodo) como un objeto JavaScript.

Posts = new Mongo.Collection('posts');


Meteor.methods({
postInsert: function(postAttributes) {
check(Meteor.userId(), String);
check(postAttributes, {
title: String,
url: String
});
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});

lib/collections/posts.js

Fjate que el mtodo _.extend() forma parte de la librera Underscore, que simplemente nos
permite extender un objeto con propiedades de otro.

Commit 7-6
Usando un mtodo para enviar un post.
Ver en GitHub

Lanzar instancia

Adis Allow/Deny
Los mtodos Meteor son ejecutados en el servidor, por lo que Meteor supone que son de
confianza. Por tanto, los mtodos Meteor obvian las llamadas a allow y deny .
Si quieres ejecutar algn cdigo antes de cada operacin de insert , update , o remove
incluso en el lado servidor, te sugerimos echar un vistazo al paquete collection-hooks.

Evitando duplicidades
Vamos a hacer una comprobacin ms antes de dar por bueno nuestro mtodo. Si ya tenemos un
post con la misma URL, no vamos a permitir que se aada una segunda vez, por el contrario,
redirijamos al usuario al post ya existente.

Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});

lib/collections/posts.js

Buscamos en nuestra base de datos las URLs duplicadas. Si se encuentra alguna, devolvemos
( return ) el _id del post junto con una marca postExists: true para informar al cliente sobre
esta situacin especial.
Y como estamos lanzando una llamada return , el mtodo se detiene en este punto sin llegar a
ejecutar la sentencia insert , evitndonos elegantemente cualquier duplicidad.
Slo falta usar postExists en nuestro ayudante de eventos en el lado del cliente para mostrarnos un

mensaje de aviso:

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);

// show this result but route anyway


if (result.postExists)
alert('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});

client/templates/posts/post_submit.js

Commit 7-7
Forzando la unicidad de las URLs.
Ver en GitHub

Lanzar instancia

Ordenando los posts


Ahora que tenemos una fecha de envo en todos nuestros posts, tiene sentido asegurarnos que se
estn ordenando usando este atributo. Para ello usaremos el operador sort de Mongo que espera
un objeto que consta de las claves de ordenacin, y un signo que indica si son ascendentes o

descendentes:

Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});

client/templates/posts/posts_list.js

Commit 7-8
Posts ordenados por fecha de envo.
Ver en GitHub

Lanzar instancia

Ha costado, pero Finalmente tenemos una interfaz en la que los usuarios introducen posts de forma
segura en nuestra aplicacin!
Sin embargo, cualquier aplicacin que permita a los usuarios crear contenido tambin debe permitir
editarla o borrarla. Eso es de lo que hablaremos en el siguiente captulo.

Compensacin de la latencia

SIDEBAR

7.5

En el ltimo captulo, se introdujo un nuevo concepto en el mundo Meteor: los mtodos.

Sin compensacin de la latencia

Un mtodo es una forma de ejecutar una serie de comandos en el servidor de una manera
estructurada. En nuestro ejemplo, hemos utilizado un mtodo porque queramos asegurarnos que los
nuevos posts se etiqueten con el nombre e identificador de su autor y con la hora actual del servidor.
Sin embargo, tendramos un problema si Meteor ejecutara los mtodos de una forma ms bsica.
Considera la siguiente secuencia de eventos (nota: las marcas de tiempo son valores aleatorios

escogidos con fines ilustrativos):


+0ms: El usuario hace clic en un botn de envo y el navegador lanza una llamada al mtodo.
+200ms: El servidor realiza cambios en la base de datos Mongo.
+500ms: El cliente recibe estos cambios y actualiza la interfaz de usuario para reflejarlos.
Si fuera as, habra un breve retraso entre la accin y la visualizacin de los resultados (que se notara
ms o menos dependiendo de lo cerca que estuvisemos del servidor).Esto no se puede permitir en
una aplicacin web moderna!

Compensacin de la latencia

Con compensacin de la latencia

Para evitar este problema, Meteor introduce un concepto llamado Compensacin de la Latencia.
Cuando definimos nuestro mtodo Post , lo colocamos dentro de un archivo en el directorio

collections/

. Esto implica que estar disponible tanto para el servidor como para el cliente - y se

ejecutar al mismo tiempo en ambos contextos!


Cuando se hace una llamada a un mtodo, el cliente no solo enva la llamada al servidor, sino que
tambin simula la accin en su propia coleccin. De esta forma, el flujo de trabajo sera:
+0ms: El usuario hace clic en un botn de envo y el navegador lanza una llamada al mtodo.
+0ms: El cliente simula la accin en su propia coleccin y cambia la interfaz de usuario para
reflejar los cambios.
+200ms: El servidor realiza cambios en la base de datos Mongo..
+500ms: El cliente recibe esos cambios deshaciendo los que ha simulado y reemplazndolos
con los del servidor (que, generalmente son los mismos) y la interfaz de usuario los refleja.
Esto se traduce en que el usuario ve los cambios instantneamente. Cuando llega la respuesta del
servidor unos momentos ms tarde, puede haber o no cambios notables. Por tanto, una cosa que
tenemos que aprender es que debemos tratar de asegurar de que, en la medida de lo posible, los
documentos simulados sean muy parecidos a los reales.

Observando la compensacin de la latencia


Haciendo un pequeo cambio en el mtodo post veremos todo esto en accin. Para ello, usaremos
la til funcin Meteor._sleepForMs() para retrasar la llamada de la funcin cinco segundos, pero
(importante) solo en el servidor.
Usaremos isServer para preguntar a Meteor si el mtodo se est invocando en el cliente (como un
stub) o en el servidor. Un stub es la simulacin del mtodo que ejecuta Meteor en el cliente,
mientras el mtodo real se est ejecutando en el servidor.
As que vamos a preguntar a Meteor si se est ejecutando el cdigo en el servidor. Si es as,
retrasaremos el proceso cinco segundos y aadiremos la cadena (server) al final del ttulo de
nuestro post. Si no, aadiremos (client) :

Posts = new Mongo.Collection('posts');


Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
if (Meteor.isServer) {
postAttributes.title += "(server)";
// wait for 5 seconds
Meteor._sleepForMs(5000);
} else {
postAttributes.title += "(client)";
}
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});

collections/posts.js

Si nos detuviramos aqu, la demostracin no sera muy concluyente. En este punto, solo parece que

el envo del formulario est siendo pausado por cinco segundos antes de redirigirte a la lista principal
de posts, y nada ms.
Para entender porqu, volvamos a ver el ayudante de envo:

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);

// show this result but route anyway


if (result.postExists)
alert('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});

client/templates/posts/post_submit.js

Hemos colocado nuestra llamada de enrutamiento Router.go() dentro de la funcin de retorno de


la llamada. Lo que significa que el formulario est esperando a que la llamada al mtodo concluya
antes de redirigir.
Normalmente, esto es el cauce normal de la accin. Despus de todo, no podemos redirigir al usuario
antes de saber si el post es vlido o no, ya que sera extremadamente confuso redirigir, y, al momento,
redirigir de nuevo hacia el formulario de envo original para corregir los datos, todo en cuestin de
segundos.

Pero para el propsito de este ejemplo, queremos ver el resultado de nuestras acciones
inmediatamente. Por lo que cambiaremos la llamada a Router para redirigir a la ruta postsList (no
podemos redirigir al post porque no sabemos su identificador fuera del mtodo). Lo sacaremos fuera
de la llamada, y veremos que pasa:

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);

// show this result but route anyway


if (result.postExists)
alert('This link has already been posted');
});
Router.go('postsList');
}
});

client/templates/posts/post_submit.js

Commit 7-5-1
Demostrar el orden en el que aparecen los posts usando un
Ver en GitHub

Lanzar instancia

Si ahora creamos un nuevo post, vemos claramente la compensacin de la latencia. En primer lugar,

se inserta el post con el ttulo (cliente) (El primer post de la lista, apuntando hacia GitHub):

Nuestro post insertado en la coleccin del cliente

Cinco segundos ms tarde, se reemplaza con el documento real insertado por el servidor:

Nuestro post una vez que el cliente recibe la actualizacin del servidor

Mtodos en el lado del cliente


Despus de todo esto, puedes pensar que todo esto de los mtodos, es complicado, pero en realidad
pueden ser bastante simples. De hecho, ya hemos visto tres mtodos muy sencillos: los mtodos
insert

, update y remove de una coleccin.

Cuando se define una coleccin 'posts' , se estn definiendo implcitamente tres


mtodos: posts/insert , posts/update y posts/delete . En otras palabras, cuando se llama
Posts.insert()

desde el cliente, se llama a un mtodo compensado en latencia que hace dos

cosas:
1. Comprueba si podemos hacer el cambio llamando a los callbacks allow y deny (sin embargo,
esto no tiene porque ser as en la simulacin).
2. Efecta, de verdad, el cambio en el almacn de datos subyacente.

Mtodos que llaman a mtodos


Si has estado atento, te habrs dado cuenta de que el mtodo post llama a su vez a otro mtodo
( posts/insert ) cada vez que insertamos un nuevo post. Y, cmo es que esto funciona?
Mientras se est ejecutando la simulacin (la versin del mtodo en el cliente), ejecutamos un
insert

simulado (lo insertamos en nuestra coleccin cliente), pero realmente no llamamos al

insert

del servidor, porque esperamos que sea la versin de post que hay en el servidor la que lo

haga.
En consecuencia, cuando el mtodo post del servidor llama a insert no tiene necesidad de
preocuparse por la simulacin, y la insercin se realiza sin problemas.
Como anteriormente, no olvides deshacer los cambios antes de avanzar al siguiente captulo.

Editando posts

Ahora que ya podemos crear posts, el siguiente paso es poder editarlos y borrarlos. Como el cdigo de
la IU ha quedado bastante simple, este parece un buen momento para hablar de cmo se gestionan
los permisos de usuario con Meteor.
Primero vamos a configurar nuestro router. Aadiremos una ruta para acceder a la pgina de edicin y
estableceremos su contexto de datos:

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});

lib/router.js

La plantilla de edicin de posts


Ahora ya nos podemos centrar en la plantilla. Nuestra plantilla postEdit tendr una forma bastante
estndar:

<template name="postEdit">
<form class="main form page">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="{{url}}" placeholder="Your
URL" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="{{title}}" placeholder
="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary submit"/>
<hr/>
<a class="btn btn-danger delete" href="#">Delete post</a>
</form>
</template>

client/templates/posts/post_edit.html

Y aqu tenemos el fichero post_edit.js que la acompaa:

Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
alert(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});

client/templates/posts/post_edit.js

Como podemos ver, la mayora de cosas ya nos son familiares.


Tenemos dos callbacks de eventos: uno para enviar el formulario submit , y click .delete para el
evento click del enlace delete.
El callback delete es muy simple: elimina el evento predeterminado y despus pide confirmacin. Si
confirmamos, obtenemos el ID del post actual desde el contexto de datos de la plantilla, lo borramos

y redirigimos al usuario a la pgina de inicio.


El callback de actualizacin es un poco ms largo, pero no mucho ms complicado. Despus de
suprimir el evento predeterminado y conseguir el post actual, obtiene los nuevos valores del
formulario y los almacena en el objeto postProperties .
A continuacin, pasa este objeto al mtodo Meteor Collection.update() usando el operador $set
(que reemplaza un conjunto de atributos dejando los dems intactos), y usa un callback para mostrar
un error si falla la actualizacin o enva al usuario a la pgina del post si la actualizacin se realiza
correctamente.

Aadiendo enlaces
Deberamos aadir enlaces a la pgina de edicin de nuestros posts para que los usuarios puedan
llegar a ella:

<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}}
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>

client/templates/posts/post_item.html

Por supuesto, no queremos que se muestre el enlace para editar un post que no haya sido creado por
ese usuario. Aqu es donde entra el ayudante ownPost :

Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});

client/templates/posts/post_item.js

Formulario de edicin.

Commit 8-1
Aadido el formulario de edicin.
Ver en GitHub

Lanzar instancia

Ya tenemos nuestro formulario de envo de edicin, pero en realidad, todava no se puede editar
nada. Qu es lo que est pasando?

Configurando los permisos


Al haber eliminado el paquete insecure , se nos deniegan todas las peticiones de modificacin
desde el cliente.
Para solucionarlo, estableceremos algunas reglas de permisos. Primero, creamos un nuevo archivo
permissions.js

dentro de lib . Esto nos asegura que nuestra lgica de permisos se cargar lo

primero (y estar disponible en los dos entornos):

// check that the userId specified owns the documents


ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}

lib/permissions.js

En el captulo Creando posts nos libramos de tener que usar el mtodo allow() porque estbamos
insertando nuevos posts a travs de un mtodo de servidor.
Pero ahora que estamos editando y borrando posts desde el cliente, vamos a necesitar volver a
collections/posts.js

y a aadir un bloque allow() :

Posts = new Mongo.Collection('posts');


Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); },
});

//...

lib/collections/posts.js

Commit 8-2
Aadidos permisos bsicos para comprobar el dueo del post.
Ver en GitHub

Lanzar instancia

Limitando las ediciones


Solo porque podamos editar nuestros propios posts, no significa debamos ser capaces de editar todas
las propiedades. Por ejemplo, no queremos que los usuarios puedan crear un post y luego asignrselo
a otro usuario.
Utilizaremos el callback deny() para asegurarnos de que los usuarios solo puedan editar los
atributos especificados:

Posts = new Mongo.Collection('posts');


Posts.allow({
update: ownsDocument,
remove: ownsDocument
});
Posts.deny({
update: function(userId, post, fieldNames) {
// may only edit the following two fields:
return (_.without(fieldNames, 'url', 'title').length > 0);
}
});

lib/collections/posts.js

Commit 8-3
Permitir cambios slo en ciertos campos.
Ver en GitHub

Lanzar instancia

Estamos cogiendo el array fieldNames que contiene la lista de los campos que quieren modificar, y
usamos el mtodo without() de Underscore para devolver un sub-array que contiene los campos
que no son url o title .
Si todo va bien, el array debe estar vaco y su longitud debe ser 0. Pero si alguien est tratando de
enredar, la longitud del array ser mayor que 0, y devolveremos true (denegando as la
actualizacin).
Te habrs dado cuenta de que, en el cdigo de edicin del post, no comprobamos si hay enlaces
duplicados. Esto significa que un usuario podra enviar un enlace y despus editarlo y cambiar su URL
para saltarse la comprobacin. La solucin a este problema podra ser utilizar un mtodo Meteor para
tratar este formulario de edicin, pero dejaremos esto como un ejercicio para el lector.

Mtodos en el servidor vs. mtodos en el cliente


Para crear un post , estamos utilizando un mtodo postInsert en el servidor, mientras
que para editarlos y eliminarlos, llamamos a update y remove directamente desde el
cliente, limitando el acceso a travs de allow y deny .
Cundo es ms adecuado usar uno u otro?
Cuando las cosas son relativamente sencillas se pueden expresar las reglas a travs de
allow

y deny , por lo general es ms fcil de hacer las cosas directamente desde el cliente.

Sin embargo, tan pronto como empecemos a hacer cosas que deberan estar fuera del
control del usuario (por ejemplo, el timestamp de un nuevo post o asignarlo al usuario
correcto), ser mejor que usemos mtodos.
Las llamadas a mtodos tambin son apropiadas en algunos otros casos:
Cuando necesitamos conocer o devolver valores a travs de un callback en lugar de
esperar a que Meteor propague la sincronizacin.
Para consultas pesadas a la base de datos.
Para resumir o agregar datos (por ejemplo, contadores, promedios, sumas).
Para conocer ms a fondo este tema echa un vistazo a nuestro blog.

Permitir y Denegar

SIDEBAR

8.5

El sistema de seguridad de Meteor nos permite controlar los cambios en la base de datos sin tener
que definir los mtodos necesarios para hacerlo.
Nosotros hemos tenido que definir un mtodo post especfico porque necesitamos hacer tareas
adicionales, tales como decorar el post con propiedades adicionales y tomar medidas si la URL del
post ya existe.
Por otro lado, tampoco hemos tenido que crear mtodos para actualizar y eliminar posts. Solo
necesitbamos comprobar si el usuario tena permiso para hacer la accin, y ha sido muy fcil usando
los callbacks allow y deny .
Usar estos callbacks nos ha permitido ser ms declarativos con las modificaciones en la base de
datos, definiendo qu tipo de cambios se pueden hacer. El hecho de que estn integrados en el
sistema de cuentas es, adems, una ventaja aadida.

Mltiples callbacks
Podemos definir todos los callbacks allow que queramos. Solo es necesario que, al menos uno de
ellos devuelva true , para el cambio actual. De esta forma, cuando se llama a Posts.insert desde
un navegador (no importa si es desde el cdigo cliente de nuestra aplicacin o desde la consola), el
servidor llamar a todos los insert -permitidos que pueda hasta que encuentre uno que devuelva
true. Si no encuentra ninguno, no permite la insercin, y se devuelve al cliente un error 403 .
Del mismo modo, podemos definir uno o varios callbacks deny . Si cualquiera de ellos devuelve
true

, el cambio se cancela y se devuelve un 403 . La lgica de todo esto implica que, para que un

insert

tenga xito, se ejecutarn uno o varios callbacks allow as como todos los deny .

Nota: n/e significa No Ejecutado

En otras palabras, Meteor recorre la lista de callbacks empezando por los deny , y luego ejecuta todos
los allow hasta que uno devuelve true .
Un ejemplo prctico de este patrn podra ser, por ejemplo, tener dos callbacks allow() , uno
comprueba si un post pertenece al usuario actual, y otro si el usuario actual es un administrador. Si es
un administrador, se asegura que el usuario ser capaz de actualizar cualquier post, ya que al menos
uno de los callbacks devolver true.

Compensacin de la latencia
Recuerda que los mtodos que provocan cambios en la base de datos (como .update() ) compensan
la latencia, igual que cualquier otro mtodo. As, por ejemplo, si desde la consola del navegador,
intentas eliminar un post que no te pertenece, vers que, por un momento, el post desaparece,
porque la coleccin local pierde el documento, pero luego vuelve a aparecer cuando el servidor nos
dice que no, que el documento no ha sido eliminado.
Por supuesto, este comportamiento no es un problema cuando se activa desde la consola (despus
de todo, si los usuarios juegan con los datos desde la consola, no es problema nuestro lo que ocurra
en sus navegadores). Sin embargo, necesitas asegurarte de que esto no sucede desde la interfaz de

usuario. Por ejemplo, necesitas tomarte la molestia de asegurar que no muestras a los usuarios
botones para eliminar documentos que no estn autorizados a borrar.
Afortunadamente, no suele requerir demasiado cdigo extra poner el cdigo que define los permisos,
compartido entre el cliente y el servidor (por ejemplo, podras escribir una funcin
canDeletePost(user, post)

y ponerla en el directorio /lib ).

Permisos en el lado del servidor


Recuerda que el sistema de permisos solo se aplica a cambios en la base de datos iniciados desde el
cliente. En el servidor, Meteor asume que se permiten todas las operaciones.
Esto significa que si escribes un mtodo DeletePost en el lado del servidor, que puede llamarse
desde el cliente, todo el mundo podr borrar cualquier post. As que, es probable que no quieras
hacer esto, a menos que compruebes los permisos de usuario dentro de ese mtodo.

Errores

Resulta poco elegante usar un alert() para advertir al usuario de que algo ha pasado. Hay que
hacerlo mejor.
Vamos a construir un mecanismo de presentacin de errores ms verstil y que va a hacer mejor el
trabajo de informar al usuario sobre lo que est pasando sin tener que romper su flujo de trabajo.
Vamos a implementar un simple sistema que muestre los nuevos errores en la parte de arriba a la
derecha de la ventana, de forma similar a la popular aplicacin Growl de MacOS.

Introduciendo las colecciones locales


Para empezar, crearemos una coleccin para almacenar nuestros errores. Puesto que los errores solo
son relevantes en la sesin actual y no necesitan ser persistentes, haremos algo nuevo, vamos a crear
una coleccin local. Es decir, la coleccin Errors solo existir en el navegador, y no se sincronizar
con el servidor.
Para conseguirlo, creamos la coleccin dentro del directorio client (para hacer que slo est
disponible en la parte cliente), con el nombre de la coleccin establecido a null (ya que los datos
nunca se almacenarn en la base de datos del servidor):

// Local (client-only) collection


Errors = new Mongo.Collection(null);

client/helpers/errors.js

Ahora que hemos creado la coleccin, podemos agregar una funcin throwError , que usaremos
para aadir errores. Al tratarse de una coleccin local, no tenemos que preocuparnos por definir
allow

actual.

o deny u otros mecanismos de seguridad, ya que esta coleccin es local para el usuario

throwError = function(message) {
Errors.insert({message: message});
};

client/helpers/errors.js

La ventaja de utilizar una coleccin local para almacenar los errores es que, como todas las
colecciones, es reactiva lo que significa que podemos mostrar errores de la misma forma que
mostramos cualquier otro dato de una coleccin.

Mostrando Errores
Mostraremos los errores en la parte de arriba de nuestro layout:

<template name="layout">
<div class="container">
{{> header}}
{{> errors}}
<div id="main">
{{> yield}}
</div>
</div>
</template>

client/templates/application/layout.html

Ahora vamos a crear la plantilla de error en errors.html :

<template name="errors">
<div class="errors">
{{#each errors}}
{{> error}}
{{/each}}
</div>
</template>
<template name="error">
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{message}}
</div>
</template>

client/templates/includes/errors.html

Dos plantillas en un archivo


Te habrs dado cuenta de que estamos definiendo dos plantillas en un solo archivo. Hasta
ahora hemos tratado de mantener la regla un archivo, una plantilla, pero con Meteor
podemos poner todas las plantillas en un nico fichero y tambin funcionara (Aunque
tendramos un fichero main.html muy confuso!).
En este caso, como las plantillas son muy pequeas, haremos una excepcin y las
pondremos en el mismo fichero para que nuestro repositorio quede un poco ms limpio.

Solo nos falta integrar el ayudante de plantilla y Estar listo!

Template.errors.helpers({
errors: function() {
return Errors.find();
}
});

client/templates/includes/errors.js

Ya puedes probar nuestros mensajes de error manualmente. Abre una consola en el navegador y
escribe:

throwError("I'm an error!");

Probando mensajes de error.

Commit 9-1
Mostrando errores.
Ver en GitHub

Lanzar instancia

Dos clases de errores


Llegados a este punto es importante hacer distincin entre los errores a nivel de aplicacin
y los errores a nivel de cdigo.
Los errores a nivel de aplicacin son, generalmente, generados por el usuario, y los usuarios
pueden interactuar con ellos. Esto incluye cosas como los errores de validacin, errores de
permisos, errores de no encontrado, etc. Estos son la clase de errores que queremos
mostrar al usuario para ayudarle a arreglar el problema que hemos encontrado.
Los errores a nivel de cdigo, en cambio, se generan de forma inesperada por fallos en
nuestro cdigo, y que probablemente no queremos mostrarle al usuario directamente, si no
que queremos registrarlo con alguna clase de sistema de registro de errores (como por
ejemplo Kadira).
En este captulo, nos centraremos en tratar con el primer tipo de error, no en capturar fallos.

Creando errores
Ya sabemos cmo mostrar los errores, pero todava tenemos que crearlos antes de poder verlos. Ya
hemos implementado un buen escenario para mostrarlos: el aviso de post duplicado. Sencillamente
remplazaremos las llamadas a alert en el ayudante de eventos de postSubmit con la nueva
funcin throwError que acabamos de crear:

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return throwError(error.reason);

// show this result but route anyway


if (result.postExists)
throwError('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});

client/templates/posts/post_submit.js

Ya que estamos, hacemos lo mismo con el ayudante de postEdit :

Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
throwError(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
//...
});

client/templates/posts/post_edit.js

Commit 9-2
Usando el mecanismo de presentar errores.
Ver en GitHub

Lanzar instancia

Vamos a probar: intenta crear un post e introduce la URL http://meteor.com . Como esta URL ya
existe en un post en nuestros datos de ejemplo, deberamos ver:

Disparando un error

Limpiando los errores


Te habrs dado cuenta de que los mensajes de error desaparecen por s mismos despus de unos
segundos. Esto es debido un poco de magia CSS incluida en la hoja de estilos que hemos aadido al
principio del libro:

@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
//...
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
//...
}

client/stylesheets/style.css

Estamos definiendo una animacin CSS fadeOut que especifica cuatro keyframes para la propiedad
opacidad (al 0%, 10%, 90% y 100% del total de la duracin de la animacin), y aplicando esta
animacin a la clase alert .
La animacin se ejecuta durante 2700 milisegundos en total, usa la ecuacin de tiempo ease-in , se
inicia con un retardo de 0 segundos, se ejecuta una sola vez, y finalmente permanece en el ltimo
keyframe una vez que se ha terminado de ejecutar.

Animaciones contra Animaciones


Te estars preguntando porqu estamos usando animaciones basadas en CSS (que son
predeterminadas y estn fuera del control de nuestra aplicacin), en lugar de animaciones
controladas por el propio Meteor.
Si bien Meteor proporciona soporte para insertar animaciones, queramos en este captulo
enfocarnos en los errores. As que, por ahora, usaremos una simple animacin CSS y
dejaremos esas cosas para el captulo de las animaciones.

Esto funciona, pero si lanzamos varios errores (enviando el mismo enlace tres veces por ejemplo)
notars que se apilarn uno encima de otro:

Stack overflow.

Esto pasa porque mientras los elementos .alert estn desapareciendo visualmente, todava estn
presentes en el DOM. Necesitamos corregir esto.
Esta es la clase de situaciones en las que Meteor reluce. Puesto que la coleccin Errors es reactiva,
Todo lo que necesitamos para deshacernos de estos antiguos errores es eliminarlos de la coleccin!
Usaremos Meteor.setTimeout para especificar una funcin que se ejecute despus de que se expire
el tiempo de espera (en este caso, 3000 milisegundos).

Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Template.error.onRendered(function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.remove(error._id);
}, 3000);
});

client/templates/includes/errors.js

Commit 9-3
Limpiar errores despus de 3 segundos.
Ver en GitHub

Lanzar instancia

La llamada a onRendered se lanza una vez que nuestra plantilla ha sido renderizada en el navegador.
Dentro de esta llamada, this hace referencia a la instancia actual de la plantilla, y this.data da
acceso a los datos del objeto que est siendo renderizado (en nuestro caso, un error).

En busca de las validaciones


An no hemos impuesto ninguna clase de validacin en nuestro formulario. Queremos que los
usuarios indiquen una URL y un ttulo para cada post. Vamos a asegurarnos de esto sea as.
Haremos dos cosas para comprobar campos ausentes: primero, aadiremos una clase css especial
has-error

al div padre de cualquier campo de formulario. Segundo, mostraremos un mensaje de

error justo debajo del campo.

Para comenzar, vamos a preparar nuestra plantilla postSubmit para aceptar este nuevo tipo de
ayudantes:

<template name="postSubmit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" c
lass="form-control"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name yo
ur post" class="form-control"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>

client/templates/posts/post_submit.html

Fjate que estamos pasando parmetros ( url y title respectivamente) a cada ayudante. Esto nos
permite reutilizar el mismo ayudante, modificando su comportamiento segn el parmetro.
Ahora la parte divertida: hacer que estos ayudantes hagan realmente algo.
Usaremos la Session para almacenar un objeto postSubmitErrors que contendr el potencial
mensaje de error. Segn el usuario interacta con el formulario, este objeto ir cambiando, por lo que
ir cambiando reactivamente el estilo y contenido del formulario.
Primero, iniciamos el objeto donde se crea la plantilla postSubmit . Esto nos asegura que el usuario
no ve un mensaje de error antiguo que se haya quedado de una visita anterior a esta pgina.

Despus definimos dos ayudantes de plantilla. Ambos buscarn la propiedad field del objeto
Session.get('postSubmitErrors')

(donde field ser url o title dependiendo del ayudante

desde que el que se llame).


Mientras que errorMessage solo devuelve el mensaje en s mismo, errorClass comprueba la
presencia de un mensaje y devuelve has-error en caso de que exista un mensaje.

Template.postSubmit.onCreated(function() {
Session.set('postSubmitErrors', {});
});
Template.postSubmit.helpers({
errorMessage: function(field) {
return Session.get('postSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
}
});

//...

client/templates/posts/post_submit.js

Puedes comprobar que nuestros ayudantes estn funcionando correctamente abriendo una consola
en el navegador y escribiendo la siguiente lnea de cdigo:

Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasin


g robo-dogs.'});

Consola del navegador

Alerta roja! Alerta roja!

El prximo paso es enganchar el objeto de sesin postSubmitErrors al formulario.


Antes de hacerlo, crearemos una nueva funcin validatePost en el fichero posts.js que mire en
el objeto post , y devuelva un objeto errors con cualquier error relevante (que los campos title
o url no estn presentes):

//...
validatePost = function (post) {
var errors = {};
if (!post.title)
errors.title = "Please fill in a headline";
if (!post.url)
errors.url =

"Please fill in a URL";

return errors;
}

//...

lib/collections/posts.js

Llamaremos a esta funcin desde el ayudante de eventos de postSubmit :

Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
var errors = validatePost(post);
if (errors.title || errors.url)
return Session.set('postSubmitErrors', errors);
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return throwError(error.reason);

// show this result but route anyway


if (result.postExists)
throwError('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});

client/templates/posts/post_submit.js

Fjate que estamos usando return para abortar la ejecucin del ayudante si hay errores, no porque
queramos devolver ningn valor concreto.

Capturando errores.

Validaciones en el lado servidor


Ya hemos terminado?. Estamos validando la presencia de una URL y un ttulo en el lado del cliente,
Pero que pasa con el servidor? Despus de todo, alguien podra aadir un post llamando
manualmente al mtodo postInsert desde la consola del navegador.
Aunque pienses que no necesitamos mostrar ningn mensaje de error en el servidor, podemos hacer
uso de la funcin validatePost . Solo que esta vez la llamaremos desde el mtodo postInsert
tambin, y no slo desde el ayudante de eventos:

Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var errors = validatePost(postAttributes);
if (errors.title || errors.url)
throw new Meteor.Error('invalid-post', "You must set a title and URL for you
r post");
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});

lib/collections/posts.js

De nuevo, los usuarios no deberan nunca ver este mensaje You must set a title and URL for your
post. Slo se mostrar si alguien quiere saltarse los controles que hemos dispuesto
concienzudamente, y usa la consola del navegador.

Para probarlo, abre una consola del navegador y prueba a enviar un post sin URL:

Meteor.call('postInsert', {url: '', title: 'No URL here!'});

Si hemos hecho bien nuestro trabajo, obtendrs un montn de cdigo extrao junto con un mensaje
You must set a title and URL for your post.

Commit 9-4
Validar el contenido de la post al enviar.
Ver en GitHub

Lanzar instancia

Validacin al editar
Pare redondear las cosas, aplicaremos la misma validacin a nuestro formulario de edicin de posts.
El cdigo es muy similar. Primero, la plantilla:

<template name="postEdit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="{{url}}" placeholder="Your
URL" class="form-control"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="{{title}}" placeholder
="Name your post" class="form-control"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary submit"/>
<hr/>
<a class="btn btn-danger delete" href="#">Delete post</a>
</form>
</template>

client/templates/posts/post_edit.html

Luego los ayudantes de plantilla:

Template.postEdit.onCreated(function() {
Session.set('postEditErrors', {});
});
Template.postEdit.helpers({
errorMessage: function(field) {
return Session.get('postEditErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
}
});
Template.postEdit.events({
'submit form': function(e) {

e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
var errors = validatePost(postProperties);
if (errors.title || errors.url)
return Session.set('postEditErrors', errors);
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
throwError(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});

client/templates/posts/post_edit.js

Tal y como hicimos en el formulario de envo de posts, queremos validar nuestros posts en el servidor.
Excepto que recordars que no estamos usando un mtodo para editar los posts, si no una llamada
update

directamente desde el lado cliente.

Esto significa que tenemos que aadir una nueva regla deny :

//...
Posts.deny({
update: function(userId, post, fieldNames, modifier) {
var errors = validatePost(modifier.$set);
return errors.title || errors.url;
}
});

//...

lib/collections/posts.js

Fjate que el argumento post se refiere al post existente. En este caso, queremos validar la
actualizacin, que es por lo que estamos llamando a validatePost con el contenido del modificador
de la propiedad $set (como en Posts.update({$set: {title: ..., url: ...}}) ).
Esto funciona porque modifier.$set contiene las mismas dos propiedades title y url que el
objeto post . Por supuesto, esto quiere decir que cualquier actualizacin parcial que afecte solo a
title

o url fallar, pero en la prctica no debera ser una complicacin.

Te habrs dado cuenta de que esta es nuestra segunda llamada deny . Cuando aadimos mltiples
llamadas deny , la operacin fallar si uno de ellos devuelve true . En este caso, esto significa que el
update

solo ser satisfactorio si estamos modificando el title y la url , y ninguna de ellas est

vaca.

Commit 9-5
Validar el contenido del post al editar.
Ver en GitHub

Lanzar instancia

Creando un paquete Meteor

SIDEBAR

9.5

Durante nuestro trabajo en los errores, hemos construido un modelo reutilizable, por qu no ponerlo
dentro de un paquete y compartirlo con el resto de la comunidad Meteor?
Para empezar, tenemos que asegurarnos de que tenemos una cuenta de desarrollador Meteor.
Puedes hacerte una en meteor.com, pero es muy probable que ya lo hayas hecho cuando te
registraste para el libro! En cualquier caso, deberas saber cul es tu nombre de usuario, porque lo
utilizaremos bastante durante este captulo.
En este captulo usaremos tmeasday como usuario puedes cambiarlo por el tuyo.
En primer lugar, necesitamos crear una estructura de carpetas para nuestro paquete. Podemos usar el
comando meteor create --package tmeasday:errors para conseguirla. Fjate que Meteor ha
creado una carpeta llamada packages/tmeasday:errors/ , con algunos ficheros dentro.
Comenzaremos por editar package.js , el archivo que informa a Meteor de cmo debe utilizar el
paquete, y los smbolos y funciones que exporta.

Package.describe({
name: "tmeasday:errors",
summary: "A pattern to display application errors to the user",
version: "1.0.0"
});
Package.onUse(function (api, where) {
api.versionsFrom('0.9.0');
api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');
api.addFiles(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');
if (api.export)
api.export('Errors');
});

packages/tmeasday:errors/package.js

Cuando desarrollamos un paquete para su uso en el mundo-real, es una buena prctica rellenar la
seccin Package.describe con la URL del repositorio Git (como por ejemplo,
https://github.com/tmeasday/meteor-errors.git

). De esta manera, los usuarios pueden acceder

al cdigo fuente, y (si usas GitHub) ver el README del paquete en Atmosphere.
Vamos a aadir al paquete los tres archivos que se pasan en la llamada a add_files . Podemos usar
los mismos que tenemos para Microscope, haciendo solo, unos pequeos cambios para los espacios
de nombres y para dejar la API un poco ms limpia:

Errors = {
// Local (client-only) collection
collection: new Mongo.Collection(null),
throw: function(message) {
Errors.collection.insert({message: message, seen: false})
}
};

packages/tmeasday:errors/errors.js

<template name="meteorErrors">
<div class="errors">
{{#each errors}}
{{> meteorError}}
{{/each}}
</div>
</template>
<template name="meteorError">
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{message}}
</div>
</template>

packages/tmeasday:errors/errors_list.html

Template.meteorErrors.helpers({
errors: function() {
return Errors.collection.find();
}
});
Template.meteorError.rendered = function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.collection.remove(error._id);
}, 3000);
};

packages/tmeasday:errors/errors_list.js

Probando el paquete con Microscope


Vamos a probar el paquete localmente con Microscope para asegurarnos de que nuestros cambios
funcionan. Para enlazar el paquete en nuestro proyecto, ejecutaremos meteor add
tmeasday:errors

. A continuacin, debemos eliminar los archivos a los que reemplaza el nuevo

paquete:

rm client/helpers/errors.js
rm client/templates/includes/errors.html
rm client/templates/includes/errors.js

Eliminando los archivos antiguos

Otra cosa que debemos hacer son algunos pequeos cambios en el cdigo de la aplicacin para que
use la API correcta:

{{> header}}
{{> meteorErrors}}

client/templates/application/layout.html

Meteor.call('postInsert', post, function(error, id) {


if (error) {
// display the error to the user
Errors.throw(error.reason);

client/templates/posts/post_submit.js

Posts.update(currentPostId, {$set: postProperties}, function(error) {


if (error) {
// display the error to the user
Errors.throw(error.reason);

// show this result but route anyway


if (result.postExists)
Errors.throw('This link has already been posted');

client/templates/posts/post_edit.js

Commit 9-5-1
Creado y enlazado un paquete bsico.
Ver en GitHub

Lanzar instancia

Una vez hechos estos cambios, deberamos ver el mismo comportamiento que tenamos con el
cdigo sin empaquetar.

Escribiendo Tests
El primer paso en el desarrollo de un paquete es probarlo contra una aplicacin, pero el siguiente es
escribir un conjunto de tests que evalen adecuadamente el comportamiento del paquete. Meteor

incluye Tinytest, que permite ejecutar este tipo de pruebas de forma fcil y, de esta forma, tener la
conciencia tranquila cuando compartimos el paquete con los dems.
Vamos a crear un archivo que usa Tinytest para ejecutar tests contra el cdigo de los errores.

Tinytest.add("Errors - collection", function(test) {


test.equal(Errors.collection.find({}).count(), 0);
Errors.throw('A new error!');
test.equal(Errors.collection.find({}).count(), 1);
Errors.collection.remove({});
});
Tinytest.addAsync("Errors - template", function(test, done) {
Errors.throw('A new error!');
test.equal(Errors.collection.find({}).count(), 1);

// render the template


UI.insert(UI.render(Template.meteorErrors), document.body);
Meteor.setTimeout(function() {
test.equal(Errors.collection.find({}).count(), 0);
done();
}, 3500);
});

packages/tmeasday:errors/errors_tests.js

Con estos tests comprobamos que las funciones bsicas de Meteor.Errors funcionan
correctamente, as como que el cdigo mostrado en la plantilla sigue funcionando bien.
No vamos a cubrir los aspectos especficos sobre cmo escribir tests de paquetes (porque la API
todava no est acabada y podra cambiar mucho), pero viendo el cdigo, puedes hacerte una idea
cmo funciona.
Para decirle a Meteor que ejecute los tests, aadimos este cdigo a package.js

Package.onTest(function(api) {
api.use('tmeasday:errors', 'client');
api.use(['tinytest', 'test-helpers'], 'client');
api.addFiles('errors_tests.js', 'client');
});

packages/tmeasday:errors/package.js

Commit 9-5-2
Tests aadidos al paquete.
Ver en GitHub

Ya podemos ejecutar los tests con:

meteor test-packages tmeasday:errors

Terminal

Lanzar instancia

Pasando todos los tests

Publicando el paquete
Ahora, queremos liberar el paquete y ponerlo a disposicin de todo el mundo. Para ello tendremos
que subirlo al servidor de paquetes de Meteor y, hacerlo miembro del repositorio Atmosphere.
Afortunadamente, es muy fcil. Entramos en el directorio del paquete, y ejecutamos meteor publish
--create

cd packages/tmeasday:errors
meteor publish --create

Terminal

Ahora que hemos publicado el paquete, podemos eliminarlo del proyecto y luego aadirlo de nuevo
directamente:

rm -r packages/errors
meteor add tmeasday:errors

Terminal (ejecutar desde el directorio raz de la aplicacin)

Commit 9-5-4
Paquete eliminado del rbol de desarrollo.
Ver en GitHub

Lanzar instancia

Ahora debemos ver a Meteor descargar nuestro paquete por primera vez. Bien hecho!
Como de costumbre, asegrate de deshacer los cambios antes de continuar (o mantenerlos,
tenindolos en cuenta en el resto del libro).

Comentarios
El objetivo de un sitio de noticias es crear una comunidad de usuarios, y ser difcil hacerlo sin que
puedan a hablar unos con otros. En este captulo, vamos a agregar los comentarios.
Empezaremos creando una nueva coleccin para almacenar los comentarios.

Comments = new Mongo.Collection('comments');

lib/collections/comments.js

// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000)
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'

10

});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000)
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000)
});
}

server/fixtures.js

No olvidemos que debemos publicar y suscribir la nueva coleccin:

Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function() {
return Comments.find();
});

server/publications.js

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});

lib/router.js

Commit 10-1
Aadidos comentarios a la coleccin, pub/sub y datos de p
Ver en GitHub

Lanzar instancia

Ten en cuenta que para que se carguen los nuevos datos de prueba, es necesario ejecutar meteor
reset

. Despus de la restauracin, no olvides crear una nueva cuenta y volver a entrar!

En primer lugar, hemos creado un par de usuarios (inventados), insertndolos en la base de datos y
usando los ids para seleccionarlos despus en la base de datos. Luego aadimos un comentario de
cada usuario al primer post, enlazando el comentario al post (con postId ), y el usuario (con
userId

). Adems, aadimos la fecha y el cuerpo de cada comentario, junto un campo

denormalizado denominado author .


Adems, hemos extendido nuestro router para que espere a un array que contiene las dos
colecciones, comentarios y posts.

Mostrando comentarios
Est bien tener comentarios en la base de datos, pero habr que mostrarlos en la pgina de discusin.
Este proceso ya nos debe ser familiar:

<template name="postPage">
<div class="post-page page">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
</div>
</template>

client/templates/posts/post_page.html

Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});

client/templates/posts/post_page.js

Ponemos el bloque {{#each comments}} dentro de la plantilla del post, por lo que this es un post
para el ayudante comments . Para encontrar los comentarios adecuados, buscamos los que estn
vinculados a ese post a travs de postId .
Con todo lo que hemos aprendido acerca de ayudantes y plantillas, sabemos que mostrar un
comentario es bastante sencillo. Vamos a crear un nuevo directorio comments dentro de templates
para almacenar toda la informacin acerca de los comentarios, y una nueva plantilla commentItem
dentro:

<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>

client/templates/comments/comment_item.html

Vamos a crear rpidamente un ayudante de plantilla para dar a nuestra fecha de envo submitted un
formato ms amigable:

Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});

client/templates/comments/comment_item.js

A continuacin, vamos a mostrar el nmero de comentarios de cada post:

<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>

client/templates/posts/post_item.html

Y aadimos el ayudante commentsCount a post_item.js :

Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
},
commentsCount: function() {
return Comments.find({postId: this._id}).count();
}
});

client/templates/posts/post_item.js

Commit 10-2
Mostrar los comentarios en `postPage`.
Ver en GitHub

Lanzar instancia

Ahora deberamos ser capaces de mostrar nuestros comentarios de prueba y ver algo como esto:

Displaying comments

Enviando comentarios
Vamos a aadir una forma de que los usuarios puedan hacer nuevos comentarios. El proceso ser
bastante similar a como ya hemos hecho para permitir a los usuarios crear nuevos posts.
Empezaremos aadiendo un rea de envo en la parte inferior de cada post:

<template name="postPage">
<div class="post-page page">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
{{#if currentUser}}
{{> commentSubmit}}
{{else}}
<p>Please log in to leave a comment.</p>
{{/if}}
</div>
</template>

client/templates/posts/post_page.html

Y a continuacin, crear la plantilla del formulario para los comentarios:

<template name="commentSubmit">
<form name="comment" class="comment-form form">
<div class="form-group {{errorClass 'body'}}">
<div class="controls">
<label for="body">Comment on this post</label>
<textarea name="body" id="body" class="form-control" rows="3"></textar
ea>
<span class="help-block">{{errorMessage 'body'}}</span>
</div>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
</template>

client/templates/comments/comment_submit.html

Para enviar comentarios, llamaremos a un mtodo comment en el fichero comment_submit.js que


funciona de forma similar a lo que hicimos para al enviar posts:

Template.commentSubmit.onCreated(function() {
Session.set('commentSubmitErrors', {});
});
Template.commentSubmit.helpers({
errorMessage: function(field) {
return Session.get('commentSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('commentSubmitErrors')[field] ? 'has-error' : '';
}
});
Template.commentSubmit.events({
'submit form': function(e, template) {
e.preventDefault();
var $body = $(e.target).find('[name=body]');
var comment = {
body: $body.val(),
postId: template.data._id
};
var errors = {};
if (! comment.body) {
errors.body = "Please write some content";
return Session.set('commentSubmitErrors', errors);
}
Meteor.call('commentInsert', comment, function(error, commentId) {
if (error){
throwError(error.reason);
} else {
$body.val('');
}
});
}
});

client/templates/comments/comment_submit.js

Al igual que anteriormente establecimos un mtodo post en el servidor, vamos a hacer lo mismo
para crear comentarios, comprobar que todo est bien, y finalmente insertar el nuevo comentario

dentro de su coleccin.

Comments = new Mongo.Collection('comments');


Meteor.methods({
commentInsert: function(commentAttributes) {
check(this.userId, String);
check(commentAttributes, {
postId: String,
body: String
});
var user = Meteor.user();
var post = Posts.findOne(commentAttributes.postId);
if (!post)
throw new Meteor.Error('invalid-comment', 'You must comment on a post');
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
return Comments.insert(comment);
}
});

lib/collections/comments.js

Commit 10-3
Creado el formulario de envo de comentarios.
Ver en GitHub

Lanzar instancia

Comprobamos que el usuario est conectado, que el comentario tiene cuerpo, y que est vinculado a
un post.

El formulario de envo de comentarios

Controlando la suscripcin a los comentarios


Tal como estn las cosas, publicamos todos los comentarios de todos los posts a todos los clientes
conectados. No estaremos derrochando recursos? Despus de todo, en un momento dado solo
usamos un pequeo subconjunto de estos datos. As que vamos a mejorar nuestras publicaciones y
suscripciones para controlar exactamente qu comentarios se publican.
Si lo pensamos bien, el nico momento en el que necesitamos suscribirnos a la publicacin
comments

es cuando un usuario accede a la pgina de un post individual, y solo hay que cargar el

subconjunto de comentarios relacionados con ese post en particular.


El primer paso va a ser cambiar la forma de suscribirse a los comentarios. Hasta ahora, nos hemos
estado suscribiendo a nivel router, lo que significa que cargamos todos nuestros datos una vez
cuando este se inicializa.
Pero ahora queremos que nuestra suscripcin dependa de un parmetro de ruta, y, obviamente, este

parmetro puede cambiar en cualquier momento. As que tendremos que cambiar nuestro cdigo de
suscripcin desde el nivel de router al nivel de ruta.
Esto tiene otra consecuencia: en vez de cargar nuestros datos cuando se inicializa la aplicacin, ahora
los cargaremos cada vez que llegamos a una ruta concreta. Esto significa que ahora tendremos
tiempos de carga mientras navegamos por la aplicacin. Esto es un inconveniente inevitable a no ser
que queramos cargar siempre todos los datos.
Primero, dejaremos de pre-cargar todos los comentarios en el bloque configure , eliminando la
lnea Meteor.subscribe('comments') (dicho de otro manera, volvemos a lo que tenamos
anteriormente):

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});

lib/router.js

Y aadiremos una nueva funcin waitOn a nivel de ruta en la ruta postPage :

//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});

//...

lib/router.js

Estamos pasando this.params._id como argumento a la suscripcin. As que, utilicemos esa nueva
informacin para asegurarnos que limitamos el conjunto de datos a los comentarios que pertenecen
al post actual:

Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});

server/publications.js

Commit 10-4
Creado un mecanismo simple de publicacin/suscripcin par
Ver en GitHub

Lanzar instancia

Solo hay un problema: cuando volvemos a la pgina principal, todos nuestros mensajes tienen 0
comentarios:

Los comentarios han desaparecido!

Contando comentarios
La razn de que esto ocurra est bien clara: solo cargaremos comentarios en la ruta postPage , as
que cuando llamamos a Comments.find({postId: this._id}) en nuestro ayudante
commentsCount

del gestor client/views/posts/post_item.js , Meteor no encuentra los datos

necesarios en el lado del cliente para devolver un resultado.


La mejor manera de resolver esto es denormalizar el nmero de comentarios dentro del post (si no
sabes lo que significa denormalizar, no te preocupes, lo veremos en el prximo captulo). Aunque,
como veremos, hay que aadir un poco de complejidad a nuestro cdigo, a cambio, mejoramos la
velocidad al no tener que publicar todos los comentarios de la base de datos solo para contarlos.
Lo conseguiremos aadiendo una propiedad commentsCount a la estructura de datos del post (y
restableceremos Meteor con meteor reset - no olvides volver a crear una cuenta de usuario):

// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),
commentsCount: 0
});

Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000),
commentsCount: 0
});
}

server/fixtures.js

Como de costumbre cuando actualizamos el fichero de fixtures, debers ejecutar meteor reset para
inicializar la base de datos y asegurarnos que se ejecutan de nuevo los fixtures.
Luego, nos aseguramos de que todos los nuevos posts empiezan con 0 comentarios:

//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);

//...

lib/collections/posts.js

Y entonces actualizamos commentsCount cuando hacemos un nuevo comentario usando el operador


$inc

de Mongo (que incrementa campos numricos):

//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});

// update the post with the number of comments


Posts.update(comment.postId, {$inc: {commentsCount: 1}});
return Comments.insert(comment);

//...

lib/collections/comments.js

Finalmente, tenemos que eliminar el ayudante commentsCount de


client/templates/posts/post_item.js

, ya que el campo est disponible directamente en el post.

Commit 10-5
Denormalizando el nmero de comentarios.
Ver en GitHub

Lanzar instancia

Ahora que los usuarios pueden hablar entre s, sera una lstima que se perdieran los nuevos
comentarios de otros usuarios. En el siguiente captulo veremos cmo implementar notificaciones!

Denormalizacin

SIDEBAR

10.5

Denormalizar los datos significa no almacenar esos datos de una manera normal. En otras palabras,
significa tener mltiples copias de la misma porcin de datos.
En el captulo anterior, denormalizamos la cantidad de comentarios dentro del objeto post para evitar
tener que cargar todos los comentarios todo el tiempo. Teniendo en cuenta el modelado de datos,
esto es redundante, ya que en su lugar podramos simplemente contar el nmero correcto de
comentarios en cualquier momento para averiguar su valor (dejando de lado las consideraciones de
rendimiento).
Denormalizar a menudo significa un trabajo extra para el desarrollador. En nuestro ejemplo, cada vez
que agregamos o eliminamos un comentario adems tenemos que acordarnos de actualizar el post
en cuestin para asegurarnos de que el campo commentsCount siga siendo correcto. Esto es
exactamente la razn por la cual las bases de datos relacionales como MySQL desaprueban esta
tcnica.
De todas maneras, la tcnica normal tambin tiene sus desventajas: sin una propiedad
commentsCount

, necesitaramos enviar todos los comentarios todo el tiempo tan slo para poder

contarlos, que es lo que estbamos haciendo en un principio. Denormalizar permite evitar esto
ltimo.

Una publicacin especial


Sera posible crear una publicacin especial que solo enve la cantidad de comentarios que
nos interesan (por ejemplo, la cantidad de comentarios en los posts que actualmente
estamos viendo, haciendo ms consultas al servidor).
Pero vale la pena considerar si la complejidad de dicha publicacin superara o no las
dificultades creadas al denormalizar

Por supuesto, dichas consideraciones son especficas para cada aplicacin: si ests escribiendo
cdigo donde la integridad de datos es fundamental, entonces evitar inconsistencias en los datos es
de lejos ms importante y de mayor prioridad que cualquier mejora de rendimiento.

Incrustar Documentos o Usar Mltiples Coleciones


Si tienes experiencia con Mongo, podrs haberte sorprendido al ver que hemos creado una segunda
coleccin solo para los comentarios: por qu no simplemente integrar los comentarios en una lista
dentro del documento post?
Resulta que muchas de las herramientas que Meteor nos da, trabajan mucho mejor operando a un
nivel de coleccin. Por ejemplo:
1. El ayudante {{#each}} es muy eficiente cuando itera sobre un cursor (el resultado de
collection.find()

). Pero no lo es cuando itera sobre un array de objetos dentro un

documento ms grande.
2. allow y deny operan a nivel de documento, por consiguiente, facilita la tarea de asegurarse
que cualquier modificacin de un comentario individual es correcta. Esto sera mucho ms
complejo si operara a nivel de post.
3. DDP opera a nivel de los atributos top-level de un documento. Esto significa que si comments
fuese una propiedad de un post , cada vez que un comentario fuese creado en un post, el
servidor debera enviar toda la lista de comentarios actualizada para ese post a cada uno de los
clientes conectados.
4. Publicaciones y suscripciones son mucho ms fciles de controlar a nivel de documentos. Por
ejemplo, si quisiramos paginar comentarios en un post sera muy difcil a menos que los
comentarios estuviesen en su propia coleccin.
Mongo sugiere incrustar documentos para reducir la cantidad de consultas necesarias para buscar los
documentos. De todos modos, esto es un problema mnimo cuando se tiene en cuenta la arquitectura
de Meteor: la mayor parte del tiempo estamos consultando comentarios en el cliente, donde el acceso
a la base de datos prcticamente no tiene coste.

Las Desventajas de la Denormalizacin


Hay buenos argumentos sobre por qu no deberas denormalizar tus datos. Para ver un buen
caso contra la denormalizacin, recomendamos Por qu nunca deberas usar MongoDB
por Sarah Mei.

Notificaciones

11

Ahora que los usuarios pueden comentar los posts de otros usuarios, sera bueno hacerles saber que
alguien ha comenzado una conversacin.
Para ello, notificaremos al autor, de que ha habido un comentario en su post, y le proporcionaremos
un enlace para poder comentar.
En este tipo de funcionalidad es en la que Meteor brilla. Como por defecto, Meteor trabaja en tiempo
real, vamos a poder mostrar notificaciones instantneamente. No necesitamos esperar a que el
usuario actualice la pgina, podemos mostrar nuevas notificaciones sin tener que escribir ningn
cdigo especial.

Creando notificaciones
Crearemos una notificacin cuando alguien comente uno de nuestros posts. En el futuro, las
notificaciones podran extenderse para cubrir muchos otros escenarios, pero, por ahora ser
suficiente con esto para mantener a los usuarios informados sobre lo que est pasando.
Vamos a crear la coleccin Notifications y la funcin createCommentNotification que insertar
una notificacin para cada comentario que se haga en uno de nuestros posts.
Puesto que estamos actualizando las notificaciones desde el lado del cliente, necesitamos
asegurarnos que nuestra llamada allow es a prueba de balas. Por lo que deberemos comprobar que:
El usuario que hace la llamada update es el dueo de la notificacin modificada.
El usuario solo est intentando modificar un solo campo.
El campo a modificar es la propiedad read de nuestra notificacin.

Notifications = new Mongo.Collection('notifications');


Notifications.allow({
update: function(userId, doc, fieldNames) {
return ownsDocument(userId, doc) &&
fieldNames.length === 1 && fieldNames[0] === 'read';
}
});
createCommentNotification = function(comment) {
var post = Posts.findOne(comment.postId);
if (comment.userId !== post.userId) {
Notifications.insert({
userId: post.userId,
postId: post._id,
commentId: comment._id,
commenterName: comment.author,
read: false
});
}
};

lib/collections/notifications.js

Al igual que con los posts o los comentarios, esta coleccin estar compartida por clientes y servidor.
Como tendremos que actualizar las notificaciones cuando un usuario las haya visto, permitimos hacer
update

siempre que se trate de los datos del propio usuario.

Tambin creamos una funcin que mira qu post est comentando el usuario, averigua qu usuario
debe ser notificado e inserta una nueva notificacin.
Ya tenemos un mtodo en el servidor para crear comentarios, por lo que podemos ampliarlo para que
llame a nuestra nueva funcin. Para guardar el _id del nuevo comentario en una variable,
cambiamos return Comments.insert(comment); , por comment._id =
Comments.insert(comment)

y llamamos a la funcin createCommentNotification :

Comments = new Mongo.Collection('comments');


Meteor.methods({
commentInsert: function(commentAttributes) {

//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});

// update the post with the number of comments


Posts.update(comment.postId, {$inc: {commentsCount: 1}});
// create the comment, save the id
comment._id = Comments.insert(comment);
// now create a notification, informing the user that there's been a comment
createCommentNotification(comment);
return comment._id;
}
});

lib/collections/comments.js

Tenemos que publicar las notificaciones:

Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find();
});

server/publications.js

Y suscribirnos en el cliente:

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
}
});

lib/router.js

Commit 11-1
Aadida la coleccin de comentarios.
Ver en GitHub

Lanzar instancia

Mostrando las notificaciones


Ahora podemos seguir y aadir una lista de notificaciones a nuestra cabecera:

<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>

client/templates/includes/header.html

Y crear las plantillas notifications y notificationItem (que pondremos en el archivo


notifications.html

):

<template name="notifications">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Notifications
{{#if notificationCount}}
<span class="badge badge-inverse">{{notificationCount}}</span>
{{/if}}
<b class="caret"></b>
</a>
<ul class="notification dropdown-menu">
{{#if notificationCount}}
{{#each notifications}}
{{> notificationItem}}
{{/each}}
{{else}}
<li><span>No Notifications</span></li>
{{/if}}
</ul>
</template>
<template name="notificationItem">
<li>
<a href="{{notificationPostPath}}">
<strong>{{commenterName}}</strong> commented on your post
</a>
</li>
</template>

client/templates/notifications/notifications.html

Podemos ver que para cada notificacin, tendremos un enlace al post que ha sido comentado junto
con el usuario que lo ha hecho.
A continuacin, hay que asegurarse de que se selecciona la lista de notificaciones correcta desde
nuestro ayudante, y actualizar las notificaciones como ledas cuando el usuario hace clic en el
enlace al que apuntan.

Template.notifications.helpers({
notifications: function() {
return Notifications.find({userId: Meteor.userId(), read: false});
},
notificationCount: function(){
return Notifications.find({userId: Meteor.userId(), read: false}).count();
}
});
Template.notificationItem.helpers({
notificationPostPath: function() {
return Router.routes.postPage.path({_id: this.postId});
}
});
Template.notificationItem.events({
'click a': function() {
Notifications.update(this._id, {$set: {read: true}});
}
});

client/templates/notifications/notifications.js

Commit 11-2
Mostrar las notificaciones en la cabecera.
Ver en GitHub

Lanzar instancia

Como podemos ver, las notificaciones no son muy diferentes de los errores, y su estructura es muy
similar. Solo hay una diferencia clave: hemos creado una coleccin sincronizada cliente-servidor. Esto
significa que nuestras notificaciones son persistentes y, siempre y cuando se utilice la misma cuenta
de usuario, persistir en distintos navegadores y dispositivos.
Abre un segundo navegador, crea una nueva cuenta de usuario, y aade un comentario en un post del
usuario anterior. Deberas ver algo as:

Mostrando las notificaciones.

Controlando el acceso a las notificaciones


Las notificaciones van bien. Sin embargo, hay un pequeo problema: nuestras notificaciones son
pblicas.
Si ejecutamos el siguiente comando en la consola del segundo navegador:

Notifications.find().count();
1

Consola del navegador

El nuevo usuario (el que ha comentado) no debera tener notificaciones. Las que vemos son las de los
dems usuarios.

Aparte de los posibles problemas de privacidad, simplemente no podemos permitirnos el lujo de


cargar las notificaciones de todos los usuarios. Con un sitio lo suficientemente grande, esto podra
sobrecargar la memoria disponible en el navegador y empezar a causar graves problemas de
rendimiento.
Resolveremos este problema mediante las publicaciones. Podemos usar las publicaciones para
especificar qu parte de nuestra coleccin queremos compartir con el navegador.
Para lograrlo, tenemos que cambiar Notifications.find() . Es decir, tenemos que devolver el
cursor que correspondiente a las notificaciones del usuario actual.
Hacer esto es bastante sencillo puesto que la funcin publish tiene el _id del usuario actual
disponible en this.userId :

Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId, read: false});
});

server/publications.js

Commit 11-3
Sincronizar solo las notificaciones relevantes al usuario.
Ver en GitHub

Lanzar instancia

Si ahora se busca en las consolas de los dos navegadores, deberamos ver dos colecciones distintas
de notificaciones:

Notifications.find().count();
1

Consola del navegador (usuario 1)

Notifications.find().count();
0

Consola del navegador (usuario 2)

De hecho, la lista de notificaciones cambiar si accedes y sales de la aplicacin. Esto se debe a que las
publicaciones se republican automticamente cada vez que cambia el estado del usuario.
Nuestra aplicacin es cada vez ms funcional, y a medida que cada vez ms usuarios entran y
empiezan a publicar enlaces, corremos el riesgo de acabar con una pgina de inicio demasiado larga.
Vamos a abordar este problema en el prximo captulo: la paginacin.

Reactividad avanzada

11.5

SIDEBAR

No es comn tener que escribir cdigo de seguimiento de dependencias por ti mismo, pero para
comprender el concepto, es verdaderamente til seguir el camino de cmo funciona el flujo de
dependencias.
Imagina que quisiramos saber a cuntos amigos del usuario actual de Facebook le ha gustado cada
post en Microscope. Supongamos que ya hemos trabajado en los detalles de cmo autenticar el
usuario con Facebook, hacer las llamadas necesarias a la API, y procesar los datos relevantes. Ahora
tenemos una funcin asncrona en el lado del cliente que devuelve el nmero de me gusta:
getFacebookLikeCount(user, url, callback)

Lo importante a recordar sobre una funcin de esta naturaleza es que no es reactiva ni funciona en
tiempo real. Har una peticin HTTP a Facebook, enviando algunos datos, y obtendremos el resultado
en la aplicacin a travs de una llamada asncrona. Pero la funcin no se va a volver a ejecutar por s
sola cuando haya un cambio en Facebook, ni nuestra UI va a cambiar cuando los datos lo hagan.
Para solucionar esto, podemos comenzar utilizando setInterval para llamar a la funcin cada
ciertos segundos:

currentLikeCount = 0;
Meteor.setInterval(function() {
var postId;
if (Meteor.user() && postId = Session.get('currentPostId')) {
getFacebookLikeCount(Meteor.user(), Posts.find(postId).url,
function(err, count) {
if (!err)
currentLikeCount = count;
});
}
}, 5 * 1000);

Cada vez que usemos esa variable currentLikeCount , obtendremos el nmero correcto con un

margen de error de cinco segundos. Ahora podemos usar esa variable en un ayudante:

Template.postItem.likeCount = function() {
return currentLikeCount;
}

Sin embargo, nada le dice todava a nuestra plantilla que se redibuje cuando cambie
currentLikeCount

. Si bien la variable ahora est en pseudo tiempo real (se cambia a s misma), no

es reactiva y por lo tanto todava no puede comunicarse correctamente con el resto del ecosistema de
Meteor.

Rastreando la Reactividad: Computaciones


La reactividad de Meteor es mediada por dependencias, estructuras de datos que rastrean una serie
de computaciones.
Como vimos anteriormente, una computacin es una parte de cdigo que usa datos reactivos. En
nuestro caso, hay una computacin que ha sido implcitamente creada por la plantilla postItem , y
cada ayudante en el manejador de esa plantilla est funcionando dentro de esa computacin.
Puedes pensar de la computacin como una parte del cdigo que se preocupa por los datos
reactivos. Cuando los datos cambien, est computacin ser informada (a travs de invalidate() ),
y es la computacin la que debe decidir si algo debe hacerse.

Trasformando una Variable en una Funcin Reactiva


Para trasformar nuestra variable currentLikeCount en una fuente de datos reactiva, necesitamos
rastrear todas las computaciones que la usan como una dependencia. Esto requiere trasformarla de
una variable a una funcin (que devolver un valor):

var _currentLikeCount = 0;
var _currentLikeCountListeners = new Tracker.Dependency();
currentLikeCount = function() {
_currentLikeCountListeners.depend();
return _currentLikeCount;
}
Meteor.setInterval(function() {
var postId;
if (Meteor.user() && postId = Session.get('currentPostId')) {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err && count !== _currentLikeCount) {
_currentLikeCount = count;
_currentLikeCountListeners.changed();
}
});
}
}, 5 * 1000);

Lo que hemos hecho es configurar una dependencia _currentLikeCountListeners , que rastrear


todas las computaciones en las cuales se utilice currentLikeCount() . Cuando el valor de
_currentLikeCount

cambie, podemos llamar a la funcin changed() en esa dependencia, que

invalida todas las computaciones realizadas.


Estas computaciones pueden entonces seguir adelante y evaluar los cambios caso por caso.
Parece un montn de cdigo para una nica fuente de datos reactiva, y tienes razn, por lo que
Meteor proporciona algunas herramientas de serie para hacerlo un poco ms sencillo. (normalmente,
no se usan computaciones directamente, si no sencillamente auto ejecuciones). Hay un paquete
llamado reactive-var que hace exactamente lo que hacemos con la funcin currentLikeCount() .
Por lo que si lo aadimos:

meteor add reactive-var

Podremos simplificar nuestro cdigo un poco:

var currentLikeCount = new ReactiveVar();


Meteor.setInterval(function() {
var postId;
if (Meteor.user() && postId = Session.get('currentPostId')) {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err) {
currentLikeCount.set(count);
}
});
}
}, 5 * 1000);

Ahora para usarlo, llamaremos a currentLikeCount.get() en nuestro ayudante y debera funcionar


como antes. Hay tambin otro paquete reactive-dict , que proporciona un almacn de datos clavevalor reactivo (exactamente igual que la Session ), que podra ser til tambin.

Comparando Tracker con Angular


Angular es una librera de renderizado reactivo del lado del cliente, desarrollada por la buena gente
de Google. Solo se puede comparar el seguimiento de dependencias de Meteor con el de Angular de
un modo ilustrativo, ya que sus enfoques son bastante diferentes.
Hemos visto que el modelo de Meteor usa bloques de cdigo llamados computaciones. Estas son
seguidas por fuentes de datos reactivas (funciones) que se ocupan de invalidarlos cuando sea
apropiado. As, la fuente de datos informa explcitamente todas sus dependencias cuando necesita
llamar a invalidate() . Ntese que a pesar de que esto sucede generalmente cuando los datos
cambian, la fuente de los datos puede adems decidir ejecutar la invalidacin por otras razones.
Adems, por ms que usualmente las computaciones simplemente se reejecutan cuando son
invalidadas, se pueden configurar para que se comporten como uno quiera. Esto nos da un alto nivel
de control sobre la reactividad.

En Angular, la reactividad es medida por el objeto scope . Un scope, o alcance, puede ser pensado
como un simple objeto de JavaScript con algunos mtodos especiales.
Cuando se desea depender reactivamente de un valor dentro del scope, se llama a scope.$watch ,
declarando la expresin en la que uno est interesado (por ejemplo, qu partes del scope te
importan) y una funcin que se ejecutar cada vez que esa expresin cambie. As, se puede declarar
explcitamente qu hacer cada vez que ese valor sea modificado.
Volviendo a nuestro ejemplo con Facebook, escribiramos:

$rootScope.$watch('currentLikeCount', function(likeCount) {
console.log('Current like count is ' + likeCount);
});

Por supuesto, es tan raro tener que configurar computaciones en Meteor, como tener que invocar a
$watch

explcitamente en Angular, ya que las directivas ng-model y las {{expressions}}

automticamente se ocupan de re-renderizarse cuando haya un cambio.


Cuando dicho valor reactivo sea cambiado, scope.$apply() debe ser llamado. Esto vuelve a evaluar
cada watcher del scope, pero solo llama a las funciones de aquellos watchers que contengan
valores modificados.
Entonces, scope.$apply() es similar a dependency.changed() , excepto que acta al nivel del
scope, en lugar de darle el control al desarrollador para decirle precisamente cules funciones
deberan ser re-evaluadas. Con eso aclarado, esta pequea falta de control le da a Angular la
habilidad de ser muy inteligente y eficiente, ya que determina precisamente qu debe ser vuelto a
evaluar.
Con Angular, nuestra funcin getFacebookLikeCount() habra sigo algo as:

Meteor.setInterval(function() {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err) {
$rootScope.currentLikeCount = count;
$rootScope.$apply();
}
});
}, 5 * 1000);

Decididamente, Meteor se ocupa de la parte ms pesada por nosotros y nos deja beneficiarnos de la
reactividad sin demasiado trabajo. Pero tal vez, aprender estos patrones ser de ayuda si alguna vez
necesitas ir ms all.

Paginacin

12

Nuestra aplicacin va tomando forma y podemos esperar un gran xito cuando todo el mundo la
conozca.
As que quizs debamos pensar un poco sobre cmo afectar al rendimiento el gran nmero de
nuevos posts que vamos a recibir.
Hemos visto antes cmo una coleccin en el cliente pude contener un subconjunto de los datos en el
servidor y lo hemos usado para nuestras notificaciones y comentarios.
Ahora pensemos en que todava estamos publicando todos nuestros posts de una sola vez a todos los
usuarios conectados. Si se publicaran miles de enlaces, esto sera un problema. Para solucionarlo
tenemos que paginar nuestros posts.

Aadiendo unos cuantos posts


En primer lugar, vamos a cargar los suficientes posts para que la paginacin tenga sentido:

// Fixture data
if (Posts.find().count() === 0) {
//...
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000),
commentsCount: 0
});
for (var i = 0; i < 10; i++) {
Posts.insert({
title: 'Test post #' + i,
author: sacha.profile.name,
userId: sacha._id,
url: 'http://google.com/?q=test-' + i,
submitted: new Date(now - i * 3600 * 1000),
commentsCount: 0
});
}
}

server/fixtures.js

Despus de ejecutar meteor reset e iniciar la aplicacin de nuevo, deberamos ver algo como esto:

Mostrando un montn de datos.

Commit 12-1
Aadidos suficientes posts para hacer necesaria la pagina
Ver en GitHub

Lanzar instancia

Paginacin infinita
Vamos a implementar una paginacin de estilo infinito. Lo que queremos decir con esto es que
primero mostramos, por ejemplo, 10 posts, con un enlace de cargar ms en la parte inferior. Al hacer
clic en este enlace se cargarn 10 ms, y as hasta el infinito y ms all. Esto significa que podemos
controlar todo nuestro sistema de paginacin con un solo parmetro que representa el nmero de
posts que mostraremos en pantalla.
Vamos a necesitar entonces una forma de pasar este parmetro al servidor para que sepa la cantidad

de mensajes que debe enviar al cliente. Se da la circunstancia de que ya estamos suscritos a la


publicacin posts en el router, as que vamos a aprovecharlo y a dejar que el router maneje tambin
la paginacin.
La forma ms fcil de configurar esto es hacer que el parmetro lmite forme parte de la ruta,
quedando las URL de la forma http://localhost:3000/25 . Una ventaja aadida de utilizar la URL
en vez de otros mtodos es que si estamos viendo 25 posts y resulta que se recarga la pgina por
error, todava seguiremos viendo 25 posts.
Para hacer esto correctamente, tenemos que cambiar la forma en que nos suscribimos a los posts. Al
igual que hicimos en el captulo Comentarios, tendremos que mover nuestro cdigo de suscripcin
desde el nivel de router al nivel de ruta.
Parece demasiado para hacerlo todo de una sola vez, pero se ver ms claro escribiendo el cdigo.
En primer lugar, vamos a dejar de suscribirnos a la publicacin posts en el bloque
Router.configure()

. Simplemente elimina Meteor.subscribe('posts') , dejando solo la

suscripcin notifications :

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});

lib/router.js

A continuacin, aadiremos el parmetro postsLimit al path de la ruta. Si aadimos un ? despus


del parmetro, lo hacemos opcional. De forma que nuestra ruta no solo coincidir con
http://localhost:3000/50

, sino tambin con http://localhost:3000 .

//...
Router.route('/:postsLimit?', {
name: 'postsList',
});

//...

lib/router.js

Es importante sealar que un path de la forma /:parameter? coincide con todos los path posibles.
Dado que cada ruta se analiza en orden secuencial para comprobar si coincide con la ruta actual,
tenemos que asegurarnos que organizamos bien nuestras rutas con el fin de disminuir la
especificidad.
En otras palabras, las rutas ms especficas como /posts/:_id deben ir primero, y nuestra ruta
postsList

debera ir en la parte inferior del grupo de rutas para que todo coincida correctamente.

Es el momento de abordar el difcil problema de suscribirse y encontrar los datos correctos. Tenemos
que lidiar con el caso en el que el parmetro postsLimit no est presente, por lo que vamos a
asignarle un valor predeterminado. Usaremos 5, que nos dar suficiente espacio para jugar con la
paginacin.

//...
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
}
});

//...

lib/router.js

Ya te habrs dado cuenta de que estamos pasando un objeto JavaScript ({sort: {submitted: -1}, limit:
postsLimit}) junto con el nombre de nuestra publicacin posts . Este objeto servir como parmetro
options

en la llamada a Posts.find() en el lado del servidor. Vamos a cambiar nuestro cdigo en

el servidor para implementarlo:

Meteor.publish('posts', function(options) {
check(options, {
sort: Object,
limit: Number
});
return Posts.find({}, options);
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId});
});

server/publications.js

Paso de parmetros
Nuestro cdigo de publicaciones est diciendo al servidor que puede confiar en cualquier
objeto JavaScript enviado por el cliente (en nuestro caso, {limit: postsLimit}) para
servir como opciones para find() . Esto hace posible que los usuarios enven cualquier
opcin a travs de la consola del navegador.
En nuestro caso, esto es relativamente inofensivo, ya que todo lo que un usuario podra
hacer es reordenar los mensajes de manera diferente, o cambiar el lmite (que es lo que
queremos hacer). De todas formas una aplicacin del mundo real debera probablemente
limitar el lmite!
Afortunadamente, usando check() sabemos que los usuarios no podrn inyectar opciones
adicionales (como la opcin fields , que en algunos casos podra exponer datos privados
en los documentos).
De todas formas, un patrn ms seguro para asegurarnos el control de nuestros datos podra
ser pasar los parmetros de forma individual en lugar de todo el objeto:

Meteor.publish('posts', function(sort, limit) {


return Posts.find({}, {sort: sort, limit: limit});
});

Ahora que nos suscribimos a nivel de ruta, tiene sentido establecer el contexto de datos en ese mismo
lugar. Vamos a desviarnos un poco de nuestro patrn anterior y hacer que la funcin data devuelva
un objeto JavaScript en lugar de simplemente devolver un cursor. Esto nos permite crear un contexto
de datos con nombre que llamaremos posts .
Lo que significa es que en lugar de disponer implcitamente de los datos en this dentro de la
plantilla, estar disponible tambin como posts . Aparte de este pequeo elemento, el cdigo debe
sernos familiar:

//...
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
},
data: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return {
posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
};
}
});

//...

lib/router.js

Ahora que hemos establecido el contexto de datos a nivel de router podemos deshacernos del
ayudante de plantilla posts del archivo posts_list.js borrando el contenido de este archivo.
Y como hemos llamado posts al contexto de datos (igual que en el ayudante), ni siquiera
necesitamos tocar la plantilla postsList !
Recapitulemos. As es como ha quedado nuestro router.js :

Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {

return Meteor.subscribe('comments', this.params._id);


},
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
},
data: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return {
posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
};
}
});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});

lib/router.js

Commit 12-2
Aumentada la ruta `postsList` para que tenga un lmite.
Ver en GitHub

Lanzar instancia

Vamos a probar nuestro nuevo sistema de paginacin. Ahora podemos mostrar un nmero arbitrario
de posts en la pgina principal simplemente cambiando el parmetro en la URL. Por ejemplo, intenta
acceder a http://localhost:3000/3 . Deberas ver algo como esto:

Controlando el nmero de posts en la pgina principal.

Y porqu no usamos pginas?


Por qu usamos el enfoque paginacin infinita en lugar de mostrar pginas sucesivas con
10 posts cada uno, como hace Google para en sus resultados de bsqueda? En realidad se
debe al paradigma de tiempo real que utiliza Meteor.
Imaginemos que paginamos nuestra coleccin Posts utilizando el patrn de resultados
Google, y que estamos en la pgina 2, que muestra los mensajes de 10 a 20. Qu pasa si otro
usuario elimina una de las 10 entradas anteriores?
Como nuestra aplicacin es en tiempo real, nuestra base de datos cambiara. El post 10 se
convertira en el 9, y desaparecera de nuestra vista y ahora veramos el 11. El resultado sera
que el usuario vera aparecer y desaparecer posts sin razn aparente.
Incluso si toleramos esta peculiaridad, la paginacin tradicional tambin es difcil de
implementar por razones tcnicas.
Volvamos a nuestro ejemplo anterior. Estamos publicando los posts 10-20 de la coleccin
Posts

, pero cmo los encontramos en el cliente? No se pueden seleccionar los mensajes

10 a 20 porque solo hay diez posts en total en el conjunto de datos del lado del cliente.
Una solucin sera publicar esos 10 mensajes en el servidor y, a continuacin, hacer un
Posts.find()

en el lado del cliente para recoger todos los posts publicados.

Esto funciona si solo tienes una suscripcin. Pero, y si tenemos ms de una?


Digamos que una suscripcin pide los posts 10 a 20, y otra 30 a 40. Ahora tenemos 20
mensajes cargados del lado del cliente en total, y no hay forma de saber cules pertenecen a
cada suscripcin.
Por todas estas razones, la paginacin tradicional simplemente no tiene mucho sentido
cuando se trabaja con Meteor.

Creando un controlador de rutas

Te habrs dado cuenta de que repetimos dos veces la lnea var limit =
parseInt(this.params.postsLimit) || 5;

. Adems, codificar el nmero 5 no es lo ideal. No te

preocupes, no es el fin del mundo, pero como siempre es mejor seguir el principio DRY (Dont Repeat
Yourself), vamos a ver cmo podemos refactorizar un poco las cosas.
Introduciremos un nuevo aspecto de Iron Router, los controladores de ruta. Un controlador de ruta es
simplemente una forma de agrupar en un paquete reutilizable, caractersticas de enrutamiento que
puede heredar cualquier ruta. Ahora solo lo utilizaremos para una sola ruta, pero en el prximo
captulo veremos que esta caracterstica es muy til.

//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
data: function() {
return {posts: Posts.find({}, this.findOptions())};
}
});

//...
Router.route('/:postsLimit?', {
name: 'postsList'
});

//...

lib/router.js

Vamos a verlo paso a paso. En primer lugar, creamos nuestro controlador extendiendo
RouteController

. A continuacin, establecemos la propiedad template tal y como hicimos antes,

y luego una nueva propiedad increment .


A continuacin, definimos una nueva funcin postsLimit que devolver el lmite actual, y una
funcin findOptions que devolver un objeto con las opciones. Esto puede parecer un paso extra,
pero vamos a hacer uso de l en el futuro.
A continuacin, definimos nuestras funciones waitOn y de data igual que antes, excepto que ahora
usan la nueva funcin findOptions .
Como nuestro controlador se llama PostsListController y nuestra ruta se llama postsList , Iron
Router usar el controlador automticamente. As que slo necesitamos eliminar waitOn y data de
la definicon de la ruta (ya que es el controlador quien los gestiona ahora). Si necesitramos utilizar un
controlador con un nombre diferente, lo podemos hacer usando la opcin controller (veremos un
ejemplo de esto en el siguiente captulo).

Commit 12-3
postLists refactorizado en un controlador de rutas
Ver en GitHub

Lanzar instancia

Aadiendo un botn Load more


Ya tenemos funcionando la paginacin. Solo hay un problema: no hay manera de utilizarla realmente
si no escribimos en la URL manualmente. Desde luego, no parece una gran experiencia de usuario, as
que vamos a arreglarlo.
Lo que queremos hacer es bastante simple. Vamos a aadir un botn Load more en la parte inferior
de nuestra lista de posts, que incrementar en 5 el nmero de posts que se muestran. As que si

estamos en la URL http://localhost:3000/5 , haciendo clic en Load more debera llevarnos a


http://localhost:3000/10

. Si has llegado hasta aqu, confiamos en que podrs manejar un poco

de aritmtica!
Al igual que antes, vamos a aadir nuestra lgica de paginacin en la ruta. Recuerdas que
nombramos explcitamente el contexto de datos en lugar de usar un cursor annimo? Bueno, pues no
hay ninguna regla que diga que la funcin data solo puede pasar cursores, de modo que usaremos
la misma tcnica para generar la URL del botn Load more.

//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment
});
return {
posts: this.posts(),
nextPath: hasMore ? nextPath : null
};
}
});

//...

lib/router.js

Echemos un vistazo en profundidad a este pequeo truco de magia que hemos puesto en el router.
Recuerda que la ruta postsList (que se hereda del controlador PostsListController con el que
estamos trabajando) toma un parmetro postsLimit .
Cuando alimentamos {postsLimit: this.postsLimit() + this.increment} a
this.route.path()

, le estamos diciendo a la ruta postsList que construya su propio path

utilizando ese objeto JavaScript como contexto de datos.


En otras palabras, es exactamente lo mismo que usar el ayudante Spacebars {{pathFor
'postsList'}}

, salvo que reemplazamos el this implcito por nuestro propio contexto de datos a

medida.
Estamos cogiendo ese path y aadindolo al contexto de datos de nuestra plantilla, pero slo si hay
ms posts que mostrar. La forma de hacerlo es un poco complicada.
Sabemos que this.limit() devuelve el nmero actual de posts que nos gustara mostrar, que
puede ser el valor de la URL actual, o el valor por defecto (5) si la URL no contiene ningn parmetro.
Por otro lado, this.posts se refiere al cursor actual, de modo que this.posts.count() es el
nmero de mensajes que hay en el cursor.
As que lo que estamos diciendo es que si pedimos n posts y obtenemos n , seguiremos mostrando
el botn Load more. Pero si pedimos n y tenemos menos de n , significa que hemos llegado al
lmite y deberamos dejar de mostrarlo.
Con todo esto, nuestro sistema falla en un caso: cuando el nmero de posts en nuestra base de datos
es exactamente n . Si eso ocurre, el cliente pedir n posts y obtendr n por lo que seguir
mostrando el botn Load more, sin darse cuenta de que ya no quedan ms elementos.
Lamentablemente, no hay soluciones sencillas para este problema, as que por ahora vamos a tener
que conformarnos con esto.

Todo lo que queda por hacer es aadir el botn Load more en la parte inferior de nuestra lista de
posts, asegurndonos de mostrarlo solo si tenemos ms posts que cargar:

<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>

client/templates/posts/posts_list.html

As es como se debera ver la lista ahora:

El botn Load more.

Commit 12-4
Aadido nextPath() al controlador para desplazarnos por l
Ver en GitHub

Lanzar instancia

Mejorando la experiencia de usuario


La paginacin funciona correctamente, pero tiene una peculiaridad algo molesta: cada vez que se
hace clic en Load more y el router pide ms posts, nos enva a la plantilla de la carga mientras
esperamos los nuevos datos. El resultado es que cada vez, nos enva a la parte superior de la pgina y
tenemos que desplazarnos hasta el final para reanudar la navegacin.
As que primero, tenemos que decirle a Iron Router que no espere ( waitOn ) la suscripcin. En su
lugar, definiremos nuestras suscripciones en el hook subscriptions .
Tambin estamos pasando una variable ready que hace referencia a this.postsSub.ready como
parte de nuestro contexto de datos, que informar a la plantilla cuando se haya terminado de cargar
la suscripcin a los posts.

//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment
});
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});

//...

lib/router.js

Comprobaremos esta variable ready en la plantilla para mostrar un spinner al final de la lista de
posts mientras estemos cargando el nuevo conjunto de posts:

<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>

client/templates/posts/posts_list.html

Commit 12-5
Aadir un spinner para hacer la pagicin mas atractiva.
Ver en GitHub

Lanzar instancia

Accediendo a cualquier post


Por defecto, cargamos los cinco ltimos posts, pero qu pasa si vamos a la pgina de un post
individual?

Una plantilla vaca.

Si lo pruebas, se mostrar un error no encontrado. En realidad, tiene sentido: le hemos dicho al


router que se suscriba a la publicacin posts cuando carga la ruta postsList , pero no le hemos
dicho qu debe hacer con la ruta postpage .
Pero hasta el momento, lo nico que sabemos es suscribirnos a una lista de los n ltimos posts.
Cmo pedimos al servidor un solo post? Te contar un pequeo secreto: podemos usar ms de una
publicacin para cada coleccin!
As que para volver a ver los posts perdidos, crearemos una nueva y separada publicacin
singlePost

que solo publica un post, identificado por _id .

Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('singlePost', function(id) {
check(id, String)
return Posts.find(id);
});

//...

server/publications.js

Ahora, vamos a suscribirnos a los posts correctos en el lado del cliente. Ya estamos suscritos a la
publicacin comments en la funcin waitOn de la ruta postPage , por lo que simplemente podemos
aadir ah la suscripcin a singlePost . Sin olvidarnos de aadir la suscripcin a la ruta postEdit ,
que tambin necesita los mismos datos:

//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('comments', this.params._id)
];
},
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
waitOn: function() {
return Meteor.subscribe('singlePost', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});

//...

lib/router.js

Commit 12-6
Usando una sola suscripcin a los posts para asegurarnos
Ver en GitHub

Lanzar instancia

Terminada la paginacin, nuestra aplicacin ya no sufre de problemas de escalado, y los usuarios


pueden contribuir con muchos ms enlaces que antes. No estara bien tener una forma de
clasificarlos? Si no lo sabas, este es precisamente el tema del siguiente captulo!

Votos

13

Ahora que nuestro sitio es cada vez ms popular, empieza a ser complicado buscar los mejores posts.
Lo que necesitamos es algn tipo de sistema de clasificacin para ordenarlos.
Podramos construir un sistema de clasificacin complejo con karma, basado en tiempo, y muchas
otras cosas (la mayora de las cuales se implementan en Telescope, el hermano mayor de
Microscope). Nosotros vamos a mantener las cosas sencillas y ordenaremos los posts por el nmero
de votos que reciban.
Vamos a empezar proporcionado a los usuarios una manera de votar los posts.

El modelo de datos
Vamos a guardar una lista de upvoters en cada post para que sepamos dnde mostrar el botn
upvote a los usuarios, as como para evitar que la gente vote varias veces el mismo post.

Privacidad de datos y publicaciones


Vamos a publicar las listas de upvoters a todos los usuarios, por lo que automticamente
tendrn a su disposicin los datos a travs de la consola de navegador.
Este es el tipo de problema de privacidad que puede surgir por la forma en la que trabajan
las colecciones. Por ejemplo, queremos que la gente pueda averiguar quin ha votado sus
posts? En nuestro caso, no tendra ninguna consecuencia, pero es importante por lo menos
reconocer el problema.

Tambin vamos a denormalizar el nmero total de upvoters de un post para que sea ms fcil
recuperar esa cifra. As que vamos a aadir dos atributos a nuestros posts, upvoters y votes .
Vamos a empezar aadindolos a nuestros datos de prueba:

// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2,
upvoters: [],
votes: 0
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),

commentsCount: 0,
upvoters: [],
votes: 0
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000),
commentsCount: 0,
upvoters: [],
votes: 0
});
for (var i = 0; i < 10; i++) {
Posts.insert({
title: 'Test post #' + i,
author: sacha.profile.name,
userId: sacha._id,
url: 'http://google.com/?q=test-' + i,
submitted: new Date(now - i * 3600 * 1000 + 1),
commentsCount: 0,
upvoters: [],
votes: 0
});
}
}

server/fixtures.js

Como de costumbre, ejecutamos meteor reset y creamos una nueva cuenta de usuario. Ahora nos
aseguraremos que inicializamos las dos nuevas propiedades cuando se crean los posts:

//...
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0,
upvoters: [],
votes: 0
});
var postId = Posts.insert(post);
return {
_id: postId
};

//...

collections/posts.js

Plantillas de voto
Lo primero es aadir un botn upvote a nuestros posts y mostrar el contador de votos en los
metadatos del post:

<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default"> </a>
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
{{votes}} Votes,
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>

client/templates/posts/post_item.html

El botn upvote

Despus, llamaremos a un mtodo en el servidor cuando el usuario haga clic en el botn:

//...
Template.postItem.events({
'click .upvote': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});

client/templates/posts/post_item.js

Finalmente, volvemos a lib/collections/posts.js para aadir el mtodo de servidor que


actualizar los votos:

//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var post = Posts.findOne(postId);
if (!post)
throw new Meteor.Error('invalid', 'Post not found');
if (_.include(post.upvoters, this.userId))
throw new Meteor.Error('invalid', 'Already upvoted this post');
Posts.update(post._id, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
}
});

//...

lib/collections/posts.js

Commit 13-1
Algoritmo bsico de voto.
Ver en GitHub

Lanzar instancia

El mtodo es bastante sencillo. Hacemos algunas comprobaciones para garantizar que el usuario ha
iniciado sesin y que el post realmente existe. Despus de corroborar que el usuario no ha votado ya
este post, incrementamos el total de votos y aadimos al usuario a la lista de upvoters.
Este ltimo paso es muy interesante. Hemos utilizado un par de operadores de Mongo que son muy
tiles: $addToSet agrega un elemento a una lista siempre y cuando este no exista ya en ella, y $inc
simplemente incrementa un entero.

Mejoras en la interfaz de usuario


Si el usuario no est conectado, o ya ha votado uno de los posts, no podr votar el post. Para reflejar
esto en nuestra interfaz de usuario, usaremos un ayudante para aadir condicionalmente una clase
CSS para que deshabilite el botn upvote.

<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default {{upvotedClass}}">
<div class="post-content">
//...
</div>
</template>

client/templates/posts/post_item.html

</a>

Template.postItem.helpers({
ownPost: function() {
//...
},
domain: function() {
//...
},
upvotedClass: function() {
var userId = Meteor.userId();
if (userId && !_.include(this.upvoters, userId)) {
return 'btn-primary upvotable';
} else {
return 'disabled';
}
}
});
Template.postItem.events({
'click .upvotable': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});

client/templates/posts/post_item.js

Creamos el ayudante upvotedClass para cambiar la clase .upvote por .upvotable , as que no
podemos olvidar hacerlo tambin en el controlador de eventos.
client/views/posts/post_item.js

Deshabilitando el botn upvote.

Commit 13-2
Deshabilitado el botn upvote si el usuario no ha accedid
Ver en GitHub

Lanzar instancia

Ahora nos damos cuenta que los posts con un solo voto estn etiquetados como 1 votes, por lo que
vamos a pararnos a pluralizar las etiquetas correctamente. La pluralizacin puede ser un proceso
complicado, pero por ahora vamos a hacerlo de una manera bastante simple. Crearemos un nuevo
ayudante Spacebars que podemos utilizar desde cualquier lugar:

Template.registerHelper('pluralize', function(n, thing) {


// fairly stupid pluralizer
if (n === 1) {
return '1 ' + thing;
} else {
return n + ' ' + thing + 's';
}
});

client/helpers/spacebars.js

Los ayudantes que hemos creado con anterioridad siempre han estado relacionados con la plantilla a
la que se aplican. Pero usando Template.registerHelper , hemos creado un ayudante global que se
puede utilizar dentro de cualquier plantilla:

<template name="postItem">
//...
<p>
{{pluralize votes "Vote"}},
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>

client/templates/posts/post_item.html

Pluralizando

Commit 13-3
Aadida una funcin para pluralizar textos.
Ver en GitHub

Lanzar instancia

Ahora ya deberamos ver 1 Vote.

Un algoritmo de voto ms inteligente


Nuestro cdigo para el mtodo upvote en collections/posts.js parece bueno, pero todava
podemos hacerlo mejor. En l, hacemos dos llamadas a Mongo: una para obtener el post y otra para
actualizarlo.
Veamos como con esta aproximacin, tenemos dos problemas. En primer lugar, es ineficaz consultar

la base de datos dos veces. Pero lo ms importante es que se introduce una condicin de carrera.
Estamos siguiendo el siguiente algoritmo:
1. Coger el post desde la base de datos.
2. Comprobar si el usuario lo ha votado.
3. Si no, aadir un voto.
Qu pasa si el mismo usuario vota el mismo post entre los pasos 1 y 3? Nuestro cdigo abre la puerta
al usuario a votar dos veces el mismo post. Afortunadamente, Mongo nos permite ser ms inteligentes
y combinar las consultas 1 y 3 en una sola:

//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var affected = Posts.update({
_id: postId,
upvoters: {$ne: this.userId}
}, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
if (! affected)
throw new Meteor.Error('invalid', "You weren't able to upvote that post");
}
});

//...

collections/posts.js

Commit 13-4
Mejorado el algoritmo de voto.
Ver en GitHub

Lanzar instancia

Lo que estamos diciendo es encuentra todos los posts con este id que todava no hayan sido
votados por este usuario, y actualzalos. Si el usuario an no ha votado el post con esta id , lo
encontrar, pero si ya lo ha hecho, la consulta no coincidir con ningn documento, y por lo tanto no
ocurrir nada.

Compensacin de la latencia
Digamos que tratas de engaarnos y pones uno de tus posts el primero de la lista ajustando
su nmero de votos desde la consola del navegador:

> Posts.update(postId, {$set: {votes: 10000}});

Consola del navegador

(Donde postId es el id de uno de tus posts))


Este descarado intento de jugar con el sistema sera capturado por nuestro callback deny()
(en collections/posts.js , recuerdas?) e inmediatamente denegada.
Pero si miras detenidamente, podrs ver la compensacin de latencia en accin. Puede ser
rpido, pero el post saltar brevemente al principio de la lista antes de volver de nuevo a su
posicin.
Qu est pasando? En su coleccin de Posts locales, la actualizacin se ha aplicado sin
incidentes. Esto sucede al instante, por lo que el mensaje se va al principio de la lista.
Mientras tanto, en el servidor, se deniega la actualizacin, de forma que un poco ms tarde
(milisegundos si ests ejecutando Meteor en tu propia mquina), el servidor devuelve un
error, obligando a la coleccin local a revertirse.
El resultado final: mientras esperamos a que el servidor responda, la interfaz de usuario no
puede dejar de confiar en la coleccin local. Tan pronto como el servidor deniega la
modificacin, las interfaces de usuario se adaptan para reflejarlo.

Posicionando los posts en la pgina principal


Ahora que disponemos de una puntuacin para cada post en funcin del nmero de votos, vamos a
mostrar una lista de los mejores. Para ello, vamos a ver cmo gestionamos dos suscripciones
separadas contra la coleccin de posts, y, de paso hacer nuestra plantilla postsList un poco ms

general.
Para empezar, queremos tener dos suscripciones, una para cada tipo de ordenacin. El truco aqu es
suscribirse a la misma publicacin, solo que con diferentes argumentos!.
Tambin crearemos dos nuevas rutas denominadas newPosts y bestPosts , accesibles desde las
direcciones /new y /best respectivamente (junto con /new/5 y /best/5 para la paginacin, por
supuesto).
Para ello, vamos a extender nuestro PostsListController en dos controladores distintos:
NewPostsListController

y BestPostsListController . Esto nos permitir reutilizar las mismas

opciones de ruta, tanto para las rutas home y newPosts , quedndonos un solo
NewPostsListController

del que heredar. Y, adems, todo esto es una buena ilustracin de lo

flexible que puede ser Iron Router.


Vamos a reemplazar la propiedad de ordenacin {submitted: -1} en PostsListController por
this.sort

, que ser proporcionado por NewPostsListController y BestPostsListController :

//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: this.sort, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
return {

posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? this.nextPath() : null
};
}
});
NewPostsController = PostsListController.extend({
sort: {submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.newPosts.path({postsLimit: this.postsLimit() + this.incre
ment})
}
});
BestPostsController = PostsListController.extend({
sort: {votes: -1, submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.bestPosts.path({postsLimit: this.postsLimit() + this.incr
ement})
}
});
Router.route('/', {
name: 'home',
controller: NewPostsController
});
Router.route('/new/:postsLimit?', {name: 'newPosts'});
Router.route('/best/:postsLimit?', {name: 'bestPosts'});

lib/router.js

Ten en cuenta que ahora que tenemos ms de una ruta, sacamos la lgica para nextPath de
PostsListController

y la ponemos en NewPostsController y BestPostsController , ya que el

path ser diferente en uno y otro caso.


Adems, cuando ordenamos por votos, establecemos un segundo orden por fecha y por _id para
garantizar que el orden est completamente especificado.
Una vez listos los nuevos controladores podemos deshacernos de la ruta anterior a postsList .

Simplemente eliminamos el siguiente cdigo:

Router.route('/:postsLimit?', {
name: 'postsList'
})

lib/router.js

Vamos a aadir los enlaces en la cabecera:

<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li>
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li>
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>

client/templates/includes/header.html

Finalmente, tambin necesitamos actualizar el controlador de eventos para borrar posts:

'click .delete': function(e) {


e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('home');
}
}

client/templates/posts/posts_edit.js

Con todo listo, ahora obtenemos lista de posts mejorada:

Posts ordenados

Commit 13-5
Aadidas rutas para las listas de posts y pginas para mo
Ver en GitHub

Lanzar instancia

Mejorando la cabecera
Ahora que tenemos dos listas, puede resultar difcil saber cul de ellas estamos viendo. As que vamos
a revisar nuestra cabecera para que sea ms evidente. Vamos a crear el gestor header.js y un
ayudante auxiliar que use la ruta actual y una o ms rutas con nombre para activar una clase en
nuestros elementos de navegacin:
La razn por la que queremos permitir varias rutas es que tanto la ruta home como la ruta newPosts
(que se corresponden con las nuevas URLs / y /new ) devuelven la misma plantilla, lo que significa
que nuestro activeRouteClass debe ser lo suficientemente inteligente como para activar la etiqueta
<li>

en ambos casos.

<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li class="{{activeRouteClass 'home' 'newPosts'}}">
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li class="{{activeRouteClass 'bestPosts'}}">
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li class="{{activeRouteClass 'postSubmit'}}">
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>

client/templates/includes/header.html

Template.header.helpers({
activeRouteClass: function(/* route names */) {
var args = Array.prototype.slice.call(arguments, 0);
args.pop();
var active = _.any(args, function(name) {
return Router.current() && Router.current().route.getName() === name
});
return active && 'active';
}
});

client/templates/includes/header.js

Mostrando la pgina activa

Argumentos de los ayudantes


Hasta ahora no hemos usado este modelo en concreto, pero al igual que para cualquier otro
tag de Spacebars, los tags de los ayudantes de plantilla tambin pueden tomar argumentos.
Y aunque por supuesto, se pueden pasar argumentos especficos a la funcin, tambin se
pueden pasar un nmero indeterminado de ellos y recuperarlos mediante una llamada al
objeto arguments dentro de la funcin.
En este ltimo caso, es probable que queramos convertir el objeto arguments a un array
JavaScript y luego llamar a pop() sobre l para deshacernos del hash que aade
Spacebars.

Para cada elemento, el ayudante activeRouteClass toma una lista de nombres de ruta, y luego
utiliza el ayudante any() de Underscore para ver si las rutas pasan la prueba (es decir, que su
correspondiente URL sea igual a la actual). Si cualquiera de las rutas se corresponde con la actual,
any()

devolver true .

Por ltimo, estamos aprovechando el patrn boolean && myString de JavaScript con el que false
&& myString

devuelve false , pero los true && myString devuelve myString .

Commit 13-6
Aadidas clases activas a la cabecera.
Ver en GitHub

Lanzar instancia

Ahora que los usuarios pueden votar en tiempo real, podemos ver cmo saltan los posts hacia arriba
o abajo segn cambia su clasificacin. Pero no sera ms agradable si hubiera una manera de
suavizar estos cambios con algunas animaciones?

Publicaciones avanzadas

SIDEBAR

13.5

A estas alturas ya deberas conocer bastante bien cmo interactan las suscripciones y las
publicaciones. As que vamos a deshacernos de las ruedas de entrenamiento y examinar algunos
escenarios ms avanzados.

Publicar una Coleccin Varias Veces


En la primera sidebar, vimos algunos de los patrones ms comunes de publicacin y suscripcin, y
aprendimos cmo la funcin _publishCursor las hace muy fciles de usar en nuestras aplicaciones.
Primero, vamos a recapitular exactamente qu hace por nosotros la funcin _publishCursor : lo que
hace es tomar todos los documentos que coinciden con un cursor dado, y los enva a la coleccin del
cliente del mismo nombre. Ten en cuenta que el nombre de la publicacin no est involucrado.
Esto significa que podemos tener ms de una publicacin de cualquier coleccin, que enlaza al cliente
y al servidor
Ya hemos encontrado este patrn en el captulo paginacin, cuando publicamos un subconjunto
paginado de todos los posts, adems del post actual.
Otro caso de uso similar es publicar una overview de un gran conjunto de documentos, as como
tambin los detalles completos de un nico tem:

Publishing a collection twice

Meteor.publish('allPosts', function() {
return Posts.find({}, {fields: {title: true, author: true}});
});
Meteor.publish('postDetail', function(postId) {
return Posts.find(postId);
});

Ahora cuando el cliente se suscriba a esas dos publicaciones, su coleccin 'posts' se rellena desde
dos fuentes: una lista de ttulos y nombres de autor de la primera suscripcin, y los detalles completos
de un nico post de la segunda.
Tal vez te hayas dado cuenta de que el post publicado por postDetail se publica tambin desde
allPosts

(aunque solo con un subconjunto de sus propiedades). Sin embargo, Meteor se hace cargo

de la superposicin fusionando los campos y asegurando que no haya duplicados.


Esto es genial, porque ahora cuando renderizemos la lista de los resmenes de los posts, estaremos
lidiando con objetos de datos que tienen lo suficiente para mostrar lo que necesitamos. Y cuando
renderizemos la pgina de un slo post, tambn lo tenemos. Por supuesto, tenemos que ocuparnos
que el cliente no espere que todos los campos estn disponibles en todos los posts esto es un error
comn!
Ten en cuenta que no estamos limitados a variar las propiedades de los documentos. Podramos
tambin, publicar las mismas propiedades en ambas publicaciones, pero ordenar los items de otra
manera.

Meteor.publish('newPosts', function(limit) {
return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});
Meteor.publish('bestPosts', function(limit) {
return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});

server/publications.js

Suscribindose a una Publicacin Mltiples Veces


Acabamos de ver cmo se puede publicar una sola coleccin ms de una vez. Resulta que se puede
lograr un resultado muy similar con otro patrn: creando una nica publicacin, pero
suscribindonos a ella varias veces.

En Microscope, nos suscribimos a la publicacin de posts varias veces, pero Iron Router activa y
desactiva cada suscripcin por nosotros. Aun as, no hay ninguna razn por la cual no podamos
suscribirnos muchas veces simultneamente.
Por ejemplo, digamos que queremos cargar los posts ms recientes y los mejores en la memoria al
mismo tiempo:

Subscribing twice to one publication

Estamos estableciendo una sola publicacin:

Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});

Luego, nos suscribimos a esta publicacin mltiples veces. De hecho, esto es ms o menos
exactamente lo que estamos haciendo en Microscope:

Meteor.subscribe('posts', {submitted: -1, limit: 10});


Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});

Entonces, qu est pasando exactamente? Cada navegador abre dos suscripciones diferentes, cada
uno se conecta a la misma publicacin en el servidor.
Cada suscripcin ofrece diferentes argumentos para esa publicacin, pero fundamentalmente, cada
vez que un conjunto de documentos (diferente) se saca de la coleccin posts , se enva por el canal a
la coleccin del lado del cliente.
Puedes incluso suscribirte dos veces a la misma publicacin con los mismos argumentos! Es difcil
pensar en escenarios donde esto sea de utilidad, pero esta flexibilidad puede que sea til algn da!

Mltiples Colecciones sobre una nica Suscripcin


A diferencia de las bases de datos relacionales tradicionales como MySQL que hacen uso de joins , las
bases de datos NoSQL como Mongo se basan en la denormalizacin e incrustacin. Vamos a ver cmo
funciona en el contexto de Meteor.
Veamos un ejemplo concreto. Hemos aadido comentarios a nuestros posts, y hasta ahora, hemos
publicado los comentarios en la pgina de un nico post.

Sin embargo, supongamos que queremos mostrar todos los comentarios en los posts en la pgina
principal (teniendo en cuenta que estos posts van a cambiar a medida que paginamos a travs de
ellos). Este caso de uso presenta una buena razn para insertar comentarios en los posts, y de hecho
es lo que nos empuja a desnormalizar la coleccin de comentarios.
Por supuesto que siempre se pueden insertar los comentarios en los posts, deshacindonos de la
coleccin de comments por completo. Pero, como hemos visto anteriormente en el captulo
Desnormalizacin, al hacerlo estaramos perdiendo algunos beneficios adicionales de trabajar con
colecciones separadas.
Pero resulta que hay un truco que hace posible embeber nuestros comentarios, preservando
colecciones separadas.
Supongamos que, junto con la lista de la pgina principal de posts, queremos suscribirnos a una lista
de los mejores 2 comentarios para cada uno de ellos.
Lograr esto con una publicacin de comentarios independiente sera difcil, sobre todo si la relacin
de posts se limita de alguna manera (por ejemplo, los 10 ms recientes). Tendramos que crear una
publicacin que se pareciera a algo como esto:

Two collections in one subscription

Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: topPostIds});
});

Esto sera un problema desde el punto de vista de rendimiento, ya que habra que eliminar y volver a
establecer la publicacin cada vez que cambiara la lista de topPostIds .

Existe una manera de evitar esto. Acabamos de utilizar el hecho de que no solo podemos tener ms de
una publicacin por coleccin, sino que tambin podemos tener ms de una coleccin por publicacin:

Meteor.publish('topPosts', function(limit) {
var sub = this, commentHandles = [], postHandle = null;

// send over the top two comments attached to a single post


function publishPostComments(postId) {
var commentsCursor = Comments.find({postId: postId}, {limit: 2});
commentHandles[postId] =
Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
}
postHandle = Posts.find({}, {limit: limit}).observeChanges({
added: function(id, post) {
publishPostComments(id);
sub.added('posts', id, post);
},
changed: function(id, fields) {
sub.changed('posts', id, fields);
},
removed: function(id) {
// stop observing changes on the post's comments
commentHandles[id] && commentHandles[id].stop();
// delete the post
sub.removed('posts', id);
}
});
sub.ready();

// make sure we clean everything up (note `_publishCursor`


//
does this for us with the comment observers)
sub.onStop(function() { postHandle.stop(); });
});

Tengamos en cuenta que no estamos devolviendo nada en esta publicacin, le enviamos mensajes
manualmente a la sub nosotros mismos (a travs de .added() y sus amigos). As que no
necesitamos que _publishCursor lo haga mediante la devolucin de un cursor.
Ahora, cada vez que publiquemos un post tambin publicaremos automticamente los dos primeros

comentarios adjuntos. Y todo con una sola llamada a una suscripcin!


Aunque Meteor an no hace este enfoque muy sencillo, tambin se puede utilizar el paquete
publish-with-relations

de Atmosphere, cuyo objetivo es hacer que este patrn sea ms fcil de

usar.

Enlazando colecciones diferentes


Qu mas puede darnos nuestro nuevo conocimiento sobre la flexibilidad de las suscripciones?
Bueno, si no usamos _publishCursor , no tendremos la restriccin de que la fuente de la coleccin
en el servidor necesita tener el mismo nombre que la coleccin de destino en el cliente.

One collection for two subscriptions

Una razn por la que no querramos hacer esto es la Herencia de Tabla Simple
Supongamos que quisiramos referenciar varios tipos de objetos desde nuestros posts, cada uno
alojado en campos comunes pero ligeramente diferentes en contenido. Por ejemplo, podramos estar
creando un motor de blogging al estilo de Tumblr en el que cada post posee el habitual ID, un
timestamp, y el ttulo. Pero tambin puede tener imgenes, videos, links o simplemente texto.

Podramos guardar todos estos objetos en una coleccin llamada 'resources' (recursos), usando
un atributo type que indique qu tipo de objeto son ( video , image , link , etc.).
Y aunque tendramos una sola coleccin resources en el servidor, podramos transformar esa nica
coleccin en mltiples colecciones en el cliente, como Videos , Images , etc., con el siguiente trozo
de magia:

Meteor.publish('videos', function() {
var sub = this;
var videosCursor = Resources.find({type: 'video'});
Mongo.Collection._publishCursor(videosCursor, sub, 'videos');

// _publishCursor doesn't call this for us in case we do this more than once.
sub.ready();
});

Le estamos diciendo a _publishCursor que publique nuestros videos (como hacer un return) como
lo hara el cursor, pero en lugar de publicar la coleccin resources en el cliente, publicamos de
resources

a videos .

Otra idea similar es usar publish para una coleccin en el lado del cliente donde no hay ninguna
coleccin en el lado servidor!. Por ejemplo, podras obtener datos de un servicio de terceros, y
publicarlos como si fuera una coleccin en el cliente.
Gracias a la flexibilidad de la API de publicacin, las posibilidades son ilimitadas.

Animaciones

14

Aunque contamos un sistema de votacin en tiempo real, no tenemos una gran experiencia de
usuario viendo la forma en la que los posts se mueven en la pgina principal. Usaremos animaciones
para suavizar este problema.

Introduciendo a los

_uihooks

Los _uihooks son una caracterstica de Blaze relativamente nueva y poco documentada. Como su
propio nombre indica, nos da acceso a acciones que podemos ejecutar cuando se insertan, eliminan o
animan elementos.
La lista completa de acciones es esta:

insertElement
moveElement

: se llama cuando se inserta un elemento.

: se llama cuando un elemento cambia su posicin.

removeElement

: se llama cuando se elimina un elemento.

Una vez definidas, estas acciones reemplazarn el comportamiento que Meteor tiene por defecto. En
otras palabras, en vez de insertar, mover o eliminar elementos, Meteor usar el comportamiento que
hayamos definido y ser cosa nuestra que ese comportamiento sea correcto!

Meteor y el DOM
Antes de poder empezar con la parte divertida (hacer que se muevan las cosas), tenemos que
entender cmo interacta Meteor con el DOM (Document Object Model - la coleccin de elementos
HTML que componen el contenido de una pgina).
Lo ms importante que hay que tener en cuenta es que los elementos del DOM realmente no se
pueden mover. Slo se pueden aadir y eliminar (esto es una limitacin del propio DOM, no de
Meteor). As que para crear la ilusin de que los elementos A y B se intercambian, Meteor tendr que

eliminar B e insertar una nueva copia (B) antes del elemento A.


Esto hace de la animacin algo complicado ya que, no podemos simplemente mover B a una nueva
posicin, porque B habr desaparecido tan pronto como Meteor redibuje de nuevo la pgina (que,
como sabemos, sucede instantneamente gracias a la reactividad). Pero no te preocupes,
encontraremos la manera de hacerlo.

El corredor ruso
Pero, primero, una historia.
En 1980, en pleno apogeo de la guerra fra, los Juegos Olmpicos se celebraban en Mosc, y los
soviticos estaban decididos a ganar la carrera de 100 metros a cualquier precio. As que un grupo de
brillantes cientficos soviticos equiparon a uno de sus atletas con un teletransportador, y en cuanto
son el disparo de salida, el corredor fue trasladado de inmediato a la lnea de meta.
Afortunadamente, los jueces de la carrera se dieron cuenta de la infraccin inmediatamente, y el
atleta no tuvo ms remedio que teletransportarse de nuevo a su casilla de salida, antes de permitirle
participar de nuevo corriendo como los dems.
Mis fuentes histricas no son muy fiables, por lo que debes tomar esa historia como cogida con
pinzas. Pero trataremos de mantener en mente la analoga del corredor sovitico con
teletransportador a medida que avancemos en este captulo.

Analizando el problema detenidamente


Cuando Meteor recibe una actualizacin y modifica reactivamente el DOM, nuestro post se
teletransporta inmediatamente a su posicin final, al igual que el corredor sovitico. Pero como en los
Juegos Olmpicos, en nuestra aplicacin, podemos tener alrededor cosas que no se teletransportan.
As que tendremos que teletransportarlo a la casilla de salida y hacerlo correr (en otras palabras,
animarlo) de nuevo hasta la lnea de meta.

Para intercambiar los elementos A y B (situados en las posiciones p1 y p2, respectivamente),


tendremos que seguir los siguientes pasos:
1. Borrar B
2. Crear B antes de A en el DOM
3. Teletransportar B a p2
4. Teletransportar A a p1
5. Amimar A hasta p2
6. Animar B hasta p1
El siguiente diagrama explica estos pasos con ms detalle:

Intercambiando dos posts

De nuevo, en los pasos 3 y 4 no estamos animando A y B hasta sus posiciones sino que las
teletransportamos all al instante. Dado que el cambio es instantneo, parecer que B no se ha
borrado, pero ya tenemos posicionados correctamente los elementos para que puedan ser animados
hasta su nueva posicin.
Afortunadamente, Meteor se ocupa de los pasos 1 y 2 y re-implementarlos ser una tarea fcil. En los
pasos 5 y 6, lo nico que hacemos es mover los elementos al lugar adecuado. As que, slo tenemos
que preocuparnos de los pasos 3 y 4, enviar los elementos al punto de arranque de la animacin.

Posicionamiento CSS
Para animar los posts que se estn reordenando por la pgina, vamos a tener que meternos en
territorio CSS. Sera recomendable una rpida revisin del posicionamiento con CSS.

Los elementos de una pgina utilizan posicionamiento esttico por defecto. Los elementos
posicionados de forma esttica estn fijos y sus coordenadas no se pueden cambiar o animar.
Por otra parte, el posicionamiento relativo, implica que el elemento est fijado a la pgina, pero se
puede mover con relacin a su posicin original.
El posicionamiento absoluto va un paso ms all y permite dar coordenadas x/y a un elemento en
relacin al documento o al primer elemento padre posicionado de forma absoluta o relativa.
Nosotros vamos a usar posicionamiento relativo en nuestras animaciones. Ya disponemos del CSS
necesario en client/stylesheets/style.css , pero si necesitas aadirlo, este es el cdigo para la
hoja de estilo:

.post{
position:relative;
}
.post.animate{
transition:all 300ms 0ms ease-in;
}

client/stylesheets/style.css

Ten en cuenta que slo animamos los posts con la clase CSS .animate . De esta forma, podemos
aadir y quitar esa clase para controlar cundo deben producirse o no las animaciones.
Esto facilita muchos los pasos 5 y 6: todo lo que necesitamos hacer es configurar la parte top a 0px
(su valor predeterminado) y nuestros posts se deslizarn de nuevo a su posicin normal.
Esto significa que nuestro nico problema es averiguar desde dnde animar los posts (pasos 3 y 4)
con respecto a su nueva posicin. En otras palabras, en qu posicin hay que ponerlo. Pero, esto no es
tan difcil: el desplazamiento correcto (oset) es la posicin anterior restada a la nueva.

implementando los _uihooks`


Ahora que entendemos los diferentes factores que entran en juego en la animacin de una lista de
elementos, estamos listos para empezar a aplicar la animacin. Lo primero que necesitaremos para
envolver nuestra lista de posts en un nuevo contenedor .wrapper :

<template name="postsList">
<div class="posts page">
<div class="wrapper">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>

client/templates/posts/posts_list.html

Antes de continuar, vamos a revisar cul es el comportamiento actual sin animaciones:

La lista de posts no-animada.

La lista de posts no-animada.

Vamos a por los _uihooks . Dentro del callback onRendered de la plantilla, seleccionamos el div
.wrapper

, y definimos la accin moveElement .

Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
// do nothing for now
}
}
});

client/templates/posts/posts_list.js

Cada vez que cambie la posicin de un elemento, en vez de obtener el comportamiento


predeterminado de Blaze, Meteor llamar a la funcin moveElement . Y, dado que la funcin est
vaca, no va a pasar nada.
Adelante, probemos: abre la vista de los Mejores posts y vota unos cuantos: el orden no cambiar
hasta que no fuerces un re-render (ya sea volviendo a cargar la pgina o movindote entre distintas
rutas).

Un callback moveElement vaco: no ocurre nada

Un callback moveElement vaco: no ocurre nada

Hemos comprobado que los _uihooks funcionan. Ahora vamos a hacer que animen los posts!

Animando los posts a travs del reordenamiento


La accin moveElement toma dos argumentos: node y next .

node

es el elemento que se est moviendo a una nueva posicin en el DOM.

next

es el elemento que hay justo despus de la nueva posicin a la que estamos moviendo

node

Sabiendo esto, podemos definir el proceso de animacin (si necesitas refrescar la memoria, no dudes
en volver al ejemplo del Corredor Ruso). Cuando detectamos un nuevo cambio en la posicin de un
elemento, tendremos que hacer lo siguiente:
1. Insertar node antes de next (en otras palabras, establecer el comportamieento por defecto,
como si no hubiramos definido la accin moveElement ).
2. Mover node a su posicin original.
3. Moveremos todos los elementos que hay entre node y next para hacer sitio a node .
4. Animaremos todos los elementos hasta su posicin original.
Para hacer todo esto usaremos la magia de jQuery, de lejos, la mejor librera de manipulacin del
DOM que existe. jQuery est fuera del alcance de este libro, pero vamos a ver rpidamente los
mtodos que vamos a usar:
Con $() convertimos cualquier elemento del DOM en un objeto jQuery.
offset()

recupera la posicin de un elemento en relacin al documento, y devuelve un objeto

que contiene las propiedades top y left .


Con outerHeight() obtenemos la altura exterior (incluyendo el padding y, opcionalmente,
el margin) de un elemento.
Con nextUntil(selector) obtenemos todos los elementos que hay despus del elemento
seleccionado con el selector , excepto ste ltimo.
Con insertBefore(selector) insertamos un elemento antes del que seleccionamos con el
selector

Con removeClass(class) eliminamos la clase CSS class , si est presente en el elemento.


Con css(propertyName, propertyValue) establecemos el valor propertyValue para la
propiedad propertyName .

Con height() obtenemos la altura de un elemento.


Con addClass(class) aadimos la clase class a un elemento.

Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
var $node = $(node), $next = $(next);
var oldTop = $node.offset().top;
var height = $node.outerHeight(true);

// find all the elements between next and node


var $inBetween = $next.nextUntil(node);
if ($inBetween.length === 0)
$inBetween = $node.nextUntil(next);
// now put node in place
$node.insertBefore(next);
// measure new top
var newTop = $node.offset().top;
// move node *back* to where it was before
$node
.removeClass('animate')
.css('top', oldTop - newTop);
// push every other element down (or up) to put them back
$inBetween
.removeClass('animate')
.css('top', oldTop < newTop ? height : -1 * height)

// force a redraw
$node.offset();
// reset everything to 0, animated
$node.addClass('animate').css('top', 0);
$inBetween.addClass('animate').css('top', 0);
}
}
});

client/templates/posts/posts_list.js

Algunas notas:
Calculamos la altura de $node para saber cunto debemos mover los elementos $inBetween .
Y usamos outerHeight(true) para incluir margen y padding en el clculo.
No sabemos si next va antes o despus de node as que comprobamos las dos
configuraciones cuando definimos $inBetween .
Para cambiar los elementos de teletransportados a animados, simplemente aadimos o
quitamos la clase animate (la animacin definida en el cdigo CSS de la aplicacin).
Dado que usamos posicionamiento relativo, siempre podemos poner a 0 la propiedad top del
elemento para devolverlo a la posicin dnde se supone que tiene que ir.

Forzando el redibujado
Te estars preguntando para qu es la lnea $node.offset() . Para qu obtenemos la
posicin de $node si no vamos a hacer nada con ella?
Mralo as: si le dices a un robot muy inteligente que se mueva al norte 5 kilmetros, y luego
al sur otros 5, probablemente sabr deducir que va a terminar en el mismo sitio, y que puede
ahorrar energa y hacer bien el trabajo sin moverse.
As que si quieres que el robot ande 10 kilmetros, le diremos que mida sus coordenadas a
los 5 kilmetros, antes de que de la vuelta.
El navegador funciona de una manera similar: si le damos las instrucciones css('top',
oldTop - newTop)

y css('top', 0) a la vez, las nuevas coordenadas reemplazarn las

viejas y no pasar nada. Si queremos ver la animacin, debemos forzar al navegador a


redibujar el elemento despus de moverlo la primera vez.
Una forma sencilla de hacerlo es pedirle al navegador el offset del elemento.

Vamos a probar de nuevo. Volvamos a la vista Best y votemos unos posts: Deberas verlos
movindose suavemente hacia arriba y hacia abajo como en un ballet!

Animated reordering

Animated reordering

Commit 14-1
Added post reordering animation.
Ver en GitHub

Aparecer y desaparecer

Lanzar instancia

Ahora que ya tenemos resuelta la reordenacin ms complicada, animar las inserciones y


eliminaciones va ser muy sencillo.
Primero, haremos aparecer nuevos posts (esta vez, por simplicidad, usaremos animaciones
JavaScript):

Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
}
}
});

client/templates/posts/posts_list.js

Para ver el resultado, podemos probar a insertar un post va consola:

Meteor.call('postInsert', {url: 'http://apple.com', title: 'Testing Animations'})

Fading in new posts

Fading in new posts

Y ahora, haremos desaparecer los posts eliminados:

Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});

client/templates/posts/posts_list.js

De nuevo, para ver el efecto, prueba a eliminar algn post desde la consola
( Posts.remove('algunPostId') ).

Fading out deleted posts

Fading out deleted posts

Commit 14-2
Fade items in when they are drawn.
Ver en GitHub

Transiciones entre pginas

Lanzar instancia

Hemos creado animaciones para elementos dentro de una pgina. Pero, qu pasa si queremos
animar las transiciones entre pginas?
Las transiciones entre pginas son trabajo del Iron Router. Haces click en un enlace y se reemplaza el
contenido del ayudante {{> yield}} en layout.html
Ocurre que, como cuando reemplazamos el comportamiento de Blaze para la lista de posts,
podemos hacer lo mismo para el elemento {{> yield}} y aadirle un efecto de transicin entre
rutas!
Si queremos animar la entrada y salida entre dos pginas, debemos asegurarnos de que se muestran
una por encima de la otra. Lo hacemos usando la propiedad position:absolute en el contenedor
.page

que envuelve a todas las plantillas de pginas.

Piensa que no queremos que las pginas estn posicionadas de forma absoluta, porque de esta
forma, se solaparan con la cabecera de la app. As que establecemos la propiedad
position:relative
.page

en el div #main que las contiene, de forma que el position:absolute de

tome su origen desde #main .

Para ahorrar tiempo, hemos aadido el cdigo necesario a style.css :

//...
#main{
position: relative;
}
.page{
position: absolute;
top: 0px;
width: 100%;
}
//...

client/stylesheets/style.css

Es el momento de aadir el cdigo para las transiciones entre pginas. Nos debe resultar familiar,
puesto que es exactamente el mismo que para las inserciones y eliminaciones de posts:

Template.layout.onRendered(function() {
this.find('#main')._uihooks = {
insertElement: function(node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});

client/templates/application/layout.js

Transitioning in-between pages with a fade

Transitioning in-between pages with a fade

Commit 14-3
Transition between pages by fading.
Ver en GitHub

Lanzar instancia

Hemos visto unos pocos patrones para animar elementos en nuestra aplicacin Meteor. Aunque no es

una lista exhaustiva, con suerte, nos aportar una base sobre la que construir transiciones ms
elaboradas.

Ir ms lejos

14.5

Esperamos que con la lectura de los captulos anteriores tengas una buena visin general de todo lo
que involucra la construccin de una aplicacin Meteor. Entonces, dnde podemos ir ahora?.

Captulos extra
En primer lugar, puedes comprar las ediciones Full o Premium para desbloquear el acceso a los
captulos adicionales. Estos captulos te guiarn a travs de escenarios del mundo real, tales como la
construccin de una API para tu aplicacin, la integracin con servicios de terceros o la migracin de
datos.

Manual de Meteor
Adems de contar con la documentacin oficial, el Manual de Meteor profundiza en temas
especficos como Tracker o Blaze.

Evented Mind
Si quieres sumergirte en los entresijos de Meteor, te recomendamos echarle un vistazo a Evented
Mind de Chris Mather, una plataforma de aprendizaje con ms de 50 vdeos sobre Meteor (y nuevos
vdeos que se agregan cada semana).

MeteorHacks
Una de las mejores formas de mantenerse al da con Meteor es suscribirse al boletn semanal de
Arunoda Susiripala MeteorHacks. En el blog tambin puedes encontrar un montn de consejos
avanzados sobre Meteor.

Atmosphere

Atmosphere es el repositorio de paquetes no oficiales de Meteor, es otro gran lugar para aprender
ms: puedes descubrir nuevos paquetes y echar un vistazo a su cdigo para ver qu patrones utiliza la
gente.
(Aviso legal: Atmosphere es mantenida, en parte por Tom Coleman, uno de los autores de este libro).

Meteorpedia
Meteorpedia es un wiki sobre Meteor. Y, por supuesto, est hecho con Meteor!

BulletProof Meteor
Otra iniciativa de Arunoda de MeteorHacks, BulletProof Meteor te guiar a travs de lecciones con
preguntas tipo test, y enfocadas al rendimiento de Meteor.

El Podcast Meteor
Josh y Ry de la empresa dierential graban el Podcast Meteor todas las semanas, otra forma de
mantenerse al da con lo que pasa en la comunidad Meteor.

Otros recursos
Stephan Hochhaus ha compilado una lista bastante exhaustiva de recursos Meteor.
El blog de Manuel Schoebel y el de Gentlenode son una buena fuente de informacin sobre Meteor.

Pedir ayuda
Si encuentras algn obstculo, el mejor lugar para preguntar es Stack Overflow. Asegrate de
etiquetar la pregunta con la etiqueta meteor .

La comunidad
Por ltimo, la mejor forma de estar al da con Meteor es mantenerse activo en la comunidad. Nosotros
recomendamos inscribirse en la lista de correo de Meteor, seguir los grupos de Google Meteor Core y
Meteor Talk y crear una cuenta en el foro de Meteor Crater.io.

Vocabulario

14.5

Cliente
Cuando hablamos del Cliente, nos referimos al cdigo que se ejecuta en el navegador de los usuarios,
ya sea uno tradicional, como Firefox o Safari, o algo tan complejo como un UIWebView en una
aplicacin nativa para el iPhone.
Coleccin
Una coleccin es el almacn de datos que se sincroniza automticamente entre el cliente y el
servidor. Las colecciones tienen un nombre (como posts ), y por lo general existen tanto en el cliente
como en el servidor. Si bien se comportan de forma distinta, tienen una API comn basada en la API
de Mongo.
Computacin
Una computacin es un bloque de cdigo que se ejecuta cada vez que cambia una de las fuentes de
datos reactivos de las que depende. Si tienes una fuente reactiva (por ejemplo, una variable de
sesin) y quieres responder reactivamente a ella, tendrs que crear una computacin.
Cursor
Un cursor es el resultado de ejecutar una consulta en una coleccin Mongo. En el lado del cliente, un
cursor no es tan slo un conjunto de resultados, sino que es un objeto reactivo desde el que se puede
observar (con observe() ) los cambios (aadir, eliminar o actualizar) en la coleccin
correspondiente.
DDP
El DDP es el Protocolo de Datos Distribuidos que utiliza Meteor para sincronizar colecciones y efectuar
llamadas a mtodos. DDP pretende ser un protocolo genrico, que toma el relevo a HTTP para

aplicaciones en tiempo real con gran carga de datos.


Deps
Deps es el sistema reactivo de Meteor. Deps se utiliza entre bastidores para sincronizar
automticamente el HTML con el modelo de datos subyacente.
Documento
Mongo es un almacn de datos basado en documentos y a los objetos que salen de las colecciones se
les llama documentos. Son objetos JavaScript sin formato (aunque no pueden contener funciones)
con una nica propiedad especial, el _id, que Meteor utiliza para realizar un seguimiento de sus
propiedades en el DDP.
Ayudantes
Cuando una plantilla necesita mostrar cosas ms complejas que una simple propiedad de un
documento, sta puede hacer uso de su ayudante, una funcin que se utiliza para procesar los datos
que se muestran en ella.
Compensacin de la latencia
Es una tcnica que permite simular llamadas a mtodos en el cliente para evitar retrasos mientras se
espera la respuesta del servidor.
Meteor Development Group (MDG)
La empresa que desarrolla Meteor.
Mtodo
Un mtodo en Meteor es una llamada desde el cliente, a un procedimiento remoto en el servidor, con
un poco de lgica aadida que permite realizar un seguimiento de los cambios en los datos adems

de compensar la latencia de la llamada.


MiniMongo
La coleccin del lado del cliente es un almacn de datos en memoria que ofrece una API tipo Mongo.
La librera que se utiliza se llama MiniMongo, para indicar que es una versin ms pequea de
Mongo que se ejecuta por completo en la memoria del navegador.
Paquete
Un paquete Meteor puede ser: cdigo JavaScript que se ejecuta en el servidor, cdigo JavaScript que
se ejecuta en el cliente, instrucciones para procesar recursos (como SASS a CSS), o recursos que
deben ser procesados.
Un paquete es como una librera con superpoderes. Meteor incluye una gran cantidad de paquetes
( meteor list ). Tambin existe Atmosphere, que es una coleccin de paquetes de terceros
mantenida por la comunidad ( mrt add ... ).
Publicacin
Una publicacin es un conjunto de datos con nombre que se personaliza para cada usuario que se
suscribe a ella. Se configuran en el servidor.
Servidor
El servidor Meteor es un servidor HTTP y DDP ejecutados va Node.js. Se compone de todas las
libreras y del cdigo JavaScript del lado del servidor. Cuando se inicia el servidor, se conecta a una
base de datos Mongo (que configura por si mismo en el primer arranque).
Sesin
La sesin en Meteor es una fuente de datos reactiva que usa tu aplicacin para hacer un seguimiento
del estado del usuario.

Suscripcin
Una suscripcin es una conexin a una publicacin desde un cliente especfico. La suscripcin es el
cdigo que ejecuta el navegador y que utiliza para comunicarse con una publicacin del servidor y
que, adems, mantiene los datos sincronizados.
Plantilla
Una plantilla es una forma de generar cdigo HTML desde JavaScript. Por defecto, Meteor slo
soporta el sistema Spacebars, pero hay planes para incluir ms.
Contexto de datos de una plantilla
Cuando se muestra un plantilla, lo que se representa es un objeto JavaScript que proporciona datos
especficos para esta representacin en particular. Por lo general, este tipo de objetos son, de tipo
POJO (plain-old-JavaScript-objects), a menudo son documentos de una coleccin, aunque pueden
ser ms complejos e incluir funciones.

Changelog
April 15, 2015

99

1.9

Updated Introduction & Getting Started sections for Windows.


Changed rendered to onRendered and created to onCreated .
Explained package names in Getting Started chapter.
Added note about default Iron Router help page in Routing chapter.
Fixed audit-argument-checks link in Creating Posts chapter.
Fixed file paths in Comments chapter.
Wrong limit() changed to postsLimit() in Pagination chapter.
Changed UI.registerHelper to Template.registerHelper in Voting chapter.
Added a note about .animate CSS class in Animations chapter.
Fixed file paths in Animations chapter.

February 10, 2015

1.8

Rewrote animation chapter to use _uihooks .


Wrapped every page in a .page div.
Used the oicial twbs:bootstrap Bootstrap package.
Added .page CSS to style.css .
Used Template.registerHelper instead of UI.registerHelper .
Removed Deploying On Modulus section (now referencing their docs instead).
Updated db.users.find() result in Adding Users chapter.
Added a note about the Meteor shell in the Collections chapter.

December 5, 2014

1.7.2

Adding paragraph about subscriptions in Pagination chapter.


Various typo fixes.
Various code fixes.

November 10, 2014

1.7.1

Various fixes.
Fix code highlighting in Voting chapter.
Change router to route in Pagination chapter.
Removed mentions of Router.map() in Comments and Pagination chapters.
Linking to Boostrap site in Adding Users chapter.
Added BulletProof Meteor to Going Further chapter.

October 28, 2014

1.7

Updating the book for Iron Router 1.0.


Defining routes with new path-first syntax.
Use subscriptions instead of onBeforeAction in posts controller.
Use this.next() in onBeforeAction hook.
Rename XyzPostsListController to XyzPostsController .

October 24, 2014

1.6.1

Fixing a few typos.


Finishing switching Meteor.Collection to Mongo.Collection .

Updated introduction.
Added Get A Load Of This section in Routing chapter.

October 15, 2014

1.6

Updating the book for Meteor 1.0.


General Changes
collections
views

directory is now in lib .

directory is now named templates .

Removed $ from bash prompts.


Now using Bootstrap 3.
Being more consistent about using //... to denote skipped lines in code.
Getting Started
Explained the advantages of Meteor packages over manually adding files.
Explicitly adding underscore package.
Updated 5 Types of Packages section.
Not creating collections directory anymore.
Updated CSS code.
Templates
Changed partials to inclusions.
Not talking about managers anymore.
Collections
Cut down Collections chapter intro.

Changed Meteor.Collection() to Mongo.Collection() .


Added Storing Data section.
General edits and tweaks.
Routing
Added Post Not Found section.
General edits and improvements.
The Session
Added reminder to revert code changes at the end of the chapter.
Adding Users
Now using ian:accounts-ui-bootstrap-3 package.
Reactivity
Using Trackers instead of Deps .
Creating Posts
Removed message field from posts.
Added Security Check section.
Added Preventing Duplicates section.
Changed post to postInsert , updated postInsert method description.
Latency Compensation
Updated code examples.
Added more explanations.

Allow & Deny


Remove Using Deny as a Callback section.
Errors
Completely rewrote error handling code.
Added Seeking Validation section.
Creating a Meteor Package
Various edits and updates.
Comments
Rename comment template to commentItem .
Various edits.
Notifications
Added No Trespassers Allowed note.
Advanced Reactivity
Added section about reactive-var package.
Pagination
Got rid of iron-router-progress .
Voting
Various edits.
Advanced Publications

Various edits.
Animations
This chapter is out of date. Update coming sometimes after 1.0.

Note: the following extra chapters are only included in the Full and Premium editions:
RSS Feeds & APIs
Updated package syntax.
Minor tweaks.
Using External APIs
Minor edits.
Implementing Intercom
Added favorite_color custom attribute.
Various minor edits.
Migrations
Minor edits.

October 3, 2014

1.5.1

Fix quotes in comments chapter.


Clarified Session chapter.
Added link to the blog in chapter 8.
Adding a note about reversing changes at the end of Session sidebar.

Reworking section about the five types of packages.


Changing partial to inclusion in Templates chapter.
Added note about Animations chapter being out of date.

August 27, 2014

1.5

Updated Pagination chapter.


Fixed typos.
Removed mentions of Meteorite throughout the book.
Updated Creating A Package sidebar for Meteor 0.9.0.
Now including changelog in book repo.
Book version is now tracked in changelog, not in intro chapter.
Added section about manual.meteor.com in Going Further chapter.

May 30, 2014

1.3.4

Replaced Vocabulary chapter with Going Further chapter.


Added new Vocabulary sidebar.

May 20, 2014

1.3.3

Various typos and highlighting fixes.


Small correction in Errors chapter.

May 5, 2014

1.3.2

Various typos fixes.

April 8, 2014

1.3.1

Finished 0.8.0 Update.


12 Pagination: Use count() instead of fetch().length() .
14 Animations: Rewrote the chapter to use a helper instead of the rendered callback.

March 31, 2014

1.3

Updated to support Meteor 0.8.0 and Blaze.


5 Routing: Routing changes to support IR 0.7.0:
{{yield}}

becomes {{> yield}}

Explicitly add loading hook.


Use onBeforeAction rather than before .
6 Adding Users: Minor change for Blaze:
{{loginButtons}}

becomes {{> loginButtons}}

7 Creating Posts:
HTML changes for stricter parser.
Update our onBeforeAction hook to use pause() rather than this.stop()
13 Voting: Small change to the activeRouteClass helper.

January 13, 2014

1.2

The first update of 2014 is a big one! First of all, youll notice a beautiful, photo-based layout that
makes each chapter stand out more and introduces a bit of variety in the book.
And on the content side, weve updated parts of the book and even written two whole new chapters:
New Chapters
[NEW!] 3.5 Using GitHub: New sidebar on how to use GitHub.

[NEW!] 17.5 Migrations: New sidebar about database migrations.


Updates
2.5 Deploying: Rewrote chapter from scratch to be more up to date.
4.5 Publications and Subscriptions: Merged in content from Understanding Meteor
Publications & Subscriptions
15 RSS Feeds & APIs: Updated chapter to use Iron Router.
16 Using External APIs: Updated chapter to use Iron Router.
17 Implementing Intercom: Rewrote chapter to match the Intercom package.

December 1, 2013

1.1

Major Updates
5 Routing: Rewrote chapter from scratch to use Iron Router.
5.5 The Session: Added a section about Autorun.
10 Comments: Updated chapter to use IR.
12 Pagination: Rewrote chapter from scratch, now managing pagination with IR.
13 Voting: Updated the chapter to use IR, simplifed the template structure.
Minor Updates
Minor updates include API changes between the old Router and Iron Router, file paths updates, and
small rewordings.
6 Adding Users
7 Creating Posts
7.5 Latency Compensation
8 Editing Posts

9 Errors
11 Notifications
12 Animations
If youd like to confirm what exactly has changed, weve created a full di of our Markdown source
files [PDF].

October 4, 2013

1.02

Various typo fixes

September 4, 2013

1.01

Updated Creating a Meteorite Package chapter to Meteor 0.6.5


Updated package syntax in Intercom and API extra chapters.

May 5, 2013
First version.

1.0

También podría gustarte