Elementos de la IU de Databinding con IndexedDB

Introducción

IndexedDB es una forma eficaz de almacenar datos del cliente. Si aún no lo hiciste, te recomendamos que leas los instructivos de MDN útiles sobre el tema. En este artículo, se supone que tienes conocimientos básicos de las APIs y las funciones. Incluso si nunca antes viste IndexedDB, espero que la demostración de este artículo te dé una idea de lo que se puede hacer con ella.

Nuestra demostración es una prueba de concepto simple de una aplicación de intranet para una empresa. La aplicación permitirá que los empleados busquen a otros empleados. Para proporcionar una experiencia más rápida y fluida, la base de datos de los empleados se copia en la máquina del cliente y se almacena con IndexedDB. La demostración solo proporciona una búsqueda y una visualización de estilo Autocompletar de un solo registro de empleado, pero lo bueno es que, una vez que estos datos estén disponibles en el cliente, también podremos usarlos de otras maneras. Este es un esquema básico de lo que debe hacer nuestra aplicación.

  1. Tenemos que configurar e inicializar una instancia de IndexedDB. En general, esto es sencillo, pero hacer que funcione en Chrome y Firefox puede ser un poco complicado.
  2. Debemos ver si tenemos datos y, de no ser así, descargarlos. Por lo general, esto se hace a través de llamadas AJAX. Para nuestra demostración, creamos una clase de utilidad simple para generar datos falsos con rapidez. La aplicación deberá reconocer cuándo está creando estos datos y evitar que el usuario los use hasta ese momento. Esta es una operación única. La próxima vez que el usuario ejecute la aplicación, no necesitará realizar este proceso. Una demostración más avanzada controlaría las operaciones de sincronización entre el cliente y el servidor, pero esta demostración se enfoca más en los aspectos de la IU.
  3. Cuando la aplicación esté lista, podremos usar el control Autocomplete de jQuery UI para sincronizarla con IndexedDB. Si bien el control Autocomplete permite listas y arrays de datos básicos, tiene una API para admitir cualquier fuente de datos. Te mostraremos cómo podemos usar esto para conectarnos a nuestros datos de IndexedDB.

Comenzar

Esta demostración tiene varias partes, así que, para comenzar de forma sencilla, analicemos la parte HTML.

<form>
 
<p>
   
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
   
</p>
</form>

<div id="displayEmployee"></div>

No mucho, ¿verdad? Hay tres aspectos principales de esta IU que nos interesan. Primero, está el campo "name" que se usará para el autocompletado. Se carga inhabilitado y se habilitará más adelante a través de JavaScript. El intervalo que se encuentra junto a él se usa durante la propagación inicial para proporcionar actualizaciones al usuario. Por último, se usará el div con el ID displayEmployee cuando selecciones un empleado de la función de sugerencias automáticas.

Ahora, veamos el código JavaScript. Hay mucho por analizar, así que lo haremos paso a paso. El código completo estará disponible al final para que puedas verlo en su totalidad.

En primer lugar, hay algunos problemas de prefijo que debemos tener en cuenta entre los navegadores que admiten IndexedDB. Este es un código de la documentación de Mozilla modificado para proporcionar alias simples para los componentes principales de IndexedDB que necesita nuestra aplicación.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

A continuación, se incluyen algunas variables globales que usaremos a lo largo de la demostración:

var db;
var template;

Ahora comenzaremos con el bloque de jQuery document ready:

$(document).ready(function() {
  console
.log("Startup...");
 
...
});

Nuestra demostración usa Handlebars.js para mostrar los detalles de los empleados. No se usará hasta más adelante, pero podemos compilar nuestra plantilla ahora y quitarla del camino. Tenemos un bloque de secuencia de comandos configurado como un tipo reconocido por Handlebars. No es muy sofisticado, pero facilita la visualización del HTML dinámico.

<h2>, </h2>
Department:
<br/>
Email:
<a href='mailto:'></a>

Luego, se vuelve a compilar en nuestro código JavaScript de la siguiente manera:

//Create our template
var source = $("#employeeTemplate").html();
template
= Handlebars.compile(source);

Ahora comencemos a trabajar con nuestro IndexedDB. Primero, lo abrimos.

var openRequest = indexedDB.open("employees", 1);

Abrir una conexión a IndexedDB nos brinda acceso para leer y escribir datos, pero antes de hacerlo, debemos asegurarnos de tener un objetoStore. Un objectStore es como una tabla de base de datos. Una IndexedDB puede tener muchos objectStores, cada uno de los cuales contiene una colección de objetos relacionados. Nuestra demostración es simple y solo necesita un objetoStore al que llamamos “empleado”. Cuando se abre indexedDB por primera vez o cuando cambias la versión en el código, se ejecuta un evento onupgradeneeded. Podemos usarlo para configurar nuestro objectStore.

// Handle setup.
openRequest
.onupgradeneeded = function(e) {

  console
.log("running onupgradeneeded");
 
var thisDb = e.target.result;

 
// Create Employee
 
if(!thisDb.objectStoreNames.contains("employee")) {
    console
.log("I need to make the employee objectstore");
   
var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore
.createIndex("searchkey", "searchkey", {unique: false});
 
}

};

openRequest
.onsuccess = function(e) {
  db
= e.target.result;

  db
.onerror = function(e) {
    alert
("Sorry, an unforseen error was thrown.");
    console
.log("***ERROR***");
    console
.dir(e.target);
 
};

  handleSeed
();
};

En el bloque del controlador de eventos onupgradeneeded, verificamos objectStoreNames, un array de almacenes de objetos, para ver si contiene employee. De lo contrario, simplemente lo hacemos. La llamada a createIndex es importante. Debemos indicarle a IndexedDB qué métodos, además de las claves, usaremos para recuperar datos. Usaremos uno llamado searchkey. Esto se explica a continuación.

El evento onungradeneeded se ejecutará automáticamente la primera vez que ejecutemos la secuencia de comandos. Después de que se ejecute o se omita en las ejecuciones futuras, se ejecutará el controlador onsuccess. Tenemos definido un controlador de errores simple (y feo) y, luego, llamamos a handleSeed.

Antes de continuar, repasemos rápidamente lo que sucede aquí. Abrimos la base de datos. Verificamos si existe nuestro almacén de objetos. Si no es así, lo crearemos. Por último, llamamos a una función llamada handleSeed. Ahora, enfoquémonos en la parte de creación de datos de nuestra demostración.

Gimme Some Data!

Como se mencionó en la introducción de este artículo, esta demostración recrea una aplicación de estilo intranet que debe almacenar una copia de todos los empleados conocidos. Por lo general, esto implicaría crear una API basada en un servidor que pueda mostrar un recuento de empleados y proporcionar una forma de recuperar lotes de registros. Puedes imaginar un servicio simple que admita un recuento de inicio y muestre 100 personas a la vez. Esto se puede ejecutar de forma asíncrona en segundo plano mientras el usuario está haciendo otras cosas.

En nuestra demostración, hacemos algo sencillo. Vemos cuántos objetos, si los hay, tenemos en nuestra IndexedDB. Si es inferior a una cantidad determinada, simplemente crearemos usuarios falsos. De lo contrario, se considera que terminamos con la parte de la propagación inicial y podemos habilitar la parte de autocompletado de la demostración. Veamos handleSeed.

function handleSeed() {
 
// This is how we handle the initial data seed. Normally this would be via AJAX.

  db
.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
   
var count = e.target.result;
   
if (count == 0) {
      console
.log("Need to generate fake data - stand by please...");
      $
("#status").text("Please stand by, loading in our initial data.");
     
var done = 0;
     
var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
     
// Generate 1k people
     
for (var i = 0; i < 1000; i++) {
         
var person = generateFakePerson();
         
// Modify our data to add a searchable field
         person
.searchkey = person.lastname.toLowerCase();
         resp
= employees.add(person);
         resp
.onsuccess = function(e) {
           done
++;
           
if (done == 1000) {
             $
("#name").removeAttr("disabled");
             $
("#status").text("");
             setupAutoComplete
();
           
} else if (done % 100 == 0) {
             $
("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           
}
         
}
     
}
   
} else {
      $
("#name").removeAttr("disabled");
      setupAutoComplete
();
   
}
 
};
}

La primera línea es un poco compleja, ya que tenemos varias operaciones encadenadas entre sí, así que analicémosla:

db.transaction(["employee"], "readonly");

Esto crea una nueva transacción de solo lectura. Todas las operaciones de datos con IndexedDB requieren algún tipo de transacción.

objectStore("employee");

Obtén el almacén de objetos de los empleados.

count()

Ejecuta la API de recuento, que, como puedes adivinar, realiza un recuento.

onsuccess = function(e) {

Cuando termines, ejecuta esta devolución de llamada. Dentro de la devolución de llamada, podemos obtener el valor del resultado, que es la cantidad de objetos. Si el recuento fue cero, comenzamos el proceso de propagación.

Usamos ese div de estado mencionado anteriormente para enviarle al usuario un mensaje de que comenzaremos a obtener datos. Debido a la naturaleza asíncrona de IndexedDB, configuramos una variable simple, done, que hará un seguimiento de las incorporaciones. Realizamos un bucle y, luego, insertamos a las personas falsas. La fuente de esa función está disponible en la descarga, pero muestra un objeto que se ve de la siguiente manera:

{
  firstname
: "Random Name",
  lastname
: "Some Random Last Name",
  department
: "One of 8 random departments",
  email
: "first letter of firstname+lastname@fakecorp.com"
}

Por sí solo, esto es suficiente para definir a una persona. Sin embargo, tenemos un requisito especial para poder buscar en nuestros datos. IndexedDB no proporciona una forma de buscar elementos sin distinción entre mayúsculas y minúsculas. Por lo tanto, hacemos una copia del campo apellido en una propiedad nueva, searchkey. Si recuerdas, esta es la clave que dijimos que se debe crear como un índice para nuestros datos.

// Modify our data to add a searchable field
person
.searchkey = person.lastname.toLowerCase();

Como esta es una modificación específica del cliente, se realiza aquí en lugar de en el servidor de backend (o, en nuestro caso, el servidor de backend imaginario).

Para realizar las incorporaciones a la base de datos de manera eficiente, debes volver a usar la transacción para todas las operaciones de escritura por lotes. Si creas una transacción nueva para cada operación de escritura, es posible que el navegador cause una operación de escritura en el disco para cada transacción, lo que hará que el rendimiento sea terrible cuando agregues muchos elementos (piensa en “1 minuto para escribir 1,000 objetos”, algo terrible).

Una vez que se completa la propagación inicial, se activa la siguiente parte de nuestra aplicación: setupAutoComplete.

Crea el Autocomplete

Ahora viene la parte divertida: conectarnos con el complemento Autocomplete de la IU de jQuery. Al igual que con la mayoría de la IU de jQuery, comenzamos con un elemento HTML básico y lo mejoramos llamando a un método de constructor. Extrajimos todo el proceso en una función llamada setupAutoComplete. Ahora, veamos ese código.

function setupAutoComplete() {

 
//Create the autocomplete
  $
("#name").autocomplete({
    source
: function(request, response) {

      console
.log("Going to look for "+request.term);

      $
("#displayEmployee").hide();

     
var transaction = db.transaction(["employee"], "readonly");
     
var result = [];

      transaction
.oncomplete = function(event) {
        response
(result);
     
};

     
// TODO: Handle the error and return to it jQuery UI
     
var objectStore = transaction.objectStore("employee");

     
// Credit: http://stackoverflow.com/a/8961462/52160
     
var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
     
var index = objectStore.index("searchkey");

      index
.openCursor(range).onsuccess = function(event) {
       
var cursor = event.target.result;
       
if(cursor) {
          result
.push({
            value
: cursor.value.lastname + ", " + cursor.value.firstname,
            person
: cursor.value
         
});
          cursor
.continue();
       
}
     
};
   
},
    minLength
: 2,
    select
: function(event, ui) {
      $
("#displayEmployee").show().html(template(ui.item.person));
   
}
 
});

}

La parte más compleja de este código es la creación de la propiedad fuente. El control Autocomplete de jQuery UI te permite definir una propiedad de origen que se puede personalizar para satisfacer cualquier necesidad posible, incluso nuestros datos de IndexedDB. La API te proporciona la solicitud (básicamente, lo que se escribió en el campo del formulario) y una devolución de llamada de respuesta. Eres responsable de enviar una matriz de resultados a esa devolución de llamada.

Lo primero que hacemos es ocultar el div displayEmployee. Se usa para mostrar un empleado individual y, si se cargó uno anteriormente, para borrarlo. Ahora podemos comenzar a buscar.

Comenzamos por crear una transacción de solo lectura, un array llamado result y un controlador oncomplete que simplemente pasa el resultado al control de autocompletado.

Para encontrar elementos que coincidan con nuestra entrada, usemos una sugerencia del usuario de StackOverflow Fong-Wan Chau: Usamos un rango de índice basado en la entrada como límite inferior y la entrada más la letra z como límite superior del rango. Ten en cuenta que también escribimos el término en minúsculas para que coincida con los datos en minúsculas que ingresamos.

Una vez que termines, puedes abrir un cursor (piensa en ello como ejecutar una consulta de base de datos) y iterar sobre los resultados. El control de autocompletado de jQuery UI te permite mostrar cualquier tipo de datos que desees, pero requiere una clave de valor como mínimo. Establecimos el valor en una versión del nombre con un formato agradable. También devolvemos a la persona completa. En un segundo, verás por qué. En primer lugar, aquí tienes una captura de pantalla del autocompletado en acción. Usamos el tema Vader para la IU de jQuery.

Esto es suficiente para mostrar los resultados de nuestras coincidencias de IndexedDB en el autocompletado. Sin embargo, también queremos admitir la visualización de una vista de detalles de la coincidencia cuando se selecciona una. Especificamos un controlador de selección cuando creamos el autocomplete que usa la plantilla de Handlebars anterior.