As the IndexedDB API is more complex than localStorage, it will be more difficult to create an IndexedDB driver for Backbone as we did with localStorage; in this section, you will use what you have learned about IndexedDB in order to build a driver for Backbone.
The driver should open a database and initialize the stores when it is created for the first time:
// indexedDB/dataStore.js 'use strict'; var Backbone = require('backbone'); const ID_LENGTH = 10; var contacts = [ // ... ]; class DataStore { constructor() { this.databaseName = 'contacts'; } openDatabase() { var defer = Backbone.$.Deferred(); // If a database connection is already active use it, // otherwise open a new connection if (this.db) { defer.resolve(this.db); } else { let request = indexedDB.open(this.databaseName, 1); request.onupgradeneeded = () => { let db = request.result; this.createStores(db); }; request.onsuccess = () => { // Cache recently opened connection this.db = request.result; defer.resolve(this.db); }; } return defer.promise(); } createStores(db) { var store = db.createObjectStore('contacts', {keyPath: 'id'}); // Create the first records contacts.forEach(contact => { store.put(contact); }); } }
When the connection is opened, it creates the contacts store and puts the first records in the store. After that it caches the database handler in the db
attribute to reuse the connection for future requests.
Now, we should create the necessary method to create, update, delete, and read the data from the store:
// indexedDB/dataStore.js var crispy = require('crispy-string'); // ... class DataStore { create(model) { var defer = Backbone.$.Deferred(); // Assign an id to new models if (!model.id&& model.id !== 0) { let id = this.generateId(); model.set(model.idAttribute, id); } // Get the database connection this.openDatabase() .then(db =>this.store(db, model)) .then(result =>defer.resolve(result)); return defer.promise(); } generateId() { return crispy.base32String(ID_LENGTH); } // ... }
When a record is created, we should ensure that the model has an ID. We can generate it for the models that do not have an ID assigned. The store()
method will put the record in the indexedDB database:
// indexedDB/dataStore.js var crispy = require('crispy-string'); // ... class DataStore { // ... store(db, model) { var defer = Backbone.$.Deferred(); // Get the name of the object store varstoreName = model.store; // Get the object store handler vartx = db.transaction(storeName, 'readwrite'); var store = tx.objectStore(storeName); // Save the model in the store varobj = model.toJSON(); store.put(obj); tx.oncomplete = function() { defer.resolve(obj); }; tx.onerror = function() { defer.reject(obj); }; return defer.promise(); } // ... }
The store()
method obtains the name of the store from the modelstore
attribute and then, creates a readwrite
transaction for the given store name to put the record on it. The update()
method uses the same store()
method to save the record:
// indexedDB/dataStore.js class DataStore { // ... update(model) { var defer = Backbone.$.Deferred(); // Get the database connection this.openDatabase() .then(db =>this.store(db, model)) .then(result =>defer.resolve(result)); return defer.promise(); } // ... }
The update method does not assign an ID to the model, it completely replaces the previous record with the new model data. To delete a record, you can use the delete()
method of the object store handler:
// indexedDB/dataStore.js class DataStore { // ... destroy(model) { var defer = Backbone.$.Deferred(); // Get the database connection this.openDatabase().then(function(db) { // Get the name of the object store let storeName = model.store; // Get the store handler vartx = db.transaction(storeName, 'readwrite'); var store = tx.objectStore(storeName); // Delete object from the database let obj = model.toJSON(); store.delete(model.id); tx.oncomplete = function() { defer.resolve(obj); }; tx.onerror = function() { defer.reject(obj); }; }); return defer.promise(); } // ... }
To get all the models stored on an object store, you need to open a cursor and put all the items in an array, as follows:
// indexedDB/dataStore.js class DataStore { // ... findAll(model) { var defer = Backbone.$.Deferred(); // Get the database connection this.openDatabase().then(db => { let result = []; // Get the name of the object store let storeName = model.store; // Get the store handler let tx = db.transaction(storeName, 'readonly'); let store = tx.objectStore(storeName); // Open the query cursor let request = store.openCursor(); // onsuccesscallback will be called for each record // found for the query request.onsuccess = function() { let cursor = request.result; // Cursor will be null at the end of the cursor if (cursor) { result.push(cursor.value); // Go to the next record cursor.continue(); } else { defer.resolve(result); } }; }); return defer.promise(); } // ... }
Note how this time the transaction opened is in the readonly
mode. A single object can be obtained by querying the model ID:
// indexedDB/dataStore.js class DataStore { // ... find(model) { var defer = Backbone.$.Deferred(); // Get the database connection this.openDatabase().then(db => { // Get the name of the collection/store let storeName = model.store; // Get the store handler let tx = db.transaction(storeName, 'readonly'); let store = tx.objectStore(storeName); // Open the query cursor let request = store.openCursor(IDBKeyRange.only(model.id)); request.onsuccess = function() { let cursor = request.result; // Cursor will be null if record was not found if (cursor) { defer.resolve(cursor.value); } else { defer.reject(); } }; }); return defer.promise(); } // ... }
In the same way as we did with localStorage, this IndexedDB driver can be used to overwrite the Backbone.sync
function:
// app.js var store = new DataStore(); // ... Backbone.sync = function(method, model, options) { var response; var defer = Backbone.$.Deferred(); switch(method) { case 'read': if (model.id) { response = store.find(model); } else { response = store.findAll(model); } break; case 'create': response = store.create(model); break; case 'update': response = store.update(model); break; case 'delete': response = store.destroy(model); break; } response.then(function(result) { if (options &&options.success) { options.success(result); defer.resolve(result); } }); return defer.promise(); };
Then, models should add the store
attribute to indicate in which object store the model will be saved:
class Contact extends Backbone.Model { constructor(options) { // ,,, this.store = 'contacts'; } // ... } class ContactCollection extends Backbone.Collection { constructor(options) { // ... this.store = 'contacts'; } // ... }
IndexedDB allows you to store more data than localStorage; therefore, you can use it to store the avatar image too. Just make sure that the avatar
attribute is set so that an image is always selected:
class ContactPreview extends ModelView { // ... fileSelected(event) { event.preventDefault(); var $img = this.$('img'); // Get a blob instance of the file selected var $fileInput = this.$('#avatar')[0]; varfileBlob = $fileInput.files[0]; // Render the image selected in the img tag varfileReader = new FileReader(); fileReader.onload = event => { $img.attr('src', event.target.result); this.model.set({ avatar: { url: event.target.result } }); }; fileReader.readAsDataURL(fileBlob); this.trigger('avatar:selected', fileBlob); } }
Do not try to upload the image:
class ContactEditor { // ... showEditor(contact) { // ... // When avatar is selected, we can save it inmediatly if the // contact already exists on the server, otherwise just // remember the file selected //this.listenTo(contactPreview, 'avatar:selected', blob => { // this.avatarSelected = blob; // if (!contact.isNew()) { // this.uploadAvatar(contact); // } //}); } saveContact(contact) { // ... // The avatar attribute is read-only //if (contact.has('avatar')) { // contact.unset('avatar'); //} // ... } // ... }
18.189.171.125