É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, je vous encourage à consulter les tutoriels MDN utiles sur ce sujet. Cet article suppose que vous disposez de connaissances de base sur les API et les fonctionnalités. Même si vous n'avez jamais vu IndexedDB auparavant, nous espérons que la démonstration de cet article vous donnera une idée de ce que vous pouvez faire avec.

Notre démonstration est une simple preuve de concept d'application Intranet pour une entreprise. L'application permettra aux employés de rechercher d'autres employés. Pour offrir une expérience 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 fournit simplement une recherche et un affichage de type saisie semi-automatique d'un seul enregistrement d'employé. Mais l'avantage est que, une fois ces données disponibles sur le client, nous pouvons également les utiliser de plusieurs autres manières. Voici un aperçu des tâches de base que notre application doit effectuer.

  1. Nous devons configurer et initialiser une instance d'IndexedDB. Dans la plupart des cas, cette opération est simple, mais la faire fonctionner à la fois dans Chrome et Firefox s'avère un peu délicate.
  2. Nous devons vérifier si nous disposons de données et, si ce n'est pas le cas, les télécharger. En règle générale, cela se fait 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 lorsqu'elle crée ces données et empêcher l'utilisateur de les utiliser jusqu'à cette date. Il s'agit d'une opération unique. 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émonstration se concentre davantage sur les aspects de l'interface utilisateur.
  3. Lorsque l'application est prête, nous pouvons utiliser la commande Autocomplete de l'interface utilisateur jQuery pour effectuer la synchronisation avec IndexedDB. Bien que le contrôle de saisie semi-automatique accepte les listes et les tableaux de données de base, il dispose d'une API permettant d'utiliser n'importe quelle source de données. Nous allons vous montrer comment nous pouvons l'utiliser pour nous connecter à nos données IndexedDB.

Premiers pas

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 grand-chose, non ? Nous accordons une grande importance à trois aspects principaux de cette interface utilisateur. Le premier champ, "name", est celui qui sera utilisé pour la saisie semi-automatique. Elle est chargée désactivée et sera activée ultérieurement via JavaScript. La span à côté est utilisée lors du premier sémaphore pour fournir des mises à jour à l'utilisateur. Enfin, le div avec l'ID displayEmployee sera utilisé lorsque vous sélectionnerez un employé dans la suggestion automatique.

Voyons maintenant le code JavaScript. Il y a beaucoup d'informations à assimiler, alors nous allons les examiner étape par étape. Le code complet sera disponible à la fin pour que vous puissiez le voir dans son intégralité.

Tout d'abord, nous devons nous préoccuper de certains problèmes de préfixe parmi les navigateurs compatibles avec IndexedDB. Voici du code de la documentation Mozilla modifié pour fournir des alias simples pour les composants IndexedDB principaux 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 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 les informations sur les employés. Nous n'utiliserons cette valeur que plus tard, mais nous pouvons compiler notre modèle maintenant pour nous en débarrasser. Nous avons configuré un bloc de script en tant que type reconnu par Handlebars. Ce n'est pas très sophistiqué, mais cela facilite l'affichage du code HTML dynamique.

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

Il est ensuite recompilé dans notre code JavaScript comme suit :

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

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

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

L'ouverture d'une connexion à IndexedDB nous permet de lire et d'écrire des données, mais avant de le faire, nous devons nous assurer que nous disposons d'un objectStore. Un objectStore est semblable à une table de base de données. Un IndexedDB peut contenir de nombreux objectStores, chacun contenant une collection d'objets associés. Notre démonstration est simple et ne nécessite qu'un seul objectStore que nous appelons "employee". Lorsque l'indexedDB est ouvert pour la toute 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 d'objets DataStore, pour voir s'il contient "employee". Si ce n'est pas le cas, nous le faisons simplement. L'appel createIndex est important. Nous devons indiquer à IndexedDB les méthodes, en dehors des clés, que nous utiliserons pour récupérer les données. Nous allons utiliser "searchkey". Nous allons vous expliquer cela dans un instant.

L'événement onungradeneeded s'exécute automatiquement la première fois que vous exécutez le script. Une fois exécuté ou ignoré lors des prochaines exécutions, le gestionnaire onsuccess est exécuté. Un gestionnaire d'erreurs simple (et moche) est défini, puis nous appelons handleSeed.

Avant de continuer, récapitulons rapidement ce qui se passe. 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. Passons maintenant à la partie de la démonstration consacrée à l'ensemencement de données.

Gimme Some Data!

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 implique de créer une API basée sur un serveur pouvant renvoyer un nombre d'employés et nous permettre de récupérer des lots d'enregistrements. Vous pouvez imaginer un service simple qui accepte un décompte de départ et renvoie 100 personnes à la fois. Cette opération peut s'exécuter de manière asynchrone en arrière-plan lorsque l'utilisateur est occupé à d'autres tâches.

Pour notre démonstration, nous allons faire quelque chose de simple. Nous voyons le nombre d'objets, le cas échéant, dans notre IndexedDB. Si le nombre est inférieur à un certain seuil, nous créons simplement de faux utilisateurs. Sinon, nous considérons que nous avons terminé avec la partie de la graine et que nous pouvons activer la partie de la saisie semi-automatique de la démonstration. Examinons 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 toute première ligne est un peu complexe, car plusieurs opérations sont enchaînées les unes aux autres. Voyons-la en détail :

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.

objectStore("employee");

Obtenez le magasin d'objets des employés.

count()

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

onsuccess = function(e) {

Une fois terminé, 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 nul, nous commençons le processus de génération de graines.

Nous utilisons la div d'état mentionnée précédemment pour indiquer à l'utilisateur que nous allons commencer à collecter des données. En raison de la nature asynchrone de IndexedDB, nous avons configuré une variable simple, c'est-à-dire, qui assurera le suivi des ajouts. Nous faisons 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 qui se présente comme suit :

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

Cela suffit à définir une personne. Cependant, nous avons une exigence particulière pour pouvoir rechercher nos données. IndexedDB ne permet pas de rechercher des éléments sans tenir compte de la casse. Nous créons donc une copie du champ "nom" dans une nouvelle propriété, "clé de recherche". Si vous vous souvenez, il s'agit de la clé que nous avons déclarée devoir être créée en tant qu'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 et non sur le serveur backend (ou dans notre cas, sur le serveur backend imaginaire).

Pour effectuer les ajouts de base de données de manière performante, vous devez réutiliser la transaction pour toutes les écritures groupées. Si vous créez une transaction pour chaque écriture, le navigateur peut provoquer une écriture sur disque pour chaque transaction, ce qui va dégrader considérablement les performances lorsque vous ajoutez de nombreux éléments (par exemple, une minute pour écrire 1 000 objets).

Une fois le seed terminé, la partie suivante de notre application est déclenchée : setupAutoComplete.

Créer la saisie semi-automatique

Passons maintenant à la partie la plus intéressante : l'intégration du plug-in Autocomplete de l'UI jQuery. Comme pour la plupart de l'interface utilisateur jQuery, nous commençons par un élément HTML de base et l'améliorons en appelant une méthode de constructeur. Nous avons extrait 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 jQuery UI vous permet de définir une propriété source pouvant être personnalisée pour répondre à tous les besoins possibles, y compris nos données IndexedDB. L'API vous fournit la requête (essentiellement 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.

Nous commençons par masquer la div displayEmployee. Elle permet d'afficher un employé spécifique et, si un employé est déjà chargé, de le supprimer. Nous pouvons maintenant commencer à chercher.

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 conseil de Fong-Wan Chau, un utilisateur de StackOverflow, nous utilisons une plage d'index basée sur l'entrée comme limite inférieure et sur la lettre z comme limite supérieure. Notez également que nous avons mis le terme en minuscules pour correspondre aux données en minuscules que nous avons saisies.

Une fois terminé, nous pouvons ouvrir un curseur (comme si nous exécutions une requête de base de données) et itérer 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 minimum une clé de valeur. Nous définissons la valeur sur une version du nom correctement formatée. Nous renvoyons également l'intégralité de la personne. Vous allez voir pourquoi dans un instant. Voici d'abord une capture d'écran de la saisie semi-automatique en action. Nous utilisons le thème Vader pour l'UI jQuery.

Cela suffit à renvoyer les résultats de nos correspondances IndexedDB à la saisie semi-automatique. Nous souhaitons également afficher une vue détaillée du match lorsqu'un match est sélectionné. Nous avons spécifié un gestionnaire de sélection lors de la création de la saisie semi-automatique qui utilise le modèle Handlebars précédent.