Client side Meteor. Parte 1


En el artículo anterior introduje el framework MeteorJS para los principiantes absolutos. Para no sobre cargarlos con contenido, meramente expliqué el funcionamiento del framework y las características que lo hacen único en su clase.

De aquella entrada debiste entender los siguientes puntos:

  1. MeteorJS es un framework full stack, o sea, con él creas aplicaciones completas, (cliente-servidor y todo lo que existe en el medio).
  2. Se utiliza JavaScript como único lenguaje.
  3. Aunque tiene soporte para más sistemas gestores de bases de datos, se utiliza MongoDB por defecto, sin necesidad de configurar nada a mano. Cada app tiene su propia base de datos.
  4. Se utilizan Colecciones para persistir los datos.
  5. Se utiliza Blaze como sistema reactivo para actualizar dinámicamente la vista, la cual a su vez utiliza Spacebars como motor de plantillas.
  6. Meteor adopta una arquitectura isomórfica, por lo que puedes utilizar un mismo snippet de código en el cliente y en el servidor.

Estructurando el aprendizaje

En esta entrada me enfocaré en el punto 6. Haré un demo que haga uso extensivo del cliente y no nos comunicaremos con el servidor. Lo hago intencionalmente pues, como principiante que fui hace un tiempo atrás, uno de los principales problemas que tuve a la hora de aprender el framework fue la liga de los conceptos a aplicar para trabajar con el servidor y el cliente a la vez.

Si alguna vez has utilizado AngularJS, debes conocer que solo se enfoca en el cliente. Luego que aprendes a trabajar lo básico con dicho framework, la próxima interrogante que te viene a la mente es ¿Cómo comunicarse con un servidor? Y es ahí cuando indagas en los servicios $http, $resource, Restangular etc.

Con esta misma filosofía quiero guiarte en el aprendizaje de MeteorJS. Lo bueno de este acercamiento específicamente con Meteor, es que luego de aprender todo lo necesario en el cliente, la transición hacia el código del servidor en parte es casi la misma, por la naturaleza isomórfica del framework. Es decir, muchos snippets de códigos que ahora aprenderás, los podrás utilizar en el servidor también, exactamente como los aprendiste.

La aplicación

Con una lista de contactos, donde se permitan agregar nuevos y eliminarlos, creo que bastará para explicarte los conceptos fundamentales. Comencemos con una estructura básica:

$ meteor create contactos
$ cd contactos

Borra todos los ficheros que se crean (exepto la carpeta .meteor) y crea una estructura similar a la brindada en el artículo anterior.

+ .meteor
+ client
+ public

No vamos a trabajar con el servidor, por eso no creamos la carpeta server. Tampoco vamos a crear nada fuera de client y public, por lo que no abrá código compartido tampoco.

El primer fichero

Puede ser cualquiera, lo que desees crear, puedes comenzar implementando las interfaces con HTML, asignar eventos en ficheros JavaScript, pero no, lo primero que quiero que hagas es decirle a Meteor que no se intente conectar al servidor:

$ nano client/un_fichero_cualquiera.js

En el fichero escribe la siguiente función:

Meteor.startup(function(){
  Meteor.disconnect();  
});

El código anterior se ejecuta tan pronto se haya inicializado la aplicación. Es el equivalente al evento onload del navegador, o el ready() de jQuery:

$(document).ready(function(){
  ...
});

Meteor.startup se utiliza para inicializar cosas que necesita tu aplicación, por ejemplo, al agregar internacionalización a tus aplicaciones, le debes decir en este método que se traduzca la app a un idioma por defecto:

Meteor.startup(function(){
  Libreria.setDefaultLanguaje('en'); // método ficticio
});

En este caso, lo único que necesitamos es que cuando nuestra app este lista para ser mostrada, no se intente crear una conexión hacia el servidor, pues no vamos a guardar datos en la base de datos (por ahora).

Renombra el fichero que creé con una notación más descriptiva, ya que el nombre anterior fue para mostrarte que tu código lo organizas como desees:

$ mv client/un_fichero_cualquiera.js client/inicio.js

El segundo fichero

Igual, puede tener cualquier nombre, pero prefiero ir a lo clásico y llamarle index.html. Aquí especificaré el head de la aplicación:

$ nano client/index.html

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

  <title> Registro de Contactos </title>
</head>

¿Necesito especificar el DOCTYPE?

No, Meteor lo inserta automáticamente por ti. Tampoco tienes que envolver el markup en la etiqueta html. Y esto es todo lo que hay que decir en esta sección.

¿Necesito la etiqueta body?

No, Meteor la inserta automáticamente como un Template, pero en este caso, no tenemos un sistema de enrutamiento aún, por lo que es conveniente crearla. En el propio index.html escribe:

...
</head>

<body>

  {{> ListaContactos }}

</body>

Los templates

La sintaxis {{> NombreDePlantilla }} es el mecanismo de Spacebars para incrustar código HTML. De esta forma podemos organizar las interfaces de la aplicación de una manera comprensiva.

El template ListaContactos lo puedes implementar en el mismo fichero index.html. Pero no sabes qué tan grande puede llegar a ser, por lo que te aconsejo sigas el principio de: “Un template, un fichero HTML”.

Incluso vamos a crear una carpeta para permitir una mayor organización cuando la aplicación crezca, realizando las operaciones pertinentes deberías tener ahora una estructura así:

+ .meteor
+ client
  - inicio.js
  - index.html
  + contactos
    - lista_contactos.html       
+ public

En el fichero lista_contactos.html define la plantilla de esta forma:

<template name="ListaContactos">
  <p> Hola, existen 0 contactos en la aplicación </p>
</template>

Observa cómo en Meteor no necesitas decirle al fichero index.html que incluya el fichero lista_contactos.html. Todo se basa en la definición de los templates.

Meteor lo único que hace aquí cuando construye la aplicación es buscar en todos los ficheros HTML los templates requeridos e inserta su contenido en los lugares definidos, en este caso, el contenido de lista_contactos.html se incrustará en el body:

Registro de contactos

Los helpers

Hagamos dinámico el mensaje anterior. Cambiaré el valor 0 por la cantidad de contactos que existen en la aplicación (o que existirán).

<template name="ListaContactos">
  <p> Hola, existen {{ cantidad }} contactos en la aplicación </p>
</template>

Esto te debe resultar familiar. El helper cantidad está asociado a una función que debemos implementar en un fichero JavaScript. Por convención, utiliza el mismo nombre para indicar que ambos ficheros, el HTML y el JS están relacionados entre sí (aunque no es obligatorio):

$ nano client/contactos/lista_contactos.js

Template.ListaContactos.helpers({
  cantidad : function(){
    return Contactos.find().count();
  }
});

MiniMongo: MongoDB en el lado del Cliente

Estamos retornando una variable que no existe, pero espera ¿por qué se utiliza en dicha variable inexistente métodos inerentes a consultas en MongoDB? ¿No debería utilizarse esto solo en el lado del servidor para comunicarnos con la base de datos?

Sí, y no.

Resulta ser que Meteor cuenta con una pequeña biblioteca del lado del cliente llamada MiniMongo, que implementa una API similar al verdadero MongoDB. Esto se diseñó así para unificar el estilo de codificación en ambos entornos (el cliente y el servidor) y poder así crear código isomórfico.

No obstante, MiniMongo también se encarga de la sincronización de los datos que están en el cliente y en el servidor.

En el caso actual, la variable Contactos es una colección que sirve para comunicarse con la base de datos, pero, como estamos completamente desconectados del servidor, los datos se almacenarán solo en la caché de MiniMongo, o sea, en el cliente.

De ahí que debes interiorizar que todas las operaciones que realizaré, se harán sobre lo que esté guardado en la caché de MiniMongo (el cliente, el navegador) y si al guardar varios contactos y presionar F5, dichos datos se perderán.

En Meteor se pueden crear aplicaciones offline y que los datos persistan aunque se actualice la página, pero eso es contenido para otra entrada.

La colección Contactos

Puedes crearla donde quieras, pero lo recomendable es crear un fichero que albergue la lógica de creación de colecciones, algo como esto parece sensato hacerlo:

$ nano client/colecciones.js

Contactos = new Meteor.Collection('contacto');

En efecto, creé una colección del lado del cliente. Aquí debes anotar dos cosas:

  1. La variable se crea global, o sea, no se utiliza la palabra reservada var
  2. El nombre de la colección se definió en singular.

Con el punto 1 se garantiza que se pueda acceder al objeto desde los helpers de las plantillas y sus eventos, así como desde cualquier otra parte, por ejemplo, desde el bloque Meteor.startup(function(){ ... });.

El punto 2 no es rígido, puedes llamar a la colección como desees, al final del camino, trabajarás solo con el objeto Contactos.

Insertando Contactos

El valor del helper cantidad se mantendrá en 0 mientras no existan contactos. Incrementemos este número. Crearé un formulario con dos campos: nombre y apellido, donde al dar click en el botón Crear se añadirá un nuevo contacto a la colección.

Ahora bien ¿Donde creo dicho formulario? Puede ser en el propio fichero lista_contactos.html, pero como el nombre lo indica, en dicho fichero solo debería estar la lógica para mostrar el listado de contactos. ¿Qué debemos hacer?

Creamos un nuevo fichero, un nuevo template y lo incluímos en lista_contactos.html.

$ nano client/contactos/nuevo_contacto.html

<template name="NuevoContacto">
    <form action="">
        <label for="nombre">Nombre</label>
        <input id="nombre" name="nombre" />

        <hr/>

        <label for="apellido">Apellidos</label>
        <input id="apellido" name="apellido" />

        <hr/>

        <button type="submit"> Crear </button>
    </form>
</template>

Nada nuevo aquí, solo el viejo y plano HTML de siempre. Veamos el fichero JavaScript asociado:

$ nano client/contactos/nuevo_contacto.js

Template.NuevoContacto.events({
    'submit form' : function (event, template) {
        event.preventDefault();

        var nombre    = event.target.nombre.value;
        var apellidos = event.target.apellido.value;


        // el nombre es requerido
        if (nombre == "") {
            alert('El nombre no puede ser vacío');
            return false;
        }

        Contactos.insert({
            nombre    : nombre,
            apellidos : apellidos
        });
    }
});

Aquí se realizan tres tareas principales:

  1. Se obtienen los datos del formulario a través del objeto event.
  2. Se valida el nombre, que no puede estar vacío.
  3. Se inserta el contacto en la colección.

Observa que el evento lo intercepto con submit form en lugar de utilizar el selector click button. Con la primera forma garantizo que se realice la operación no solo cuando se de clic en el botón, sino también cuando se presione la tecla Enter en alguno de los campos.

Para poder ver estos cambios desde el navegador, incrusta la plantilla NuevoContacto dentro de la plantilla ListaContactos:

<template name="ListaContactos">
  {{> NuevoContacto }}

  <p> Hola, existen {{cantidad}} contactos en la aplicación </p>
</template>

¿Qué le sucedió al helper cantidad?

Se incrementó en 1 de forma automática. Esto es gracias a Blaze, que mantiene un registro de las operaciones que se realizan y actualiza la vista en los lugares que sea necesario.

En este caso se reevaluó la función cantidad porque una fuente de datos reactiva (en este caso, la colección Contactos) cambió en otro lugar.

Mostrando los contactos

Para visualizar los contactos conforme se van añadiendo, debemos iterar por la lista de contactos existentes. Pero qué sucede, el template ListaContactos no sabe cómo encontrar tales contactos.

Para lograrlo, hay que crear un helper que devuelva los contactos que la plantilla necesita:

Template.ListaContactos.helpers({
    cantidad : function () {
        return Contactos.find().count();
    },
    contactos : function () {
        return Contactos.find();
    }
});

Con este helper creado, podemos iterar sobre la colección Contactos desde la vista y mostrar, para cada uno, sus propiedades:

<template name="ListaContactos">
    {{> NuevoContacto }}

    <p> Hola, existen {{ cantidad }} contactos en la aplicación </p>

    {{#each contactos}}
      <p>
          Nombre completo: {{ nombre }} {{apellidos}}
      </p>
    {{/each}}
</template>

Al insertar par de registros en la colección, la aplicación luce similar a la siguiente imagen:

Registro de contactos mostrar

Eliminando contactos

Voy a poner un botón con una cruz al lado de cada contacto. Al dar clic en él se ejecutará un helper que se encargará de eliminarlo.

{{#each contactos}}
  <p>
    Nombre completo: {{ nombre }} {{apellidos}} 
    <button class="del"> x </button>
  </p>
{{/each}}

Le puse la clase del al botón para crear un evento que reaccione al clic en dicho botón:

Template.ListaContactos.events({
   'click .del' : function (event, template) {
       var idContacto = this._id;
       Contactos.remove({ _id : idContacto });
   }
});

El contexto de datos

¿Cómo obtuve el id del contacto? Esto es un concepto que debes conocer. Cuando se está iterando por la lista de contactos, internamente Spacebars le asigna un contexto de datos a los helpers y los eventos que se definan dentro.

En este caso, a la función que define el evento de eliminar, se le asigna el objeto por el cual se está iterando, accesible mediante this. Por ende, el id del contacto se puede obtener en el evento clic por this._id, el nombre por this.nombre y así sucesivamente para todas sus propiedades.

Conclusiones

El presente artículo reforzó algunos conceptos que debes conocer para no atascarte en estas cosas básicas cuando necesites aprender temas más avanzados.

Para reforzar lo aprendido recomiendo bajes el código fuente desde Github, examínalo con detalle, modifícalo y trata de agregar otro botón que te permita editar un Contacto. Si tienes problemas en algún paso no dudes pedir ayuda en los comentarios.

Próximamente estaré ampliando esta app. Antes de conectarme al servidor permitiré que la app sea totalmente funcional offline, o sea, que los datos persistan al actualizar la página, la internacionalizaré y la integraré con el framework Ionic para que se vea exelente en dispositivos móbiles.

Stay tunned!