Éléments d'interface utilisateur Databinding avec IndexedDB

Raymond Camden
Raymond Camden

Introduction

IndexedDB est un moyen efficace de stocker des données côté client. Si vous ne l'avez pas encore fait, nous vous invitons à consulter les tutoriels utiles sur le Réseau Display de Google à ce sujet. Cet article suppose que vous disposez de connaissances de base sur les API et leurs fonctionnalités. Même si vous n'avez encore jamais vu IndexedDB, nous espérons que la démonstration présentée dans cet article vous donnera une idée de ce que vous pouvez faire avec cet article.

Notre démonstration est une simple démonstration de faisabilité concernant l'application Intranet d'une entreprise. L'application permettra aux employés de rechercher d'autres employés. Pour offrir une expérience plus rapide et plus rapide, la base de données des employés est copiée sur l'ordinateur du client et stockée à l'aide d'IndexedDB. La démonstration propose simplement une recherche de type saisie semi-automatique et l'affichage d'un enregistrement d'employé. L'avantage, c'est que, une fois que ces données sont disponibles sur le client, nous pouvons également les utiliser de plusieurs autres façons. Voici une présentation basique de ce que doit faire notre application.

  1. Nous devons configurer et initialiser une instance d'une base de données indexée (IndexedDB). Dans l'ensemble, c'est simple, mais le faire fonctionner à la fois dans Chrome et Firefox s'avère un peu compliqué.
  2. Nous devons vérifier si nous disposons de données et, si ce n'est pas le cas, les télécharger. Généralement, cela se fait généralement via des appels AJAX. Pour notre démonstration, nous avons créé une classe utilitaire simple permettant de générer rapidement des données fictives. L'application doit reconnaître la création de ces données et empêcher l'utilisateur de les utiliser jusqu'à cette date. Vous n'aurez à effectuer cette opération qu'une seule fois. La prochaine fois que l'utilisateur exécutera l'application, il n'aura pas besoin de suivre ce processus. Une démonstration plus avancée gérerait les opérations de synchronisation entre le client et le serveur, mais cette démo se concentre davantage sur les aspects de l'interface utilisateur.
  3. Lorsque l'application est prête, nous pouvons utiliser la commande de saisie semi-automatique de l'interface jQuery pour effectuer la synchronisation avec IndexedDB. Bien que la commande de saisie semi-automatique vous permette d'utiliser des listes et des tableaux de données de base, elle dispose d'une API permettant d'utiliser n'importe quelle source de données. Nous verrons comment l'utiliser pour nous connecter à nos données IndexedDB.

Getting Started

Cette démonstration comporte plusieurs parties. Pour commencer, examinons la partie HTML.

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

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

Pas beaucoup, n'est-ce pas ? Cette interface utilisateur est axée sur trois aspects principaux. Le premier est le champ "name" (nom) qui sera utilisé pour la saisie semi-automatique. Le chargement est désactivé et sera activé ultérieurement via JavaScript. Le segment qui se trouve à côté est utilisé lors de la phase initiale pour fournir des mises à jour à l'utilisateur. Enfin, le div associé à l'ID displayEmployee sera utilisé lorsque vous sélectionnerez un employé dans la suggestion automatique.

Penchons-nous à présent sur JavaScript. Il y a beaucoup de choses à digérer ici, nous allons donc y arriver étape par étape. Le code complet est disponible à la fin pour que vous puissiez le voir dans son intégralité.

Tout d'abord, nous devons nous soucier de certains problèmes de préfixe parmi les navigateurs compatibles avec IndexedDB. Voici du code issu de la documentation Mozilla modifié afin de fournir des alias simples pour les principaux composants IndexedDB dont notre application a besoin.

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

Voici maintenant quelques variables globales que nous utiliserons tout au long de la démonstration:

var db;
var template;

Commençons par le bloc "jQuery document ready" :

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

Notre démonstration utilise Handlebars.js pour afficher des informations sur l'employé. Cela n'est pas utilisé plus tard, mais nous pouvons compiler notre modèle maintenant et le supprimer. Nous avons configuré un bloc de script dont le type est reconnu par les guidons. Cette méthode n'est pas très sophistiquée, mais elle facilite l'affichage du code HTML dynamique.

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

Le code est ensuite compilé dans notre JavaScript de la manière suivante:

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

Commençons à travailler avec IndexedDB. Tout d'abord, nous l'ouvrons.

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

L'ouverture d'une connexion à IndexedDB nous donne accès en lecture et en écriture aux données, mais avant cela, nous devons nous assurer que nous disposons d'un magasin d'objets. Un objet ObjectStore est semblable à une table de base de données. Une base de données indexée peut comporter plusieurs objets ObjectStore, chacun contenant une collection d'objets associés. Notre démonstration est simple et n'a besoin que d'un objet ObjectStore que nous appelons "employé". Lorsque la base de données indexée est ouverte pour la première fois ou lorsque vous modifiez la version dans le code, un événement "onupgradeneeded" est exécuté. Nous pouvons l'utiliser pour configurer notre 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();
};

Dans le bloc du gestionnaire d'événements onupgradeneeded, nous vérifions objectStoreNames, un tableau de magasins d'objets, pour voir s'il contient employee. Si ce n'est pas le cas, nous le faisons. L'appel createIndex est important. Nous devons indiquer à IndexedDB quelles méthodes, en dehors des clés, nous utiliserons pour récupérer les données. Nous en utiliserons une appelée clé de recherche. Cela est expliqué plus en détail.

L'événement onungradeneeded s'exécutera automatiquement la première fois que nous exécuterons le script. Une fois exécuté, ou ignoré lors des exécutions futures, le gestionnaire onsuccess est exécuté. Nous avons défini un gestionnaire d'erreurs simple (et moche), puis nous appelons handleSeed.

Avant de continuer, examinons rapidement ce qui se passe ici. Nous ouvrons la base de données. Nous vérifions si notre magasin d'objets existe. Si ce n'est pas le cas, nous le créons. Enfin, nous appelons une fonction nommée handleSeed. Intéressons-nous maintenant à la partie de notre démonstration consacrée à l'injection de données.

Donne des données !

Comme indiqué dans l'introduction de cet article, cette démonstration recrée une application de type intranet qui doit stocker une copie de tous les employés connus. Normalement, cela impliquerait de créer une API basée sur un serveur qui pourrait renvoyer un nombre d'employés et nous permettre de récupérer des lots d'enregistrements. Vous pouvez imaginer un service simple pouvant comptabiliser un nombre de démarrages et renvoyer 100 personnes à la fois. Elle peut s'exécuter de manière asynchrone en arrière-plan tandis que l'utilisateur ne fait pas autre chose.

Pour notre démonstration, nous faisons quelque chose de simple. Nous voyons combien d'objets avons notre IndexedDB, le cas échéant. S'il est inférieur à un certain nombre, nous créerons simplement des utilisateurs fictifs. Dans le cas contraire, la partie "seed" est considérée comme terminée et nous pouvons activer la saisie semi-automatique dans la démo. Regardons 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 première ligne est un peu complexe, car plusieurs opérations sont enchaînées les unes aux autres. Décomposons-la:

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

Cette opération crée une transaction en lecture seule. Toutes les opérations de données avec IndexedDB nécessitent une transaction quelconque.

objectStore("employee");

Permet d'obtenir le magasin d'objets des employés.

count()

Exécutez l'API count, qui, comme vous pouvez le deviner, effectue un décompte.

onsuccess = function(e) {

Une fois l'opération terminée, exécutez ce rappel. Dans le rappel, nous pouvons obtenir la valeur du résultat, qui correspond au nombre d'objets. Si le nombre est égal à zéro, nous commençons alors notre processus de graine.

Nous utilisons ce div d'état mentionné précédemment pour indiquer à l'utilisateur que nous allons commencer à obtenir des données. En raison de la nature asynchrone de IndexedDB, nous avons configuré une variable simple, faite, qui va suivre les ajouts. Nous effectuons une boucle et insérons les fausses personnes. La source de cette fonction est disponible dans le téléchargement, mais elle renvoie un objet semblable à celui-ci:

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

En soi, cela suffit à définir une personne. Mais nous avons une exigence particulière pour pouvoir effectuer des recherches dans nos données. IndexedDB ne permet pas de rechercher des éléments sans tenir compte de la casse. Par conséquent, nous créons une copie du champ lastname dans une nouvelle propriété, searchkey. Rappelez-vous qu'il s'agit de la clé qui, selon nous, doit être créée comme index pour nos données.

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

Comme il s'agit d'une modification spécifique au client, elle est effectuée ici plutôt que sur le serveur backend (ou, dans notre cas, sur le serveur backend imaginaire).

Pour effectuer les ajouts de bases de données de manière performante, vous devez réutiliser la transaction pour toutes les écritures par lot. Si vous créez une transaction pour chaque écriture, le navigateur peut provoquer une écriture sur disque pour chaque transaction, ce qui nuit à vos performances lors de l'ajout d'un grand nombre d'éléments (pensez à "1 minute pour écrire 1 000 objets").

Une fois la source de données terminée, la partie suivante de l'application est déclenchée : setupAutoComplete.

Créer la saisie semi-automatique

Passons maintenant à la partie la plus amusante : utiliser le plug-in Autocomplete de l'interface utilisateur jQuery. Comme pour la plupart des interfaces utilisateur jQuery, nous commençons par utiliser un élément HTML de base, puis nous l'améliorons en appelant une méthode constructeur sur celui-ci. Nous avons transformé l'ensemble du processus dans une fonction appelée setupAutoComplete. Examinons maintenant ce code.

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 partie la plus complexe de ce code est la création de la propriété source. La commande de saisie semi-automatique de l'interface jQuery vous permet de définir une propriété source qui peut être personnalisée pour répondre à tous les besoins, y compris à nos données IndexedDB. L'API fournit la requête (autrement dit, ce qui a été saisi dans le champ du formulaire) et un rappel de réponse. Vous êtes responsable de renvoyer un tableau de résultats à ce rappel.

La première chose à faire est de masquer le div displayEmployee. Cela permet d'afficher un employé individuel et de l'effacer, le cas échéant. Maintenant, nous pouvons commencer la recherche.

Nous commençons par créer une transaction en lecture seule, un tableau appelé "result" et un gestionnaire oncomplete qui transmet simplement le résultat à la commande de saisie semi-automatique.

Pour trouver des éléments correspondant à notre entrée, nous allons utiliser le pourboire de Fong-Wan Chau, utilisateur de StackOverflow. Nous utilisons une plage d'index basée sur l'entrée en tant que limite inférieure, et sur l'entrée plus la lettre z comme limite supérieure de la plage. Notez également que nous mettons le terme en minuscules pour correspondre aux données en minuscules que nous avons saisies.

Une fois l'opération terminée, nous pouvons ouvrir un curseur (comme s'il s'agissait d'une requête de base de données) et effectuer une itération sur les résultats. La commande de saisie semi-automatique de l'interface utilisateur jQuery vous permet de renvoyer n'importe quel type de données, mais nécessite au moins une clé de valeur. Nous définissons la valeur sur une version du nom correctement formatée. Nous renvoyons également la personne entière. Vous verrez pourquoi dans une seconde. Tout d'abord, voici une capture d'écran de la saisie semi-automatique. Nous utilisons le thème Vader pour l'interface utilisateur jQuery.

Cela suffit pour renvoyer les résultats de nos correspondances IndexedDB à la saisie semi-automatique. Mais nous voulons également permettre l'affichage d'une vue détaillée de la correspondance lorsqu'une correspondance est sélectionnée. Nous avons spécifié un gestionnaire "select" lors de la création de la saisie semi-automatique qui utilise le modèle "Handlebars" que vous avez utilisé précédemment.