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 presupone que tienes conocimientos básicos sobre las APIs y sus 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. Es necesario 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 de autocompletar de la IU de jQuery para sincronizarse 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

Tenemos varias partes en esta demostración, así que para comenzar con facilidad, veamos la parte de 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 junto a él se usa durante el valor 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 con los prefijos que debemos preocuparnos entre los navegadores compatibles con 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 seguir compilando 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 objeto objectStore que llamamos “empleado”. Cuando se abre la base de datos indexada por primera vez o cuando se cambia la versión en el código, se ejecuta un evento onupgradeneeded. Podemos usar esto 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 explicará en breve.

El evento onungradeneeded se ejecutará automáticamente la primera vez que ejecutemos la secuencia de comandos. Después de ejecutarlo, o de omitirlo en las ejecuciones futuras, se ejecuta 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 lo hace, la creamos. Por último, llamamos a una función denominada handleSeed. Ahora, centrémonos en la parte de la demostración de propagación de datos.

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 necesita 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 tareas.

Para nuestra demostración, hacemos algo simple. Vemos cuántos objetos, si los hay, tenemos en nuestro IndexedDB. Si es inferior a una cantidad determinada, simplemente crearemos usuarios falsos. De lo contrario, se considera que ya terminamos con la parte inicial y podemos habilitar la parte de autocompletar 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();

Dado que esta es una modificación específica del cliente, se hace aquí en lugar de hacerlo en el servidor de backend (o, en nuestro caso, en 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, el navegador puede generar una operación de escritura en el disco para cada transacción, lo que puede afectar tu rendimiento si agregas muchos elementos (piensa en “1 minuto para escribir 1,000 objetos” muy mal).

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 de displayEmployee. Se usa para mostrar un empleado individual y, si uno ya se cargó, 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 hecho esto, podemos 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é. Primero, esta es 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 una vista detallada de la coincidencia cuando se selecciona una. Especificamos un controlador de selección cuando creamos el autocomplete que usa la plantilla de Handlebars anterior.