Client side Meteor. Parte 2


Continúo con la parte 2 de esta mini serie: “Client side Meteor”. Resolveré en esta parte la última operación del CRUD que dejé pendiente en el artículo anterior: Editar contactos.

Para dejar a un lado el esquema clásico, me impondré una restricción para implementar este requisito:

  • No se pueden crear nuevos templates.

Un formulario, dos operaciones

La aplicación que estoy construyendo solo tiene un formulario, encerrado en el template NuevoContacto, por lo que tendré que arreglármelas para reutilizarlo en la operación editar.

Aunque esta aproximación necesita más lógica que solo crear un nuevo template y asignarle nuevos eventos para lograr realizar la operación, será más elegante pues hacemos uso del patrón DRY (Don’t Repeat Yourself) y reutilizaré código HTML que de otra forma estaría duplicado.

El botón que desencadena el caos

¿Recuerdas el bucle que itera por la lista de contactos? Añadiré un nuevo botón de clase edit que servirá para editar el contacto actual.

<!-- fichero lista_contactos.html -->

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

A este botón se le registra el evento click, pero dentengámonos a pensar un momento…

¿Qué se debe hacer en tal evento?

En pocas palabras, debemos “enviarle una señal” al formulario de insertar contacto que se cambie a “modo edición”.

El mensajero del caos: Session

En el artículo donde introduje el framework hablé sobre el objeto Session, objeto global y reactivo accesible desde cualquier lugar en el cliente. ¡Mandemos la señal!

// fichero lista_contactos.js

Template.ListaContactos.events({
    'click .del' : function (event, template) {
        ... // código de eliminar
    },
    'click .edit' : function () {
        Session.set('editando' , true );
        Session.set('contactoId' , this._id);
    }
});

Observa cómo creé dos variables nuevas en el objeto Session: editando y contactoId. Nuevamente, como el botón de editar entra dentro del contexto del bucle, dentro de la variable this está el Contacto actual.

Recibiendo la señal

Con la variable de sesión editando, modificaré el formulario para que se comporte como uno de edición. Tendré que hacer dos tareas fundamentales:

  1. Cambiar el nombre del botón submit a Editar.
  2. Establecer el valor de los inputs nombre y apellidos con los valores del Contacto que se va a editar.

Solución al punto 1

<!-- fichero nuevo_contacto.html -->

...
  <button type="submit">
    {{#if editando}}
      Editar
    {{else}}
      Crear
    {{/if}}
  </button>
</form>

El nombre se cambia en dependencia de lo que devuelva el helper editando. Este helper aún no existe, por lo que necesito crearlo. ¿Qué devuelve este helper? Veamos:

Template.NuevoContacto.helpers({
    editando : function () {
        return Session.get("editando") || false;
    }    
});

En efecto, devuelve el valor de la entrada editando, que toma valor verdadero cuando se presiona el botón editar en el template ListaContactos.

Solución al punto 2

Seguro estarás pensando que la solución a este punto es tan fácil como duplicar cada input y hacer una condicional, algo similar a esto:

<label for="nombre"> Nombre </label>

{{#if editando }}
  <input id="nombre" name="nombre" value="{{ contacto.nombre }}">
{{else}}
  <input id="nombre" name="nombre">
{{/if}}

Y lo mismo para el campo apellidos. En efecto, aquí estamos diciendo que si el formulario está en modo edición, los campos deben tener en su atributo value, el valor del contacto que se está editando.

En este caso, {{ contacto.nombre }} no es un objeto, sino un helper que en su función devuelve el objeto Contacto que se desea editar:

Template.NuevoContacto.helpers({
  editando : function () {
    return Session.get("editando") || false;
  },
  contacto : function () {
    return Session.get("contactoId") ? Contactos.findOne({_id : Session.get("contactoId")}) : false;
  }
});

El helper contacto dice:

“Si la variable sesión en su clave contactoId tiene un valor distinto de falso, entonces devuelve un contacto con el id de dicho valor, sino, devuelve falso”.

Por tanto, este helper devolverá un Contacto si el formulario está en modo edición y falso cuando no lo esté. Es por eso que accediendo desde la vista a {{ contacto.nombre }} se devolverá el nombre asociado al Contacto que se está editando.

Spacebars puede brillar más

La solución anterior, a pesar de ser válida, tiene el mismo problema de crear un template a parte para albergar un formulario completo de edicion: Hay código duplicado.

Observa que en dicho código duplicado, lo único diferente es el atributo value. Qué sucede: cuando estamos en modo edición, en el atributo value se asignará un valor distinto de falso, pero si por alguna casualidad, este atributo value toma valor false viniendo dicho valor de un helper, Spacebars elimina el atributo completo.

Esto es justamente lo que necesito para eliminar el código duplicado. Con solo dejar el input con el atributo value propenso a tomar el valor false cuando salga del modo de edición, Spacebars quitará dicho atributo y dejará el input con su aspecto original. Por tanto, el aspecto final del formulario de inserción/edición queda así:

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

        <input id="nombre"
               name="nombre"
               placeholder="su nombre"
               value="{{ contacto.nombre }}"/>
        <hr/>

        <label for="apellido">Apellidos</label>
        <input id="apellido"
               name="apellido"
               placeholder="sus apellidos"
               value="{{ contacto.apellidos }}"/>

        <hr/>

        <button type="submit">
            {{#if editando}}
              Editar
            {{else}}
              Crear
            {{/if}}
        </button>
    </form>
</template>

Ajustando el evento submit

Solo queda modificar el evento submit para cuando se quiera editar. El siguiente snippet logra este objetivo, observa además que cuando se inserta un nuevo Contacto, los campos se limpian.

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;
        }

        if (Session.get("editando")) {

            var cid = Session.get("contactoId");
            // forma de actualizar una colección en MongoDB
            Contactos.update({_id : cid}, {$set : {nombre : nombre, apellidos : apellidos}});

            /* 
             * Actualizar las variables reactivas para
             * salir del modo edición de forma automática 
             */
            Session.set("editando"   , false);
            Session.set("contactoId" , false);
        }
        else {

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

            event.target.nombre.value   = "";
            event.target.apellido.value = "";
        }
    }
});

Bastante claro, se utiliza el modificador $set en la consulta de MongoDB (en este caso su copia, MiniMongo) para actualizar los campos del documento que tenga el id del Contacto a actualizar.

Luego se resetean las variables de sesión a su estado original y Blaze se encarga de las dependencias de forma automática (ej: cambiar el nombre del botón Editar a Crear)

Conclusiones

El resultado, una pequeña pero extremadamente dinámica aplicación Web que permite realizar un CRUD del lado del cliente sobre la colección Contactos.

No quiero terminar sin antes mostrarte un pequeño detalle de organización que puedes aplicar, es un patrón que se utiliza en los bucles en la vista. Siempre que puedas toma el código dentro del bucle y mételo en un template a parte.

...
{{#each contactos}}
  {{> Contacto }}
{{/each}}
...

<!-- fichero client/contactos/un_contacto.html -->

<template name="Contacto">
    <p>
        Nombre completo: {{ nombre }} {{apellidos}}
        <button class="edit"> e </button>
        <button class="del">  x </button>
    </p>
</template>

No tendrás que cambiar nada porque el contexto de datos se pasa hacia el template Contacto de forma automática. Lo que sí puedes hacer (aunque no es obligado) es trasladar los eventos sobre los botones .del y .edit hacia el template Contacto.

// fichero client/contactos/un_contacto.js

Template.Contacto.events({
    'click .del' : function (event, template) {
        ... // el mismo código        
    },
    'click .edit' : function () {
        ... // el mismo código
    }
});

De esta forma cumples con el patrón de asignación de responsabilidades y el código es mucho más legible, flexible y reutilizable.

Espero te sirva de mucho y, hasta la próxima entrega, ¡Stay tunned!