terça-feira, 2 de setembro de 2014

Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL

Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL


  • By Matt Andrews

  • September 2nd, 2014

  • TechniquesTools

  • 1 Comment

Making an focus work offline can be a daunting task. In this article, Matthew Andrews, a lead developer behind FT Labs, shares a few insights he had schooled along a proceed while building a FT application. Matthew will also be regulating a “Making It Work Offline” workshop1 during a arriving Smashing Conference in Freiburg in mid-September 2014. — Ed.


We’re going to make a elementary offline-first to-do application2 with HTML5 technology. Here is what a app will do:


  • store information offline and bucket though an Internet connection;

  • allow a user to supplement and undo equipment in a to-do list;

  • store all information locally, with no behind end;

  • run on a first- and second-most new versions of all vital desktop and mobile browsers.

The finish plan is prepared for forking on GitHub3.


Which Technologies To Use


In an ideal world, we’d use usually one customer database technology. Unfortunately, we’ll have to use two:


  • IndexedDB

    This is a customary for client-side storage and a usually choice accessible on Firefox and Internet Explorer4.

  • WebSQL

    This is a deprecated antecedent to WebSQL and a usually choice accessible on stream versions of iOS5 (although iOS 8 will finally give us IndexedDB).

Veterans of a offline-first universe competence now be thinking, “But we could usually use localStorage6, that has a advantages of a many easier API, and we wouldn’t need to worry about a complexity of regulating both IndexedDB and WebSQL.” While that is technically true, localStorage has series of problems7, a many critical of that is that a volume of storage space accessible with it is significantly reduction than IndexedDB and WebSQL.


Luckily, while we’ll need to use both, we’ll usually need to consider about IndexedDB. To support WebSQL, we’ll use an IndexedDB polyfill8. This will keep a formula purify and easy to maintain, and once all browsers that we caring about support IndexedDB natively, we can simply undo a polyfill.


Note: If you’re starting a new plan and are determining either to use IndexedDB or WebSQL, we strongly disciple regulating IndexedDB and a polyfill. In my opinion, there is no reason to write any new formula that integrates with WebSQL directly.


I’ll go by all of a stairs regulating Google Chrome (and a developer tools), though there’s no reason because we couldn’t rise this focus regulating any other complicated browser.


1. Scaffolding The Application And Opening A Database


We will emanate a following files in a singular directory:


  • /index.html

  • /application.js

  • /indexeddb.shim.min.js

  • /styles.css

  • /offline.appcache

/index.html


!DOCTYPE html
html
head
integrate rel='stylesheet' href='./styles.css' type='text/css' media='all' /
/head
body
h1Example: Todo/h1
form
submit placeholder="Type something" /
/form
ul
/ul
book src="./indexeddb.shim.min.js"/script
book src="./application.js"/script
/body
/html

Nothing startling here: usually a customary HTML web page, with an submit margin to supplement to-do items, and an dull unordered list that will be filled with those items.


/indexeddb.shim.min.js


Download a essence of a minified IndexedDB polyfill9, and put it in this file.


/styles.css


body 
margin: 0;
padding: 0;
font-family: helvetica, sans-serif;


*
box-sizing: border-box;


h1
padding: 18px 20px;
margin: 0;
font-size: 44px;
border-bottom: plain 1px
line-height: 1em;


form
padding: 20px;
border-bottom: plain 1px


input
width: 100%;
padding: 6px;
font-size: 1.4em;


ul
margin: 0;
padding: 0;
list-style: none;


li
padding: 20px;
border-bottom: plain 1px
cursor: pointer;


Again, this should be utterly familiar: usually some elementary styles to make a to-do list demeanour tidy. You competence select not to have any styles during all or emanate your own.


/application.js


(function() 

// 'global' non-static to store anxiety to a database
var db;

databaseOpen(function()
alert("The database has been opened");
);

duty databaseOpen(callback)
// Open a database, mention a name and version
var chronicle = 1;
var ask = indexedDB.open('todos', version);

request.onsuccess = function(e)
db = e.target.result;
callback();
;
request.onerror = databaseError;


duty databaseError(e)
console.error('An IndexedDB blunder has occurred', e);


());

All this formula does is emanate a database with indexedDB.open and afterwards uncover a user an out-of-date warning if it is successful. Every IndexedDB database needs a name (in this case, todos) and a chronicle series (which I’ve set to 1).


To check that it’s working, open a focus in a browser, open adult “Developer Tools” and click on a “Resources” tab.


In a Resources panel, we can check either it's working.
In a “Resources” panel, we can check either it’s working.

By clicking on a triangle subsequent to “IndexedDB,” we should see that a database named todos has been created.


2. Creating The Object Store


Like many database formats that we competence be informed with, we can emanate many tables in a singular IndexedDB database. These tables are called “objectStores.” In this step, we’ll emanate an intent store named todo. To do this, we simply supplement an eventuality listener on a database’s upgradeneeded event.


The information format that we will store to-do equipment in will be JavaScript objects, with dual properties:


  • timeStamp

    This timestamp will also act as a key.

  • text

    This is a content that a user has entered.

For example:


 timeStamp: 1407594483201, text: 'Wash a dishes' 

Now, /application.js looks like this (the new formula starts during request.onupgradeneeded):


(function() 

// 'global' non-static to store anxiety to a database
var db;

databaseOpen(function()
alert("The database has been opened");
);

duty databaseOpen(callback)
// Open a database, mention a name and version
var chronicle = 1;
var ask = indexedDB.open('todos', version);

// Run migrations if necessary
request.onupgradeneeded = function(e)
db = e.target.result;
e.target.transaction.onerror = databaseError;
db.createObjectStore('todo', keyPath: 'timeStamp' );
;

request.onsuccess = function(e)
db = e.target.result;
callback();
;
request.onerror = databaseError;


duty databaseError(e)
console.error('An IndexedDB blunder has occurred', e);


());

This will emanate an intent store keyed by timeStamp and named todo.


Or will it?


Having updated application.js, if we open a web app again, not a lot happens. The formula in onupgradeneeded never runs; try adding a console.log in a onupgradeneeded callback to be sure. The problem is that we haven’t incremented a chronicle number, so a browser doesn’t know that it needs to run a ascent callback.


How to Solve This?


Whenever we supplement or mislay intent stores, we will need to increment a chronicle number. Otherwise, a structure of a information will be opposite from what your formula expects, and we risk violation a application.


Because this focus doesn’t have any genuine users yet, we can repair this another way: by deletion a database. Copy this line of formula into a “Console,” and afterwards modernise a page:


indexedDB.deleteDatabase('todos');

After refreshing, a “Resources” mirror of “Developer Tools” should have altered and should now uncover a intent store that we added:


The Resources quarrel should now uncover a intent store that was added.
The “Resources” quarrel should now uncover a intent store that was added.

3. Adding Items


The subsequent step is to capacitate a user to supplement items.


/application.js


Note that I’ve wanting a database’s opening code, indicated by ellipses (…) below:


(function() 

// Some tellurian variables (database, references to pivotal UI elements)
var db, input;

databaseOpen(function()
submit = document.querySelector('input');
document.body.addEventListener('submit', onSubmit);
);

duty onSubmit(e)
e.preventDefault();
databaseTodosAdd(input.value, function()
input.value = '';
);


[…]

duty databaseTodosAdd(text, callback)
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var ask = store.put(
text: text,
timeStamp: Date.now()
);

request.onsuccess = function(e)
callback();
;
request.onerror = databaseError;


());

We’ve combined dual pieces of formula here:


  • The eventuality listener responds to any submit event, prevents that event’s default movement (which would differently modernise a page), calls databaseTodosAdd with a value of a input element, and (if a intent is successfully added) sets a value of a input component to be empty.

  • A duty named databaseTodosAdd stores a to-do intent in a internal database, along with a timestamp, and afterwards runs a callback.

To exam that this works, open adult a web app again. Type some difference into a input component and press “Enter.” Repeat this a few times, and afterwards open adult “Developer Tools” to a “Resources” add-on again. You should see a equipment that we typed now seem in a todo intent store.


03-step3-dev-tools-opt-50010
After adding a few items, they should seem in a todo intent store. (View vast version11)

4. Retrieving Items


Now that we’ve stored some data, a subsequent step is to work out how to collect it.


/application.js


Again, a ellipses prove formula that we have already implemented in stairs 1, 2 and 3.


(function() 

// Some tellurian variables (database, references to pivotal UI elements)
var db, input;

databaseOpen(function()
submit = document.querySelector('input');
document.body.addEventListener('submit', onSubmit);
databaseTodosGet(function(todos)
console.log(todos);
);
);

[…]

duty databaseTodosGet(callback)
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');

// Get all in a store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);

// This fires once per quarrel in a store. So, for simplicity,
// collect a information in an array (data), and pass it in the
// callback in one go.
var information = [];
cursorRequest.onsuccess = function(e)
var outcome = e.target.result;

// If there's data, supplement it to array
if (result)
data.push(result.value);
result.continue();

// Reach a finish of a data
else
callback(data);

;


());

After a database has been initialized, this will collect all of a to-do equipment and outlay them to a “Developer Tools” console.


Notice how a onsuccess callback is called after any intent is retrieved from a intent store. To keep things simple, we put any outcome into an array named data, and when we run out of formula (which happens when we’ve retrieved all of a items), we call a callback with that array. This proceed is simple, though other approaches competence be some-more efficient.


If we free a focus again, a “Developer Tools” console should demeanour a bit like this:


The console after reopening a application
The console after reopening a application

5. Displaying Items


The subsequent step after retrieving a equipment is to arrangement them.


/application.js


(function() {

// Some tellurian variables (database, references to pivotal UI elements)
var db, input, ul;

databaseOpen(function()
submit = document.querySelector('input');
ul = document.querySelector('ul');
document.body.addEventListener('submit', onSubmit);
databaseTodosGet(renderAllTodos);
);

duty renderAllTodos(todos)
var html = '';
todos.forEach(function(todo)
html += todoToHtml(todo);
);
ul.innerHTML = html;


duty todoToHtml(todo)
lapse '
  • '+todo.text+'
  • ';


    […]

    All we’ve combined are a integrate of really elementary functions that describe a to-do items:


    • todoToHtml

      This takes a todos intent (i.e. a elementary JavaScript intent that we tangible earlier).

    • renderAllTodos

      This takes an array of todos objects, translates them to an HTML fibre and sets a unordered list’s innerHTML to it.

    Finally, we’re during a indicate where we can indeed see what a focus is doing though carrying to demeanour in “Developer Tools”! Open adult a app again, and we should see something like this:


    Your focus in a front-end view12
    Your focus in a front-end perspective (View vast version13)

    But we’re not finished yet. Because a focus usually displays equipment when it launches, if we supplement any new ones, they won’t seem unless we modernise a page.


    6. Displaying New Items


    We can repair this with a singular line of code.


    /application.js


    The new formula is usually a line databaseTodosGet(renderAllTodos);.


    […]

    function onSubmit(e)
    e.preventDefault();
    databaseTodosAdd(input.value, function()
    // After new equipment have been added, re-render all items
    databaseTodosGet(renderAllTodos);
    input.value = '';
    );


    […]

    Although this is really simple, it’s not really efficient. Every time we supplement an item, a formula will collect all equipment from a database again and describe them on screen.


    7. Deleting Items


    To keep things as elementary as possible, we will let users undo equipment by clicking on them. (For a genuine application, we would substantially wish a dedicated “Delete” symbol or uncover a dialog so that an intent doesn’t get deleted accidentally, though this will be excellent for a small prototype.)


    To grasp this, we will be a small hacky and give any intent an ID set to a timeStamp. This will capacitate a click eventuality listener, that we will supplement to a document’s body, to detect when a user clicks on an intent (as against to anywhere else on a page).


    /application.js


    (function() 

    // Some tellurian variables (database, references to pivotal UI elements)
    var db, input, ul;

    databaseOpen(function()
    submit = document.querySelector('input');
    ul = document.querySelector('ul');
    document.body.addEventListener('submit', onSubmit);
    document.body.addEventListener('click', onClick);
    databaseTodosGet(renderAllTodos);
    );

    duty onClick(e)

    // We'll assume that any component with an ID
    // charge is a to-do item. Don't try this during home!
    if (e.target.hasAttribute('id'))

    // Because a ID is stored in a DOM, it becomes
    // a string. So, we need to make it an integer again.
    databaseTodosDelete(parseInt(e.target.getAttribute('id'), 10), function()

    // Refresh a to-do list
    databaseTodosGet(renderAllTodos);
    );



    […]

    duty todoToHtml(todo)
    lapse 'li id="'+todo.timeStamp+'"'+todo.text+'/li';


    […]

    duty databaseTodosDelete(id, callback)
    var transaction = db.transaction(['todo'], 'readwrite');
    var store = transaction.objectStore('todo');
    var ask = store.delete(id);
    request.onsuccess = function(e)
    callback();
    ;
    request.onerror = databaseError;


    ());

    We’ve done a following enhancements:


    • We’ve combined a new eventuality handler (onClick) that listens to click events and checks either a aim component has an ID attribute. If it has one, afterwards it translates that behind into an integer with parseInt, calls databaseTodosDelete with that value and, if a intent is successfully deleted, re-renders a to-do list following a same proceed that we took in step 6.

    • We’ve extended a todoToHtml duty so that any to-do intent is outputted with an ID attribute, set to a timeStamp.

    • We’ve combined a new function, databaseTodosDelete, that takes that timeStamp and a callback, deletes a intent and afterwards runs a callback.

    Our to-do app is fundamentally feature-complete. We can supplement and undo items, and it works in any browser that supports WebSQL or IndexedDB (although it could be a lot some-more efficient).


    Almost There


    Have we indeed built an offline-first to-do app? Almost, though not quite. While we can now store all information offline, if we switch off your device’s Internet tie and try loading a application, it won’t open. To repair this, we need to use a HTML5 Application Cache14.


    Warning


    • While HTML5 Application Cache works pretty good for a elementary single-page focus like this, it doesn’t always. Thoroughly investigate how it works15 before deliberation either to request it to your website.

    • Service Worker16 competence shortly reinstate HTML5 Application Cache, nonetheless it is not now serviceable in any browser, and conjunction Apple nor Microsoft have publicly committed to ancillary it.

    8. Truly Offline


    To capacitate a focus cache, we’ll supplement a manifest charge to a html component of a web page.


    /index.html


    !DOCTYPE html
    html manifest="./offline.appcache"
    […]

    Then, we’ll emanate a perceptible file, that is a elementary content record in that we carelessly mention a files to make accessible offline and how we wish a cache to behave.


    /offline.appcache


    CACHE MANIFEST
    ./styles.css
    ./indexeddb.shim.min.js
    ./application.js

    NETWORK:
    *

    The territory that starts CACHE MANIFEST tells a browser a following:


    • When a focus is initial accessed, download any of those files and store them in a focus cache.

    • Any time any of those files are indispensable from afterwards on, bucket a cached versions of a files, rather than redownload them from a Internet.

    The territory that starts NETWORK tells a browser that all other files contingency be downloaded uninformed from a Internet any time they are needed.


    Success!


    We’ve combined a discerning and elementary to-do app17 that works offline and that runs in all vital complicated browsers, interjection to both IndexedDB and WebSQL (via a polyfill).


    Resources


    • “A Simple TODO list Using HTML5 IndexedDB18,” Paul Kinlan, HTML5 Rocks

      The app we usually grown builds on Kinlan’s article.

    • “IndexedDB Polyfill Over WebSQL19,” Parashuram Narasimhan

    • “Application Cache Is a Douchebag20,” Jake Archibald, A List Apart

      A extensive beam to what’s wrong with a HTML5 Application Cache.

    • Is Service Worker Ready?21

      Track a doing of Service Worker opposite vital web browsers.

    (al, ml, il)


    Footnotes


    1. 1 http://smashingconf.com/workshops/matthew-andrews

    2. 2 https://matthew-andrews.github.io/offline-todo/

    3. 3 https://github.com/matthew-andrews/offline-todo

    4. 4 http://caniuse.com/indexeddb

    5. 5 http://caniuse.com/sql-storage

    6. 6 http://caniuse.com/namevalue-storage

    7. 7 https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/

    8. 8 https://github.com/axemclion/IndexedDBShim

    9. 9 https://raw.githubusercontent.com/matthew-andrews/offline-todo/gh-pages/indexeddb.shim.min.js

    10. 10 http://www.smashingmagazine.com/wp-content/uploads/2014/08/03-step3-dev-tools-opt.jpg

    11. 11 http://www.smashingmagazine.com/wp-content/uploads/2014/08/03-step3-dev-tools-opt.jpg

    12. 12 http://www.smashingmagazine.com/wp-content/uploads/2014/08/05-step5-app-opt.jpg

    13. 13 http://www.smashingmagazine.com/wp-content/uploads/2014/08/05-step5-app-opt.jpg

    14. 14 https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache

    15. 15 http://alistapart.com/article/application-cache-is-a-douchebag

    16. 16 http://www.serviceworker.org/

    17. 17 https://matthew-andrews.github.io/offline-todo/

    18. 18 http://www.html5rocks.com/en/tutorials/indexeddb/todo/

    19. 19 http://nparashuram.com/IndexedDBShim/

    20. 20 http://alistapart.com/article/application-cache-is-a-douchebag

    21. 21 https://jakearchibald.github.io/isserviceworkerready/

    ↑ Back to topShare on Twitter



    Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL

    Nenhum comentário:

    Postar um comentário