Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL
- By Matt Andrews
- September 2nd, 2014
- 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.
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:
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), callsdatabaseTodosAdd
with a value of ainput
element, and (if a intent is successfully added) sets a value of ainput
component to be empty. - A duty named
databaseTodosAdd
stores a to-do intent in a internal database, along with a timestamp, and afterwards runs acallback
.
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.
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:
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 atodos
intent (i.e. a elementary JavaScript intent that we tangible earlier).renderAllTodos
This takes an array oftodos
objects, translates them to an HTML fibre and sets a unordered list’sinnerHTML
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:
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 withparseInt
, callsdatabaseTodosDelete
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 atimeStamp
. - We’ve combined a new function,
databaseTodosDelete
, that takes thattimeStamp
and acallback
, deletes a intent and afterwards runs acallback
.
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 http://smashingconf.com/workshops/matthew-andrews
- 2 https://matthew-andrews.github.io/offline-todo/
- 3 https://github.com/matthew-andrews/offline-todo
- 4 http://caniuse.com/indexeddb
- 5 http://caniuse.com/sql-storage
- 6 http://caniuse.com/namevalue-storage
- 7 https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/
- 8 https://github.com/axemclion/IndexedDBShim
- 9 https://raw.githubusercontent.com/matthew-andrews/offline-todo/gh-pages/indexeddb.shim.min.js
- 10 http://www.smashingmagazine.com/wp-content/uploads/2014/08/03-step3-dev-tools-opt.jpg
- 11 http://www.smashingmagazine.com/wp-content/uploads/2014/08/03-step3-dev-tools-opt.jpg
- 12 http://www.smashingmagazine.com/wp-content/uploads/2014/08/05-step5-app-opt.jpg
- 13 http://www.smashingmagazine.com/wp-content/uploads/2014/08/05-step5-app-opt.jpg
- 14 https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache
- 15 http://alistapart.com/article/application-cache-is-a-douchebag
- 16 http://www.serviceworker.org/
- 17 https://matthew-andrews.github.io/offline-todo/
- 18 http://www.html5rocks.com/en/tutorials/indexeddb/todo/
- 19 http://nparashuram.com/IndexedDBShim/
- 20 http://alistapart.com/article/application-cache-is-a-douchebag
- 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