Categorías: Labs

Construyendo una Lista de Tareas para Firefox OS, parte 2

Este artículo fue originalmente publicado en el blog de Mozilla Hacks. Traducción por Ángel Fernando Quiroz.

En la Parte 1, desarrollamos la interfaz gráfica de la aplicación de Lista de Tareas para Firefox OS. En esta parte, haremos que funcione escribiendo código JavaScript usando Backbone.

El código fuente para la aplicación está disponible en GitHub.

Nota: recomendamos revisar la Parte 1 antes de seguir porque agregamos un botón Editar para cada elemento de la lista en #view-todos y el módulo CSS de botón ha sido cambiado un poco para hacerlo más reusable.

Librerías requeridas

El primer paso es agregar las librerías JS usando volo. Ejecuta todo los siguientes comandos volo mientras permaneces en el directorio raíz (/fos-todo-app) del proyecto.

Agregar Requirejs/Text

Text es un plugin para cargar los recursos de texto como archivos de plantillas, CSS, etc.

volo add requirejs/text

Agregar Backbone.js

El siguiente comando agregará Backbone incluyendo sus dependencias, por ejemplo Underscore y jQuery.

volo add backbone

Agregar Zepto.js

Como mencionamos en la parte 1, usaremos Zepto en vez de jQuery, así que necesitamos agregar Zepto y reemplazando a jQuery.

volo add zepto

Ahora, abre el archivo /fos-todo-app/package.json y elimina la siguiente línea:

"jquery": "github:jquery/jquery/2.0.2",

Esto eliminará la dependencia de jQuery de nuestro proyecto volo. Ahora quitamos el archivo jquery.js del directorio fos-todo-app/www/js/lib. Finalmente necesitamos decirle a Require.js que cargue Zepto cuando solicite jQuery. Esto lo podemos hacer agregando una propiedad map en la configuración de Require.js en /fos-todo-app/www/js/app.js.

requirejs.config({
   baseUrl: 'js/lib',
 
   // Map jquery to zepto
   map: { '*': { 'jquery': 'zepto' } },
 
   paths: {
       app: '../app'
   }
});

El adaptador Backbone IndexedDB

Vamos a utilizar el adaptador IndexedDB de superfeedr para este tutorial.

volo add superfeedr/indexeddb-backbonejs-adapter/master/backbone-indexeddb.js

Agregar rutas

De nuevo, abre el archivo /fos-todo-app/www/js/app.js para agregar las siguientes rutas a la configuración de Require.js.

paths: {
   app: '../app',
   models: '../app/models',
   collections: '../app/collections',
   views: '../app/views',
   templates: '../app/templates',
   db: '../app/db',
}

Ahora, crea todos los directorios mencionados (models, collections, views, templates, db) en las rutas anteriores en el directorio www/js/app. También elimina el archivo www/js/app/main.js porque no lo utilizaremos.

Nota: Para aplicaciones móviles, las rutas no son tan importantes como lo son para las aplicaciones Web. Asi que no usaremos las rutas de Backbone en esta aplicación.

Configuración de IndexedDB

Crea un archivo TodosDB.js en www/js/app/db que retorne un objeto que contenga las configuraciones relacionadas con IndexedDB.

define(['backbone', 'backbone-indexeddb'], function (Backbone, idxdb) {
   Backbone.sync = idxdb.sync;
 
   return {
       id: 'todos-db',
       migrations: [{
           version: 1,
           migrate: function (transaction, next) {
               var store = transaction.db.createObjectStore('todos');
               next();
           }
       }]
   };
});

Ten en cuenta que el método Backbone.sync es reemplazado con el método sync retornado por el adaptador IndexedDB. Ahora, en vez de sincronizar con el servidor, sincronizará con IndexedDB localmente.

El id en el objeto devuelto es el id único para la base de datos. La propiedad migrations contiene los métodos version y migrate. Una vez que se ejecuta migrate, la versión de la base de datos cambia al valor especificado (en nuestro caso es 1). El método migrate es llamado por el driver para realizar la migración. Para más detalles, lee la sección sobre implementación.

Modelo de tareas

Ahora, crea el archivo www/js/app/models/Todo.js y agrégale el siguiente código.

define(['backbone', 'db/TodosDB'],
function (Backbone, TodosDB) {
   var Todo = Backbone.Model.extend({
       database: TodosDB,
       storeName: 'todos',
 
       defaults: {
           task: 'my task',
           completed: false,
           created_on: new Date()
       },
 
       validate: function (attrs) {
           if (!attrs.task) {
               return 'La tarea no puede quedar vacía';
           }
       }
   });
 
   return Todo;
});

La única cosa a tener en cuenta en el Model anterior, son las propiedades database y storeName. Simplemente necesitas asignar la configuración de la base de datos devuelta desde TodosDB a la propiedad database y storeName es el nombre del almacén usado para los objetos de este Model. Recuerda que no es lo mismo que el id único definido en la configuración de la base de datos.

El resto son cosas normales de Backbone. Si sabes Backbone, puedes entender fácilmente lo que está pasando.

Colección de tareas

Ahora, agrega la siguiente colección en www/app/js/collections/todos.js.

define(['backbone', 'models/Todo', 'db/TodosDB'],
function (Backbone, Todo, TodosDB) {
   var Todos = Backbone.Collection.extend({
       database: TodosDB,
       storeName: 'todos',
       model: Todo,
       comparator: function (m) {
           return m.get('created_on').getTime();
       }
   });
 
   var todos = new Todos();
 
   //recuperar todos los registros desde IndexedDB
   todos.fetch();
 
   return todos;
});

Cada vez que un módulo devuelve una instancia, escribimos el nombre de ese archivo empezando con letras minúsculas, por ejemplo: todos.js, para la colección anterior.

Asegúrate de tener el mismo storeName en esta colección y en el modelo creado previamente. Además de eso, queremos que nuestra colección se mantenga almacenada de acuerdo a la fecha de creación, así que se agregó el método comparator que devuelve el tiempo en milisegundos desde el 1 de enero de 1970 usando el método getTime.

Después de crear la colección, una nueva instancia es creada y todos los registros son recuperados usando todos.fetch() y todos es devuelto.

Plantilla para la tarea

Ahora crea el archivo www/js/app/templates/todo.htm para la plantilla y agrégale el siguiente código.

<label>
  <input type="checkbox" <% if (completed) { %/>checked="checked"< % } %>>
  <span>< %= task%></span>
</label>
 
<input type="text" aria-hidden="true"/>
<button class="btn edit"><span>Edit</span></button>

Este es el mismo código HTML que escribimos para una tarea, con una diferencia: el código agregado para comprobar la propiedad completed del modelo y mostrar la propiedad task en el elemento span.

Mostrando la Lista de Tareas

Ahora, en vez de mostrarte cada módulo, te mostraremos cómo es implementada cada característica, así que puede que tengas que intercambiar entre diferentes módulos/archivos. Lo primero que necesitamos hacer es mostrar la lista de tareas usando la colección www/js/app/collections/todos.js (que es recuperada desde IndexedDB).

Vista del modelo de las tareas

Para mostrar la lista de tareas, primero, necesitamos crear una vista para el modelo www/js/app/models/Todo.js. Esta vista representará cada elemento tarea en la lista como un elemento li. Crea un nuevo archivo www/js/app/views/Todo.js.

define(['underscore', 'backbone', 'text!templates/todo.htm'],
function (_, Backbone, todoTempl) {
   var TodoView = Backbone.View.extend({
       tagName: 'li',
 
       template: _.template(todoTempl),
 
       //events: {},
 
       initialize: function () {
           this.$el.attr('role', 'listitem');
       },
 
       render: function () {
           var self = this;
 
           //--- render template ---//
           self.$el.html(self.template( self.model.toJSON() ));
 
           //--- cache DOM elements ---//
           self.$taskEdit = self.$('input[type=text]');
             self.$taskView = self.$('label');
           self.$btnEdit = self.$('.btn.edit');
 
           return self;
       }
   });
 
   return TodoView;
});

La vista anterior creará un elemento li conteniendo el HTML requerido desde la plantilla todo.htm, cuando es renderizada. Ten en cuenta que en el método initialize, al elemento li se le aplica un role de listitem. También algunos elementos del DOM son almacenados en caché después de la renderización de la plantilla en el método render, porque los necesitaremos repetidas veces.

Vista de colección de tareas

Ahora, crearemos la vista para la colección de tareas que representará ul#todo-list en section#view-todos. Esta vista creará una nueva instancia del TodoView definido anteriormente y la añadirá a sí mismo. Agrega el siguiente código en www/js/app/views/Todos.js.

define(['backbone', 'views/Todo', 'collections/todos'],
function (Backbone, TodoView, todos) {
   var TodosView = Backbone.View.extend({
       tagName: 'ul',
       id: 'todo-list',
       className: 'todo-list reset-list',
 
       initialize: function () {
           this.collection = todos;
 
           this.listenTo(this.collection, 'add', this.addTodo);
           this.listenTo(this.collection, 'reset', this.render);
           this.listenTo(this.collection, 'sort', this.render);
       },
 
       addTodo: function (todo) {
           var todoView = new TodoView({model: todo}).render();
           this.$el.append(todoView.el);
       },
 
       render: function () {
           this.$el.html('');
           this.collection.each(this.addTodo, this);
           return this;
       }
   });
 
   return TodosView;
});

En el método initialize, la colección de tareas es asignada a this.collection. Recuerda que esta colección de tareas es un instancia devuelta desde el módulo app/collections/todos.js. Ahora, cada vez que requerimos este módulo (en otros módulos), siempre retornará la misma instancia de la colección. Básicamente esto actúa como un Singleton.

El método addTodo es usado para crear una nueva instancia de TodoView (views/Todo.js) haciéndola pasar al modelo todo que se pasa como parámetro a el addTodo. Luego es renderizado usando el método render en la cadena y finalmente es agregado al elemento ul.

El método render hace llamadas repetidas a addTodo por cada elemento en this.collection.

Vista ViewTodos

Ahora, necesitamos crear una vista que representará la región section#view-todos. Esta es la vista donde TodosView (app/views/Todos.js) será agregado y contendrá los botones Agregar y Eliminar en el footer. Crea el archivo app/views/ViewTodos.js para esta vista.

define(['backbone', 'views/Todos', 'collections/todos'],
function (Backbone, TodosView, todos) {
   var ViewTodos = Backbone.View.extend({
       el: '#view-todos',
 
       //events: {},
 
       setBtnDelDisabled: function () {
           if (todos.length) {
               this.$btnDel.removeAttr('disabled');
           } else {
               this.$btnDel.attr('disabled', 'disabled');
           }
       },
 
       initialize: function () {
           //init todo list
           this.todosView = new TodosView().render();
 
           this.listenTo(todos, 'add', this.setBtnDelDisabled);
       },
 
       render: function () {
           this.$('.view-content').append(this.todosView.el);
 
           this.$btnDel = this.$('footer .btn.del');
 
           this.setBtnDelDisabled();
       }
   });
 
   return ViewTodos;
});

La vista anterior no creará un nuevo elemento porque tagName no es usado, en su lugar, se utiliza la propiedad el para seleccionar el elemento #view-todos existente en el DOM.

El método setBtnDelDisabled es muy sencillo. Deshabilita this.$btnDel si no hay elementos en la colección. De otra forma habilita el botón Eliminar, removiendo el atributo disabled. this.$btnDel contiene el elemento footer .btn.del (agregado a la caché en el método render).

En el método initialize, una nueva instancia de TodosView (la vista de la lista de tareas) es creada y asignada a this.todosView. Luego, esta vista (ViewTodos) inicia escuchando el evento add en la colección todos. Cuando un nuevo elemento es agregado en la colección todos, setBtnDelDisabled será llamado.

En el método render, la colección todosView es agregada a la vista actual (#view-todos). Luego, .btn.del es almacenado en caché en $btnDel. Finalmente setBtnDelDisabled es llamado para habilitar o deshabilitar a $btnDel, de acuerdo al número de elementos en la colección todos.

Bootstrap

Ahora, necesitamos crear una nueva instancia de ViewTodos y renderizarlo en el archivo app.js. Reemplazando la sentencia requirejs([‘app/main’]); con el siguiente código.

requirejs(['views/ViewTodos', 'collections/todos', 'zepto'], function (ViewTodos, todos, $) {
   new ViewTodos().render();
 
   //--- solo para pruebas ---//
   window.todos = todos; //colección todos
   window.$ = $;         //zepto
});

La colección todos es global, así que puedes probar fácilmente si tu aplicación está realizando las operaciones satisfactoriamente (Leer, Agregar, Editar, Eliminar) en esta colección.

Probar

Ahora, antes de agregar cualquier nueva característica en la aplicación, necesitamos estar seguros que nuestro código se ejecuta sin errores en la consola. Si estás probándola en Firefox, asegúrate de ejecutarla usando algún servidor web (http://localhost/fos-todo-app) porque IndexedDB te mostrará un error si intentas usar file:///.

Nota: Hemos notado que Firefox lanza el error InvalidStateError de IndexedDB si la opción Historial no está establecida a “Firefox podrá: Recordar el historial” en la configuración de Privacidad. Así que asegúrate de establecer “Recordar el historial” antes de probarlo. Esto no sucede en Chrome. También IndexedDB da el mismo error en una ventana en modo Privado.

En la consola, tenemos el siguiente resultado cuando se ejecuta la prueba por primera vez.

opening database todos-db in version #1
onupgradeneeded = 0 => 1
migrate begin version from #0
migrate begin before version #1
migrate done before version #1
migrate begin migrate version #1
migrate done migrate version #1
migrate begin after version #1
migrate done after version #1
Migrated to 1
migrate setting transaction.oncomplete to finish  version #1
migrate done transaction.oncomplete version #1
Done migrating
execute : read on todos for undefined

Obtendrás el resultado anterior solamente si el número de versión es diferente del definido en app/db/TodosDB.js, de otra manera obtendrás un mensaje de lectura en la consola.

Agregar tarea

Vamos a implementar el “Agregar tarea“ antes de las características “Editar“, “Eliminar“ o “Marcar como terminada“, porque con “Agregar tarea” podemos probar tanto la agregación como la obtención de la lista de tareas desde la propiedad IndexedDB.

Vista ViewAdd

Antes de implementar esta vista, vamos a revisar la funcionalidad que queremos aplicar en ella.
Vista de agregar

Queremos que esta vista se abra usando una animación CSS3. Queremos que esta vista agregue una nueva tarea en la colección todos cuando hagamos clic en el botón Hecho en el pie de página. Pero lo más importante es mantener el botón deshabilitado hasta que el usuario ingrese algo de texto en el campo de texto. Cuando el elemento tarea es agregado satisfactoriamente en la colección todos y guardado en IndexedDB, nosotros queremos cerrar esta vista usando una animación CSS3, y en el final de la animación, queremos ocultarla usando el atributo aria-hidden. De igual forma, el botón cancelar (equis) en el pie de página realizará la misma funcionalidad pero sin guardar el elemento tarea.

Ahora, crea un nuevo archivo para la vista app/views/ViewAdd.js que se encargará de la región section#view-add.

define(['backbone', 'views/Todos', 'collections/todos'],
function (Backbone, TodosView, todos) {
   var ViewAdd = Backbone.View.extend({
       el: '#view-add',
 
       //--- Reemplaza click con tap antes de la construcción final ---//
       events: {
           //Nosotros asignamos un id a .btn.done así que por qué no seleccionarlo usándolo
           'click #btn-add-done' : 'addTodo',
           //'tap #btn-add-done' : 'addTodo',
 
           'click .btn.del'      : 'cancel',
           //'tap .btn.del' : 'cancel',
 
           'keyup #task'         : 'setAddBtnDisabled',
           'keypress #task'      : 'addTodo'
       },
 
       setAddBtnDisabled: function () {
           var taskLen = this.$task.val().length;
 
           taskLen ? this.$btnAdd.removeAttr('disabled') : this.$btnAdd.attr('disabled', 'disabled');
       },
 
       addTodo: function (e) {
           var task = this.$task.val();
 
           console.log('ViewAdd:addTodo');
 
           //Si se hace clic en el botón Hecho o se presiona la tecla Enter y
           //se ha ingresado un nombre para la tarea
           if ((e.type === 'click' || e.keyCode === 13) &amp;&amp; task.length) {
           todos.create({task: task, created_on: new Date()});
               this.cancel();
               return false;
           }
 
       },
 
       cancel: function () {
           this.$el.removeClass('slide-up-in').addClass('slide-down-out');
       },
 
       hideView: function (e) {
           var $target = $(e.target);
 
           if (e.animationName === 'slide-down-out') {
               $target.attr('aria-hidden', 'true');
           }
       },
 
       initialize: function () {
           this.$task = this.$('#task');
           this.$btnAdd = this.$('#btn-add-done');
           this.$el.on('animationend', this.hideView);
       },
 
       render: function () {
           this.$el.removeAttr('aria-hidden').removeClass('slide-down-out').addClass('slide-up-in');
           this.$task.val('');
           this.$btnAdd.attr('disabled', 'disabled');
           return this;
       }
   });
 
   return ViewAdd;
});

En el método initialize, el campo de texto #task y el #btn-add-done son almacenados en caché. En la tercera línea, un evento animationend es adjuntado al elemento DOM de la vista actual (#view-add). Cada vez que la animación en ViewAdd termine, ésta invocará al método hideView.

El método hideView oculta la vista usando aria-hidden solamente si la animación slide-down-out termina, porque no queremos ocultar la vista iniciando la animación (#slide-up-in).
En el método render, la vista se hace visible mediante la eliminación de las clases aria-hidden y slide-down-out, y la clase slide-up-in es agregada para mostrar la vista mediante la animación.

El método cancel es un manejador de eventos que se activa cuando el usuario pulsa o hace clic en .btn.del. Este método cierra las vistas usando clases de animación.

El manejador setAddBtnDisabled se activa en el evento keyup. Este método simplemente habilita o deshabilita $btnAdd de acuerdo a la longitud de cadena en #task. El evento keyup es usado porque necesitamos la longitud de cadena después que la tecla es liberada por el usuario.

El manejador addTodo es lanzado en dos eventos ('click #btn-add-done' y 'keypress #task'). En este método, la nueva tarea es almacenada solamente si el usuario ha hecho clic en #btn-add-done o la tecla Enter es presionada y el usuario debe haber ingresado algo de texto en #task. En tal caso, la nueva tarea es agregada en la colección todos y el método cancel es llamado para ocultar #view-add.

Renderizando ViewAdd

Como se mencionó antes, nosotros no usaremos rutas en nuestra aplicación. Así que necesitamos lanzar/renderizar ViewAdd desde ViewTodos cuando el usuario haga clic en el botón Agregar en el pie de página de ViewTodos. Para que cargue ViewAdd, debe ser añadido en el array pasado como primer parámetro a define en ViewTodos.

define(['backbone', 'views/Todos', 'views/ViewAdd', 'collections/todos'],
function (Backbone, TodosView, ViewAdd, todos) {
   //body...
});

Luego, descomentar la propiedad events en ViewTodos y agregar el manejador para el evento click en él.

events: {
   //'tap footer .btn.add' : 'showAddView'
   'click footer .btn.add' : 'showAddView'
},

Ahora agrega el manejador showAddView que renderizará a ViewAdd.

showAddView: function () {
   console.log('showAddView');
 
   this.viewAdd = new ViewAdd().render();
 
   //Redefine showAddView
   this.showAddView = function () {
       this.viewAdd.render();
   };
 
   //Remove existing events and reattach then
   //using this.events hash
   this.delegateEvents();
},

La vista anterior crea una nueva instancia de ViewAdd, llama al método render y asigna la instancia de ViewAdd a this.viewAdd. Luego se redefine a sí mismo (showAddView) en el que sólo el método render es llamado sin crear una nueva instancia de él. En el final del método modificado necesitas volver a enlazar, y esto se hace usando el método delegateEvents de Backbone.

Pruebas

Ahora, intenta agregar algunos elementos tarea usando ViewAdd y prueba la conexión todos en la consola usando console.log(todos.toJSON()).
JSON de tareas

Nota: He notado un bug en Firefox 21 para Ubuntu. El footer fijo en una región/vista fija no anima apropiadamente cuando ViewAdd (#view-add) es renderizado. También algunas veces no se muestra el footer en ViewAdd . El mismo código trabaja absolutamente bien en Firefox OS Simulator.

Editar tarea

Ahora, vamos a hacer funcionar la edición de tareas, pero primero revisemos cómo trabajará. Cuando el usuario haga clic en el ícono editar al costado de cada elemento tarea, un campo de texto aparecerá mostrando el mismo texto que se muestra cuando no está en modo edición, como se muestra a continuación.
Editar tarea

El usuario puede ahora editar el texto y pulsar/presionar la tecla Enter en el teclado para guardar los cambios o presionar la tecla Esc para deshacer los cambios y salir del modo edición. Claro que la característica de la tecla Esc no es usable para usuarios de teléfonos porque no tienen tecla Esc en el teclado de Firefox OS.

Implementaremos esto en app/view/Todo.js porque la vista Todo representa cada elemento en la lista de tareas. Agregar los eventos clic en la vista Todo para el botón editar y keypress para el campo de texto.

events: {
   //'tap label'            :    'editTodo',
   'click .btn.edit'        :    'editTodo',
 
   'keypress input[type=text]'    :    'saveTodo'
}

Ahora, agrega el método editTodo en la vista Todo.

editTodo: function () {
   var self = this;
   console.log('editTodo');
 
   self.$taskEdit.val(self.$taskView.find('span').text());
 
   self.$btnEdit.attr('aria-hidden', 'true');
 
   self.$taskEdit
       .removeAttr('aria-hidden')
       .removeClass('slide-left-out')
       .addClass('slide-right-in')
       .focus();
 
   self.$taskView.attr('aria-hidden', 'true');
 
   return false;
},

Antes de ingresar al modo edición, el texto en span es copiado al campo de texto. Luego el botón editar ($btnEdit) es ocultado. El campo de texto ($taskEdit) es hecho visible usando una animaciones CSS3 y es enfocado para que el usuario pueda editar directamente en él. Finalmente, necesitamos ocultar $taskView para que el usuario solamente vea $taskEdit. Ahora el usuario está en modo edición y puede editar la tarea.

Pero nosotros también necesitamos guardar los cambios cuando el usuario pulse/golpee la tecla Enter y retorne al modo vista. De igual forma en la tecla Esc, salir del modo edición sin guardar los cambios. Agrega el manejador saveTodo que realizará estas operaciones.

saveTodo: function (e) {
   var self = this;
 
   //Guardar en caso sea Enter
   if (e.keyCode === 13) {
       console.log('enter');
       self.$taskView.find('span').text(self.$taskEdit.val());
 
   self.model
       .set({
           task: self.$taskEdit.val()
       })
       .save();
   }
 
   //27 es para la tecla Esc
   if (e.keyCode === 13 || e.keyCode === 27) {
       self.$taskEdit
       .removeClass('slide-right-in')
       .addClass('slide-left-out');
 
       self.$btnEdit.removeAttr('aria-hidden');
   }
},

El manejador del evento saveTodo es muy sencillo. En caso de presionar la tecla Enter, el texto es copiado desde el campo de texto al span y guardado en el modelo. En caso de las teclas Enter o Esc, el campo de texto ($taskEdit) es ocultado y el botón editar ($btnEdit) se vuelve visible. Pero te podrás estar preguntando por qué $taskView no se vuelve visible. Sí, nosotros necesitamos mostrarlo cuando la animación en $taskEdit termine. Para hacer eso, agrega el siguiente manejador de evento en el método render exactamente antes del return false;

self.$taskEdit
   .on('animationend', function (e) {
       if (e.animationName === 'slide-left-out') {
           self.$taskEdit.attr('aria-hidden', 'true');
           self.$taskView.removeAttr('aria-hidden');
       }
   });

Pruebas

Intenta editar diferentes tareas y también intenta actualizar la aplicación (en Firefox y Firefox OS Simulator) para verificar que si se guarda en IndexedDB o no. Si pruebas en el navegador Firefox, recuerda que forzar la actualización algunas veces no obtiene los archivos modificados cargados usando Require.js.

Marcar Tarea como terminada

Nosotros implementaremos la función de marcar como terminada también en la vista Todo, donde implementamos la función editar. Primero, agrega un manejador de evento en el checkbox que marca el elemento tarea como terminada.

//'tap input[type=checkbox]'   : 'markCompleted',
'click input[type=checkbox]' : 'markCompleted',

Ahora, agrega el manejador markCompleted en la vista Todo.

markCompleted: function (e) {
   var isCompleted = !this.model.get('completed');
   console.log('markCompleted');
 
   this.model
       .set({
           completed: isCompleted
       })
       .save();
},

Aquí, la propiedad completed se invierte y guarda de nuevo el modelo.

Pruebas

Intenta marcar diferentes tareas como terminadas o no terminadas. Luego, intenta todos.toJSON() en la consola para asegurarte que ha cambiado la bandera completed.

Eliminar tareas

Implementaremos Eliminar de una forma dinámica dándole al usuario posibilidades extra. Así es como funcionará el eliminar: si el usuario marca un o más elementos tarea como terminadas, entonces el botón eliminar funcionará como “eliminar elementos completados” de otra forma, funcionará como “eliminar todos los elementos”.

Su implementación abarcará varios archivos. Primero, abre ViewTodos y agrega un manejador para el botón eliminar.

//'tap footer .btn.del' : 'delTodos',
'click footer .btn.del' : 'delTodos',

Agregar el manejador delTodos.

delTodos: function () {
   /**
    * Otra solución posible para la siguiente condición es
    * this.$('#todo-list input[type=checkbox]:checked').length
    * Pero no lo usé para evitar el acceso al DOM
    */
 
   if (todos.where({completed: true}).length) {
   //funcionar como eliminar terminadas
 
       if (confirm('Delete Completed Tasks?')) {
           this.todosView.delCompleted();
       }
   } else {
       if (confirm('Delete All Tasks?')) {
           //funcionar como eliminar todos
           this.todosView.delAll();
       }
   }
 
   this.setBtnDelDisabled();
 
},

En este manejador, el total de elementos tarea terminados es verificado usando el método where de Backbone. Si hay elementos terminados, se llamará al método delCompleted de TodosView (que lo implementaremos en un momento). De otra forma, llama al método delAll. Al terminar, setBtnDelDisabled es llamado, algo que ya hemos tratado en este tutorial.

Ahora abre app/view/Todos.js y agrégale el método delCompleted.

delCompleted: function () {
   this.collection.remove(
       this.collection.where({completed: true})
   );
},

El método anterior, solamente remueve todos los modelos que tengan la propiedad completed como true. Ahora, agrega el método dellAll en la misma vista.

delAll: function () {
   /**
    * Podemos remover todos los modelos usando reset
    * this.collection.reset([]);
    *
    * Pero usé el método remove porque quiero remover
    * eventos que se lanzan para cada modelo
    */
 
   this.collection.remove(this.collection.models);
},

dellAll solamente remueve todos los modelos desde una colección.

Eliminar desde el DOM

Una cosa final que necesitamos hacer es eliminar el elemento correspondiente del DOM, el cual es borrado del modelo. Agrega la siguiente sentencia en el método initialize de la vista app/views/Todo.js

this.listenTo(this.model, 'remove', this.destroy);

La vista Todo inicia escuchando al evento remove desde su método model.destroy que será llamado si el modelo lanza el evento remove. Así que agrega el método destroy en la misma vista.

destroy: function () {
   this.$el.remove();
   this.model.destroy();
},

Simplemente, remove lo elimina del DOM y destroy es usado para eliminar de IndexedDB.

Pruebas

Intenta borrar algunas tareas usando el botón eliminar. Prueba ambos casos. Agrega algunos elementos tarea y marca unos cuantos como terminados. Luego intenta usar el botón eliminar y asegúrate que los elementos sean eliminados actualizando tu aplicación. De igual forma intenta eliminar todos los casos. No marques ningún elemento como terminado y presiones el botón eliminar. Esto debe eliminar todos los elementos en la lista.

Web Activity para Agregar/Seleccionar un contacto

La aplicación está casi completa. Un asunto final que nos gustaría enseñarte es Web Activities. Queremos darle al usuario una opción para insertar información de un contacto en el campo de texto en la vista ViewAdd (#view-add) . La actividad Web para seleccionar un contacto será lanzada cuando el usuario pulse en el enlace Insertar Contacto en ViewAdd, así que necesitamos agregar un manejador de evento para el enlace Insertar Contacto.

'click #activities .add-contact' : 'addContact'

Ahora, agrega el manejador addContact.

addContact: function () {
   var self = this,
       pick = new MozActivity({
           name: 'pick',
           data: {
               type: 'webcontacts/contact'
           }
       });
 
   pick.onsuccess = function () {
       var res = this.result;
       self.$task.val(self.$task.val() + res.name[0] + '(' + res.number + ')');
   };
 
   pick.onerror = function () {
       alert('ERROR: No se puede agregar el contacto!');
   };
 
   return false;
},

En el manejador anterior, la actividad pick es lanzada usando MozActivity. Todas las aplicaciones pueden manejar tipo webcontacts serán mostradas en la lista en el caso que haya más que una aplicación que maneje datos como de contactos. Si el contacto es seleccionado satisfactoriamente, el manejador será lanzado y la información del contacto es almacenada en this.result. De result, la información como el nombre y el número es agregado al campo de texto #task.

Pruebas

Tú no puedes probar esta característica en el navegador Firefox. Necesitas Firefox OS Simulator o un teléfono real para probarlo.

Eso es todo. Esperamos que hayas disfrutado este tutorial. Déjanos tu opinión en los comentarios.

The following two tabs change content below.

jorgev

Add-ons Developer Relations Lead at Mozilla
Jorge trabaja para el equipo de complementos de Mozilla, y se dedica a Mozilla Hispano y Mozilla Costa Rica en su tiempo libre. Actualmente está encargado del blog de Mozilla Hispano Labs.