Trabajando con IndexedDB - Parte 3

Bienvenida a la final parte de mi serie IndexedDB. Cuando comencé esta serie, mi intención era explicar una tecnología que no siempre es la más amigable para trabajar. De hecho, cuando intenté trabajar con IndexedDB por primera vez, el año pasado, mi reacción inicial fue algo negativa ("Algo negativo" como el Universo es "algo antiguo"). Ha sido un largo viaje, pero finalmente me siento un poco cómodo trabajando con IndexedDB y respeto lo que permite. Sigue siendo una tecnología que no se puede usar en todas partes (lamentablemente no fue agregada a iOS7), pero realmente creo que es una tecnología que la gente puede aprender y usar hoy en día..

En este artículo final, vamos a demostrar algunos conceptos adicionales que se basan en la demostración "completa" que construimos en el artículo anterior. Para ser claros tu debe estar atrapado en la serie o esta entrada será difícil de seguir, por lo que también puede consultar la primera parte.


Contando datos

Vamos a empezar con algo simple. Imagina que quieres añadir la paginación a tus datos. ¿Cómo obtendría un recuento de sus datos para poder manejar adecuadamente esa función? Ya te he mostrado cómo puedes conseguir todos sus datos y, ciertamente, podría usar eso como una forma de contar los datos, pero eso requiere recuperar todo. Si su base de datos local es enorme, podría ser lento. Por suerte, la especificación IndexedDB proporciona una forma mucho más sencilla de hacerlo.

El método count (), ejecutado en un objectStore, devolverá un conteo de datos. Como todo lo que hemos hecho, esto será asíncrono, pero puede simplificar el código en una llamada. Para nuestra base de datos de notas, he escrito una función llamada doCount () eso hace justamente esto:

function doCount () db.transaction (["note"], "readonly"). objectStore ("note"). count (). onsuccess = function (event) $ ("# sizeSpan"). text ("( "+ event.target.result +" Total de notas) "); ; 

Recuerde: si el código anterior es un poco difícil de seguir, puede dividirlo en varios bloques. Vea los artículos anteriores donde demostré esto. Al manejador de resultados se le pasa un valor de resultado que representa el número total de objetos disponibles en la tienda. Modifiqué la interfaz de usuario de nuestra demostración para incluir un espacio vacío en el encabezado.

Base de datos de notas 

Lo último que debo hacer es simplemente agregar una llamada a doCount cuando la aplicación se inicie y después de cualquier operación de agregar o eliminar. Aquí hay un ejemplo del controlador de éxito para abrir la base de datos.

openRequest.onsuccess = function (e) db = e.target.result; db.onerror = function (event) // ¡Controlador genérico de errores para todos los errores dirigidos a las // solicitudes de esta base de datos! alert ("Error de base de datos:" + event.target.errorCode); ; displayNotes (); doCount (); ;

Puede encontrar el ejemplo completo en el zip que descargó como fulldemo2. (Como un FYI, fulldemo1 es la aplicación como estaba al final del artículo anterior).


Filtrar a medida que escribe

Para nuestra próxima función, vamos a agregar un filtro básico a la lista de notas. En los artículos anteriores de esta serie cubrí cómo IndexedDB hace no Permitir la búsqueda de forma libre. No puedes (bueno, no fácilmente) buscar contenido que contiene una palabra clave. Pero con el poder de los rangos, es fácil al menos admitir la coincidencia al principio de una cadena.

Si recuerdas, un rango nos permite capturar datos de una tienda que comienza con un cierto valor, termina con un valor o se encuentra en medio. Podemos usar esto para implementar un filtro básico contra el título de nuestros campos de notas. Primero, necesitamos agregar un índice para esta propiedad. Recuerda, esto solo se puede hacer en el evento onupgradeneeded.

 if (! thisDb.objectStoreNames.contains ("note")) console.log ("Necesito hacer el almacén de objetos de la nota"); objectStore = thisDb.createObjectStore ("note", keyPath: "id", autoIncrement: true); objectStore.createIndex ("title", "title", unique: false); 

A continuación, agregué un campo de formulario simple a la interfaz de usuario:


Luego agregué un controlador "keyup" al campo para ver las actualizaciones inmediatas mientras escribo.

$ ("# filterField"). on ("keyup", function (e) var filter = $ (this) .val (); displayNotes (filter););

Observe cómo estoy llamando a displayNotes. Esta es la misma función que usé antes para mostrar todo. Voy a actualizarlo para admitir tanto una acción de "obtener todo" como una acción de tipo "obtener filtrado". Echémosle un vistazo..

función displayNotes (filtro) var transaction = db.transaction (["note"], "readonly"); contenido var = ""; transaction.oncomplete = function (event) $ (" # noteList "). html (content);; var handleResult = function (event) var cursor = event.target.result; if (cursor) content + = ""; content + =""; content + =""; content + =""; cursor.continue (); else content + ="
TítuloActualizadoY
"+ cursor.value.title +""+ dtFormat (cursor.value.updated) +"Editar Borrar
";; var objectStore = transaction.objectStore (" note "); if (filter) // Crédito: http://stackoverflow.com/a/8961462/52160 var range = IDBKeyRange.bound (filter, filter + "\ uffff"); var index = objectStore.index ("title"); index.openCursor (range) .onsuccess = handleResult; else objectStore.openCursor (). onsuccess = handleResult;

Para ser claros, el único cambio aquí está en la parte inferior. Abrir un cursor con o sin un rango nos da el mismo tipo de resultado del controlador de eventos. Eso es útil entonces, ya que hace que esta actualización sea tan trivial. El único aspecto complejo es en realidad la construcción de la gama. Note lo que he hecho aquí. La entrada, filtro, es lo que el usuario escribió. Así que imagina que esto es "El". Queremos encontrar notas con un título que comience con "The" y termine en cualquier carácter. Esto se puede hacer simplemente configurando el extremo lejano del rango a un carácter ASCII alto. No puedo tomar crédito por esta idea. Vea el enlace de StackOverflow en el código para la atribución.

Puedes encontrar esta demo en el fulldemo3 carpeta. Tenga en cuenta que está utilizando una nueva base de datos, por lo que si ha ejecutado los ejemplos anteriores, esta estará vacía cuando la ejecute por primera vez..

Si bien esto funciona, tiene un pequeño problema. Imagina una nota titulada "Regla de los santos". (Porque lo hacen. Solo lo dicen.) Lo más probable es que intentes buscarlo escribiendo "santos". Si haces esto, el filtro no funcionará porque distingue entre mayúsculas y minúsculas. Como nos las arreglamos?

Una forma es simplemente almacenar una copia de nuestro título en minúsculas. Esto es relativamente fácil de hacer. Primero, modifiqué el índice para usar una nueva propiedad llamada titlelc.

 objectStore.createIndex ("titlelc", "titlelc", unique: false);

Luego modifiqué el código que almacena las notas para crear una copia del campo:

$ ("# saveNoteButton"). on ("click", function () var title = $ ("# title"). val (); var body = $ ("# body"). val (); var key = $ ("# clave"). val (); var titlelc = title.toLowerCase (); var t = db.transaction (["note"], "readwrite"); if (key === "")  t.objectStore ("nota") .add (title: title, body: body, actualizado: new Date (), titlelc: titlelc); else t.objectStore ("note") .put (title: title, body: body, actualizado: new Date (), id: Number (key), titlelc: titlelc);

Finalmente, modifiqué la búsqueda para simplemente escribir en minúsculas las entradas del usuario. De esa manera, si ingresas a "Santos" funcionará igual de bien que a "santos".

 filter = filter.toLowerCase (); var range = IDBKeyRange.bound (filter, filter + "\ uffff"); var index = objectStore.index ("titlelc");

Eso es. Puedes encontrar esta versión como fulldemo4.


Trabajar con propiedades de matriz

Para nuestra mejora final, agregaré una nueva función a nuestra aplicación de notas: etiquetado. Esta voluntad
le permite agregar cualquier número de etiquetas (piense en las palabras clave que describen la nota) para que luego pueda encontrar otras
Notas con la misma etiqueta. Las etiquetas se almacenarán como una matriz. Eso por sí solo no es tan importante. Mencioné al comienzo de esta serie que podría almacenar fácilmente los arreglos como propiedades. Lo que es un poco más complejo es manejar la búsqueda. Comencemos por hacerlo para que pueda agregar etiquetas a una nota.

Primero, modifiqué mi formulario de nota para tener un nuevo campo de entrada. Esto permitirá al usuario ingresar etiquetas separadas por una coma:


Puedo guardar esto simplemente actualizando mi código que maneja la creación / actualización de notas.

 var etiquetas = []; var tagString = $ ("# tags"). val (); if (tagString.length) tags = tagString.split (",");

Observe que estoy predeterminando el valor a una matriz vacía. Solo lo relleno si escribiste algo. Guardar esto es tan simple como agregarlo al objeto que pasamos a IndexedDB:

 if (key === "") t.objectStore ("note") .add (title: title, body: body, actualizado: new Date (), titlelc: titlelc, tags: tags);  else t.objectStore ("note") .put (title: title, body: body, actualizado: new Date (), id: Number (key), titlelc: titlelc, tags: tags); 

Eso es. Si escribes algunas notas y abres la pestaña de Recursos de Chrome, puedes ver los datos que se almacenan.


Ahora agreguemos etiquetas a la vista cuando muestres una nota. Para mi aplicación, me decidí por un caso de uso simple para esto. Cuando se muestra una nota, si hay etiquetas, las incluiré en una lista. Cada etiqueta será un enlace. Si hace clic en ese enlace, le mostraré una lista de notas relacionadas con la misma etiqueta. Veamos esa lógica primero.

función displayNote (id) var transaction = db.transaction (["note"]); var objectStore = transaction.objectStore ("nota"); var request = objectStore.get (id); request.onsuccess = function (event) var note = request.result; contenido var = "

"+ nota.título +"

"; if (note.tags.length> 0) content + ="Etiquetas: "; note.tags.forEach (función (elm, idx, arr) content + =" "+ elm +" ";); content + ="
"; content + ="

"+ note.body +"

"; I $ noteDetail.html (contenido) .show (); $ noteForm.hide ();;

Esta función (una nueva adición a nuestra aplicación) maneja el código de visualización de la nota vinculado formalmente al evento de clic de la celda de la tabla. Necesitaba una versión más abstracta del código para que cumpla con ese propósito. En su mayor parte es lo mismo, pero tenga en cuenta la lógica para comprobar la longitud de la propiedad de etiquetas. Si la matriz no está vacía, el contenido se actualiza para incluir una lista simple de etiquetas. Cada uno está envuelto en un enlace con una clase particular que usaré para buscar más tarde. También he añadido un div específicamente para manejar esa búsqueda.


En este punto, tengo la capacidad de agregar etiquetas a una nota y mostrarlas más tarde. También he planeado permitir al usuario hacer clic en esas etiquetas para que puedan encontrar otras notas usando la misma etiqueta. Ahora viene la parte compleja..

Has visto cómo puedes obtener contenido basado en un índice. Pero, ¿cómo funciona eso con propiedades de matriz? Resulta que la especificación tiene una marca específica para tratar esto: multiEntry. Al crear un índice basado en matrices, debe establecer este valor en verdadero. Aquí es cómo mi aplicación lo maneja:

objectStore.createIndex ("tags", "tags", unique: false, multiEntry: true);

Que maneja bien el aspecto de almacenamiento. Ahora hablemos de búsqueda. Aquí está el controlador de clic para la clase de enlace de etiqueta:

$ (document) .on ("click", ".tagLookup", function (e) var tag = e.target.text; var parentNote = $ (this) .data ("noteid"); var doneOne = false; contenido var = "Notas relacionadas:
"; var transaction = db.transaction ([" note "]," readonly "); var objectStore = transaction.objectStore (" note "); var tagIndex = objectStore.index (" tags "); var range = IDBKeyRange.only (etiqueta); transaction.oncomplete = function (event) if (! doneOne) content + = "Ninguna otra nota usó esta etiqueta."; content + = "

"; $ (" # relatedNotesDisplay "). html (content);; var handleResult = function (event) var cursor = event.target.result; if (cursor) if (cursor.value.id! = parentNote) doneOne = true; content + = ""+ cursor.value.title +"
"; cursor.continue ();; tagIndex.openCursor (range) .onsuccess = handleResult;);

Hay bastante aquí, pero honestamente, es muy similar a lo que hemos discutido antes. Cuando hace clic en una etiqueta, mi código comienza por tomar el texto del enlace para el valor de la etiqueta. Creo mis objetos de transacción, almacén de objetos e índice como he visto antes. La gama es nueva esta vez. En lugar de crear un rango de algo y para algo, podemos usar la única () api para especificar que queremos un rango de solo un valor. Y sí, eso también me pareció extraño. Pero funciona muy bien. Puedes ver, luego abrimos el cursor y podemos iterar sobre los resultados como antes. Hay un poco de código adicional para manejar los casos donde no puede haber coincidencias. También tomo nota de la original note, es decir, el que está viendo ahora, para que no lo muestre también. Y eso es realmente. Tengo un último fragmento de código que controla los eventos de clic en esas notas relacionadas para que pueda verlos fácilmente:

$ (document) .on ("click", ".loadNote", function (e) var noteId = $ (this) .data ("noteid"); displayNote (noteId););

Puedes encontrar esta demo en la carpeta. fulldemo5.


Conclusión

Espero sinceramente que esta serie te haya sido útil. Como dije al principio, IndexedDB no era una tecnología que disfrutara usando. Cuanto más trabajaba con él, y cuanto más empecé a comprender cómo hacía las cosas, más comenzaba a apreciar cuánto podía ayudarnos esta tecnología como desarrolladores web. Definitivamente tiene espacio para crecer, y definitivamente puedo ver a las personas que prefieren usar las bibliotecas de envoltorios para simplificar las cosas, pero creo que el futuro para esta característica es genial!