Elementos de la IU de Databinding con IndexedDB

Introducción

IndexedDB es una poderosa manera de almacenar datos en el cliente. Si aún no lo has revisado, te recomendamos que leas los útiles instructivos de MDN sobre este tema. En este artículo, se asume que tienes conocimientos básicos de las APIs y sus funciones. Aunque nunca hayas visto la herramienta IndexedDB, esperamos que la demostración de este artículo te de una idea de lo que puedes 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 ágil, la base de datos de empleados se copia en la máquina del cliente y se almacena mediante IndexedDB. La demostración simplemente proporciona una búsqueda con el estilo de autocompletado y una visualización de un solo registro de empleado, pero lo bueno es que una vez que estos datos están disponibles en el cliente, también podemos usarlos de muchas otras formas. Este es un esquema básico de lo que debe hacer nuestra aplicación.

  1. Debemos configurar e inicializar una instancia de IndexedDB. En general, esto es sencillo, pero hacer que funcione tanto en Chrome como en Firefox resulta un poco complicado.
  2. Necesitamos ver si tenemos algún dato y, de no ser así, descargarlo. Por lo general, esto se hace mediante 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 entonces. Esta operación se realiza una sola vez. La próxima vez que el usuario ejecute la aplicación, no será necesario que realice este proceso. Una demostración más avanzada se encarga de las operaciones de sincronización entre el cliente y el servidor, pero esta demostración se centra más en los aspectos de la IU.
  3. Cuando la aplicación está lista, podemos usar el control de autocompletado de la IU de jQuery para sincronizarla con IndexedDB. Si bien el control de autocompletar permite listas y conjuntos de datos básicos, tiene una API para habilitar cualquier fuente de datos. Mostraremos cómo usar esto para conectar con nuestros datos de IndexedDB.

Getting Started

Esta demostración contiene varias partes, así que para comenzar, veamos 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? Existen tres aspectos principales de esta IU que nos interesan. El primero es el campo "nombre" que se usará para autocompletar. Se carga inhabilitada y se habilitará más adelante mediante JavaScript. El intervalo junto a él se usa durante el valor inicial inicial para proporcionar actualizaciones al usuario. Por último, se usará el elemento div con el ID displayEmployee cuando selecciones un empleado en las sugerencias automáticas.

Ahora, echemos un vistazo a JavaScript. Hay mucha información que asimilar aquí, así que la abordaremos paso a paso. El código completo estará disponible al final para que puedas verlo completo.

En primer lugar, existen algunos problemas con los prefijos de los que debemos preocuparnos entre los navegadores compatibles con IndexedDB. A continuación, te mostramos parte de 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, presentamos algunas variables globales que usaremos a lo largo de la demostración:

var db;
var template;

Ahora comenzaremos con el bloque jQuery document listo:

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

En nuestra demostración, se 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. Tenemos un bloque de secuencia de comandos configurado como un tipo reconocido por Handlebars. No es muy complicado, pero facilita la visualización del código HTML dinámico.

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

Luego, esto se compila en nuestro JavaScript de la siguiente manera:

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

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

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

Abrir una conexión con IndexedDB nos brinda acceso para leer y escribir datos, pero antes de hacerlo, debemos asegurarnos de tener un objectStore. Un objectStore es como una tabla de base de datos. Un IndexedDB puede tener muchos objectStores, cada uno contiene una colección de objetos relacionados. Nuestra demostración es simple y solo necesita un objeto ObjectStore que llamamos “employee”. Cuando se abre indexDB por primera vez o cuando cambias la versión en el código, se ejecuta un evento onupgrade needed. Podemos usar esto para configurar nuestro objeto 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 un empleado. Si no es así, solo lo hacemos. La llamada createIndex es importante. Debemos indicarle a IndexedDB qué métodos, fuera de las claves, usaremos para recuperar datos. Usaremos una que se llama valor de búsqueda. Esto se explicará en breve.

El evento onungradeneeded se ejecutará automáticamente la primera vez que ejecutemos la secuencia de comandos. Después de que se ejecuta o de que se omite en 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. Comprobamos si nuestro almacén de objetos existe. Si no es así, lo crearemos. Por último, llamamos a una función llamada handleSeed. Ahora centrémonos en la parte de la siembra de datos de nuestra demostración.

Envíanos algunos datos

Como se mencionó en la introducción de este artículo, esta demostración recrea una aplicación con estilo de intranet que necesita almacenar una copia de todos los empleados conocidos. Normalmente, esto implicaría crear una API basada en el servidor que pueda mostrar un recuento de empleados y proporcionar una forma para que recuperemos lotes de registros. Imagina un servicio simple que admite un recuento de inicio y devuelve 100 personas a la vez. Esto se podría ejecutar de forma asíncrona en segundo plano mientras el usuario está realizando otras acciones.

Para nuestra demostración, hacemos algo simple. Vemos cuántos objetos tenemos, si los hay, en nuestra IndexedDB. Si es inferior a un número determinado, simplemente crearemos usuarios falsos. De lo contrario, se considerará que finalizó la parte 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 vamos a desglosarla:

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

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

objectStore("employee");

Obtener el almacén de objetos del empleado

count()

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

onsuccess = function(e) {

Cuando hayas terminado, 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, entonces iniciamos nuestro proceso de origen.

Usamos el elemento div de estado que se mencionó anteriormente para informar al usuario que vamos a comenzar a obtener datos. Debido a la naturaleza asíncrona de IndexedDB, configuramos una variable simple para hacer un seguimiento de las adiciones. Volvimos a pedalear e insertamos a las personas falsas. La fuente de esa función está disponible en la descarga, pero muestra un objeto similar al siguiente:

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

Por sí mismo, esto es suficiente para definir a una persona. Sin embargo, tenemos un requisito especial para poder buscar nuestros datos. IndexedDB no proporciona una forma de buscar elementos sin distinguir mayúsculas de minúsculas. Por lo tanto, hacemos una copia del campo de apellido en una propiedad nueva, searchkey. Si recuerdan, esta es la clave que decimos que deberíamos crearse como un índice para nuestros datos.

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

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

Para realizar las incorporaciones de la base de datos de manera eficaz, debes reutilizar la transacción para todas las escrituras en lotes. Si creas una transacción nueva para cada escritura, el navegador puede provocar una escritura en el disco para cada transacción, y eso hará que tu rendimiento sea terrible cuando agregues muchos elementos (piensa en “1 minuto para escribir 1,000 objetos”; terrible).

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

Creación del autocompletado

Ahora viene la parte divertida: la conexión con el complemento Autocomplete de la IU de jQuery. Al igual que con la mayoría de las IU de jQuery, comenzamos con un elemento HTML básico y lo mejoramos llamando a un método de constructor. Simplificamos 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 de autocompletado de la IU de jQuery te permite definir una propiedad fuente 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. Esto se usa para mostrar un empleado individual y, si ya se cargó uno, para borrarlo. Ahora podemos empezar a buscar.

Para comenzar, creamos una transacción de solo lectura, un array llamado result y un controlador oncomplete que simplemente pase el resultado al control de autocompletado.

Para encontrar elementos que coincidan con nuestra entrada, usemos una propina del usuario de StackOverflow, Fong-Wan Chau: usamos un rango de índice basado en la entrada como un límite inferior y la entrada más la letra z como límite de rango superior. También fíjate que el término está en minúscula para que coincida con los datos que ingresamos en minúscula.

Cuando termine, podemos abrir un cursor (como ejecutar una consulta en la base de datos) y, luego, iterar los resultados. El control de autocompletado de la IU de jQuery te permite mostrar cualquier tipo de datos que desees, pero requiere una clave de valor como mínimo. Configuramos el valor con una versión del nombre con el formato correcto. También devuelvemos la persona completa. Verás por qué en un segundo. Primero, aquí hay una captura de pantalla del autocompletado en acción. Utilizamos el tema Vader para la IU de jQuery.

Por sí mismo, esto es suficiente para mostrar los resultados de nuestras coincidencias de IndexedDB con el autocompletado. Pero también queremos admitir una vista detallada de la coincidencia cuando se selecciona una. Durante la creación del autocompletado, especificamos un controlador de selección que utiliza la plantilla de Handlebars que usamos anteriormente.