Repository: matthew-andrews/workshop-making-it-work-offline
Branch: master
Commit: a5d9c55362d3
Files: 136
Total size: 277.6 KB
Directory structure:
gitextract_2g5j4x9v/
├── .gitignore
├── .jshintrc
├── 01-introduction/
│ ├── README.md
│ ├── dysfunctional-family.md
│ ├── how.md
│ └── why.md
├── 02-new-apis/
│ ├── README.md
│ ├── fetch.md
│ └── solutions.md
├── 03-offline-todo/
│ ├── 01-scaffolding/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── offline.appcache
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 02-opening-a-database/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 03-using-dev-tools/
│ │ └── README.md
│ ├── 04-creating-object-stores/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 05-review-window-indexeddb/
│ │ └── README.md
│ ├── 06-adding-data/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 07-getting-data/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 08-rendering-todos/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 09-deleting-data/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 10-review-requests-transactions/
│ │ └── README.md
│ ├── 11-appcache/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── offline.appcache
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 12-appcache-gotcha-1/
│ │ └── README.md
│ ├── 13-success/
│ │ └── README.md
│ └── README.md
├── 04-offline-todo-with-sync/
│ ├── 01-architecture/
│ │ └── README.md
│ ├── 02-mark-for-deletion/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── index.html
│ │ ├── offline.appcache
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 03-adding-ajax/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── fetch.js
│ │ ├── index.html
│ │ ├── offline.appcache
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 04-synchronize/
│ │ ├── README.md
│ │ ├── application.js
│ │ ├── fetch.js
│ │ ├── index.html
│ │ ├── offline.appcache
│ │ ├── promise.js
│ │ └── styles.css
│ ├── 05-success/
│ │ └── README.md
│ └── README.md
├── 05-offline-news/
│ ├── 01-scaffolding/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── public/
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 02-single-multi-page/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── public/
│ │ ├── application.js
│ │ ├── fetch.js
│ │ ├── promise.js
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 03-hacking-appcache/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── public/
│ │ ├── application.js
│ │ ├── fetch.js
│ │ ├── iframe.html
│ │ ├── promise.js
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 04-more-hacking-appcache/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── public/
│ │ ├── appcache.js
│ │ ├── application.js
│ │ ├── fetch.js
│ │ ├── iframe.html
│ │ ├── iframe.js
│ │ ├── promise.js
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 05-success/
│ │ └── README.md
│ └── README.md
├── 06-offline-news-with-service-worker/
│ ├── 01-scaffolding/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── public/
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 02-registering-a-service-worker/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── application.js
│ │ │ ├── service-worker.js
│ │ │ ├── styles.css
│ │ │ └── templates.js
│ │ ├── styles.css
│ │ └── templates.js
│ ├── 03-service-worker-caches/
│ │ └── README.md
│ ├── 04-success/
│ │ └── README.md
│ └── README.md
├── 07-dexie/
│ └── README.md
├── 08-success/
│ └── README.md
├── README.md
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/node_modules/
================================================
FILE: .jshintrc
================================================
{
"browser": true,
"laxbreak": true,
"debug": true
}
================================================
FILE: 01-introduction/README.md
================================================
# Introduction
Today we're going to explore all the technologies available in browsers today that can be brought together to build **offline websites**.
## Me
- Core dev FT web app, Economist HTML5 app (both successful offline-first applications)
- Author of tutorial series: [How to make an offline HTML5 web app, FT style](http://labs.ft.com/2012/08/basic-offline-html5-web-app/)
- [Building A Simple Cross-Browser Offline To-Do List With IndexedDB And WebSQL on Smashing Magazine](http://www.smashingmagazine.com/2014/09/02/building-simple-cross-browser-offline-todo-list-indexeddb-websql/)
## You
- Introduction yourselves
- How would you describe your JavaScript skills?
- What do you hope to gain from today? (Mostly to learn about techniques to use today, or one that will be usable in a few month's time?)
## Course structure
The structure of this course will be very practical and hands-on focused on building prototypes, and will typically follow this pattern:
- Hack something together that works.
- Talk about why it works (explaining any browser APIs that we've used that may be unfamiliar).
- Discuss why it's inefficient or inelegant and then improve on it.
- There are exercises at the end of many sections if you finish the core material quickly.
- Schedule for the day: 9 start, 10.30 break (½ hour), 12.30 lunch (1 hour), 15.00 break (½ hour), 17.00 the end.
## What you should get out of today's course
- A few working prototypes.
- All the prototypes are on my GitHub (publicly so if you have any problems with any after today - please feel free to raise issues there and I will try to find time to help)
## Group chat
http://tlk.io/
---
[← Back to *contents*](https://github.com/matthew-andrews/workshop-making-it-work-offline) | [Continue to *why* →](./why.md)
================================================
FILE: 01-introduction/dysfunctional-family.md
================================================
# Meet the dysfunctional family
## Quiz
- Can you name all the offline storage technologies that exist in browsers today?
- What are the differences between them?
- What sort of data can you store offline?
- Which technologies allow you to shared data between domain names?
- What are the rules for offline technologies that don't share data across domains? How is https affected this? How about subfolders?
- If you built an offline web application and then migrated to https what potential problems might you need to consider?
## Choices
- Cookies
- Local Storage / Session Storage
- WebSQL
- IndexedDB
- Browser Cache
- Application Cache
- Service Worker
### Cookies
- Basically work on *every* browser
- Only supports very small pieces of information
- Only supports strings
- Sent on every http request
- A bit fiddly to use without a helper library/function
### Local Storage
- Works on a lot of browsers
- Supports more data than cookies
- But still only a small amount of space available on most browsers
- Very *fast*
- Only supports strings
- Not sent on HTTP request (only available client side)
- Synchronous, which can cause performance issues
### Local database (WebSQL, IndexedDB)
- One or the other (or both) work on most modern browsers
- Supports more data than local storage (typically around ~50mb, but can be much bigger on Desktop)
- Supports complex data structures and multiple data types
- Built in mechanism for migrations
- Slower than Local Storage
- Not sent on http request (only available client side)
- Asynchronous
### Browser Cache
- Works on every browser
- *Sometimes* does work offline (which can be confusing when trying to test AppCache issues) but…
- It can't be relied on
### Application Cache
- Works on most (90%+) modern browsers
- Best suited for storing application *code*
- The only option to reliably load a website from nothing offline
- Can storage a significant amount of data (50mb+)
- Very difficult use without causing unintended consequences for most websites
### Service Worker
- Application Cache's replacement, but unlikely to be possible to polyfill
- Not available in any browser (bits of it work on some cutting-edge browsers with feature flags switched on)
- Safari / Microsoft have not announced any intention to implement it
- Will only work on https
- Promises to be less difficult to use and cause fewer side effects than Application Cache
Note. There was another storage technology relevant to offline websites called the HTML5 FileSystem API but as of April 2014, [that spec is dead](http://www.html5rocks.com/en/tutorials/file/filesystem/)
#### Q. Which technology do you need to make an offline-first web application?
A. Most, if not all, of them.
For today's browsers I advocate the following approach and the reasons why will hopefully become clear over the course of the workshop:
### Guidance
- **Delivering the application** - we will use the Application Cache to store the JavaScript, HTML and CSS to launch our application. We will not store any content here.
- **Storing content** - we will use a clientside database technology, either IndexedDB or WebSQL, to store our content. We will not store any application assets here.
(Note: when we are able to use Service Worker we will be able to take a more flexible approach)
---
[← Back to *how*](./how.md) | [Continue to *a promising start* →](../02-new-apis)
================================================
FILE: 01-introduction/how.md
================================================
# How
There are four distinct problems to solve*:
1. Delivering the application
2. Storing data
3. Syncing data
4. Third parties
## Delivering the application
- How do we efficiently cache enough of the pieces that make up a website for it to be useful offline?
- How do we guarantee that the browser won't delete them?
## Storing content
- How do we store content downloaded to or created on users' devices?
## Synchronizing content
- How do we ensure the data on users' devices are kept in sync with content on the server?
- What if there are conflicts?
## Third parties
One of the great things about the web is how easy it is to bring multiple products, widgets and services together onto a single page.
Offline technologies have only just started to be developed - using them enable us to create incredible user experience that previously would have been impossible on the web - but, also by using them, you will discover many of the third party components you relied on to build websites don't work well offline.
### What happens to Google Analytics data?
Basically, nothing.
- For each tracking event Google Analytics will try to send a tracking event to the server.
- As the device is offline that tracking event will fail.
- Google Analytics will not retry or store that data locally for retry later.
- **If you want to keep that data, you will have to implement the storing and resending of it yourself.**
- Even, even if you _did_ store that data and sent it later manually yourself, as the web requires your website to be open in order to make http requests that data might only be sent after a very long time after it was recorded - _perhaps even weeks or months_.
\* [Adapted from Caolan McMahon's presentation on the status of offline web](http://www.infoq.com/presentations/status-web-offline)
[← Back to *why*](./why.md) | [Continue to *meet the dysfunctional family* →](./dysfunctional-family.md)
================================================
FILE: 01-introduction/why.md
================================================
# Why
We live in a bubble. We work all day on high-spec machines, plugged into high-speed connections and use the latest cut of our favourite web browser. The real world isn't like that.
Taking just network connection as one example. It's no longer as simple as online-versus-offline.
- Connected to a high speed fibre connection via wifi every few minutes for less than a minute (the London underground)
- In a region where the cost of using the local network is so high the user has _chosen_ not to be connected.
- Connected to a weak wireless signal where only a small number of requests are successful.
- Using a connection where only a subset of websites are blocked (an airport or in China - it's possible to make your website nearly unusable in China by poorly integration of a Facebook widget)
- Device prefer to connect to wifi hotspots that require logins, even when they have perfectly good mobile connections.
## People need the web offline
Whilst the web is slowly reaching ever more remote locations - wifi on planes and underground - the amount by which we depend on it is growing even faster.
As developers we need to do a better job.
[← Back to *introduction*](./README.md) | [Continue to *how* →](./how.md)
================================================
FILE: 02-new-apis/README.md
================================================
# Promises
## What is a Promise?
> The Promise interface **represents a proxy for a value not necessarily known when the promise is created**. It allows you to associate handlers to an asynchronous action's eventual success or failure. **This lets asynchronous methods return values like synchronous methods**: instead of the final value, the asynchronous method returns a promise of having a value at some point in the future.
>
> A pending promise can become either **fulfilled with a value**, or **rejected with a reason**. When either of these happens, the associated handlers queued up by a promise's then method are called. (If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.)
>
> As the `Promise.prototype.then` and `Promise.prototype.catch` methods return promises, they can be chained—an operation called composition.
\- Source: [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), emphasis mine.
In short, Promises are a way to organise asynchronous logic.
## Lots of Promise libraries
Or similar libraries that achieve the same thing:-
- Q
- async
- when
- RSVP
But the implementation we're going to use is **ES6 Promise**. It's a browser standard that is already available and also works in Node.
## Why are we talking about it?
We're going to need Promises because new browser APIs, such as **Service Worker** which we will cover later, are built on top of them. By covering them early we will be able to use them in all of our prototypes and exercises so that hopefully they will be second nature (if they aren't already) by the end of the day.
Take for example messy logic that **POSTs** two todos to the server and opens an alert when both have succeeded (or an alert if one fails).
```js
request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-first-todo',
text: 'Buy some bread'
},
method: 'POST'
}, function(err) {
var firstSuccess = !!err;
request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-second-todo',
text: 'Buy some bread'
},
method: 'POST'
}, function(err) {
var secondSuccess = !!err;
if (firstSuccess && secondSuccess) {
alert('success!');
} else {
alert('something failed');
}
});
});
```
## What's wrong with this code?
- Difficult to follow
- The second request doesn't start until the first is complete (they could be running at the same time)
- Error handling is messy
## What are we aiming for?
If we didn't need to worry about the asynchronous ajax request we'd probably write something like this:-
```js
try {
var responses = [];
responses[0] = request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-first-todo',
text: 'Buy some bread'
},
method: 'POST'
});
responses[1] = request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-second-todo',
text: 'Buy some bread'
},
method: 'POST'
});
alert('success!');
} catch(e) {
alert('something failed');
}
```
### Promises let us write code like this.
```js
Promise.all(
[
request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-first-todo',
text: 'Buy some bread'
},
method: 'POST'
}),
request({
url: "https://offline-todo-api.herokuapp.com/todos",
body: {
_id: 'my-second-todo',
text: 'Buy some bread'
},
method: 'POST'
})
]
).then(function success(responses) {
alert('success!');
}, function failure() {
alert('something failed');
});
```
### Nice things about promises
- Abstracts the complexity of dealing with asynchronous logic - lets you write performant but (relatively) simple code.
- Allows you to deal with errors in asynchronous libraries in a sensible way.
### Maybe not so nice things about promises
- Most browser methods were made pre-promises and so need wrapping to be useful.
- Cannot pipe data. (eg. it might be nice for ajax request that returned a lot of data to start passing that data to the application as soon as data starts being received. With promises you must wait until *all the data arrives* before your application will get to see *any of it*.
### Like them or not they're here to stay
- Browsers now support them natively, Node v0.11 ships with them built in
- Old familiar APIs (e.g. XMLHttpRequest) may be replaced by new shiny promising versions (e.g. request)
- They are a fundamental building block to ServiceWorkers, which will be a key component of today's workshop
- Many, many languages support promises (even [ReactPHP](https://github.com/reactphp/promise)) or equivalents (sometimes called futures - promises were actually first proposed in 1976)
### Getting setup for Promises
```
mkdir promises
cd promises
echo '{}' >> package.json
npm install --save es6-promise
```
Then, in that folder create a file called `test.js` with the following contents:-
```js
require('es6-promise').polyfill();
console.log(Promise);
```
Hopefully the output will be:-
```
{ [Function: Promise]
all: [Function: all],
race: [Function: race],
resolve: [Function: resolve],
reject: [Function: reject] }
```
### Exercises
Create a function that returns a promise that successfully resolves after 1 second with `setTimeout`
---
Create a function that returns a promise that rejects a promise after half a second with `setTimeout`
---
Explain the difference between this:-
```js
promise
.then(function() {
alert("Success!");
}, function() {
alert("Fail!");
});
```
And this:-
```js
promise
.then(function() {
alert("Success!");
})
.catch(function() {
alert("Fail!");
});
```
---
Explain the difference between this:-
```js
Promise.all([promise1, promise2])
.then(function() {
alert("Success!");
});
```
And this:-
```js
Promise.race([promise1, promise2])
.then(function() {
alert("Success!");
});
```
---
When might race be useful in an offline web application?
---
[← Back to *meet the dysfunctional family*](../01-introduction/dysfunctional-family.md) | [Continue to *Fetch* →](./fetch.md)
================================================
FILE: 02-new-apis/fetch.md
================================================
# Fetch
By now I hope I've convinced you that ES6 Promises are the one and only solution to asynchronous flow and that — if you're anything like me — will no longer be able to look at a web api built in the Pre-Promise era without cringing.
Let's talk about ajax.
```js
function success(response) { /* do something */ }
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4) {
if (request.status === 200) {
success(request.response);
}
}
};
request.open('https://offline-news-api.herokaupp.com/stories');
request.send();
```
OK, to be fair nobody actually writes this. They'll normally do something like:-
```js
function success(response) { /* do something */ }
$.get('https://offline-news-api.herokaupp.com/stories', success);
```
Which is much better but what's the point of a standard API that is so difficult to use nobody uses it?
What would be nice if browsers shipped with a simple, _Promise_ based API for _fetching_ ajax requests…
```js
fetch('https://offline-news-api.herokaupp.com/stories')
.then(success);
```
[Luckily other people think this is a good idea too](http://fetch.spec.whatwg.org/), and is the mechanism you *must* use within Service Worker, which we'll cover later. Oddly though Chrome hasn't made this available in the front end, however the lovely people at [GitHub](https://www.github.com/github/fetch) have built a polyfill for modern browsers.
## Gotchas
The Fetch API builds on top of the W3C Streams _as well as_ Promises. This is good as there are times where you won't want to wait for an ajax request to finish loading before you want to use the data it's downloading — in these cases you can pipe that raw stream of downloaded data into different bits of your application. JavaScript streams are out of scope for this workshop but you can read more about them on the [Stream Handbook](https://github.com/substack/stream-handbook).
Warning: the Fetch Polyfill _only_ implements the Promise part of the API, *not* the Streams part.
The reason why I mention it is that the `fetch` API returns a Promise that resolves with a Response object that gives you access to *streams* — not an object containing all the data you requested. You need to then convert that into the data format you want. For example:-
```js
fetch('http://mattandre.ws/my-json-file.json')
.then(function(response) {
return response.json();
});
```
## Exercises
- Use the Fetch API to create a, delete a and download all the todos from the API described: https://github.com/matthew-andrews/offline-todo-api.
---
[← Back to *Promises*](./) | [Continue to *offline todo with IndexedDB* →](../03-offline-todo)
================================================
FILE: 02-new-apis/solutions.md
================================================
# Solutions
```js
function successful() {
return new Promise(resolve, reject) {
setTimeout(resolve, 1000);
};
}
```
---
```js
function unsuccessful() {
return new Promise(resolve, reject) {
setTimeout(reject, 500);
};
}
```
---
`then` actually takes two arguments - one for success, one for failure, and `catch(function() {})` is just shorthand for `then(undefined, function() {})`.
So really we are comparing the difference between:-
```js
promise
.then(function() {
alert("Success!");
}, function() {
alert("Fail!");
});
```
And this:-
```js
promise
.then(function() {
alert("Success!");
})
.then(undefined, function() {
alert("Fail!");
});
```
When an error occurs promises _jump forward_ to the next `then` with a rejection callback. So with`.then(funcA, funcB)` either `funcA` or `funcB` will be called (not both) - whereas with `.then(funcA).catch(funcB)` both `funcA` and `funcB` will get called (as they're separate steps).
[Read more detail about error handling in promises on HTML5 rocks](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-error-handling)
---
- `all` resolves when **all** the promises passed into it resolve. If any rejects, it immediately reject - discarding the results of all other promises, regardless of their result.
- `race` resolves **as soon as one** of the promises resolves or rejects.
Race could be useful in an offline web app when retrieving fresh data from a server that is also cached locally.
```js
Promise.race([getFromServer(), getFromCache()])
.then(function() {
// Put some data on screen
});
```
================================================
FILE: 03-offline-todo/01-scaffolding/README.md
================================================
# Scaffolding the application
We will create the following files in a single directory:
- [`/index.html`](./index.html)
- [`/application.js`](./application.js)
- [`/indexeddb.shim.min.js`](./indexeddb.shim.min.js)
- [`/promise.js`](./promise.js)
- [`/styles.css`](./styles.css)
- [`/offline.appcache`](./offline.appcache)
##### `/index.html`
```html
Example: Todo
```
Nothing surprising here: just a standard HTML web page, with an input field to add to-do items, and an empty unordered list that will be filled with those items.
##### `/indexeddb.shim.min.js`
Download the contents of [the minified IndexedDB polyfill](https://raw.githubusercontent.com/matthew-andrews/offline-todo/gh-pages/indexeddb.shim.min.js), and put it in this file.
##### `/promise.js`
Download the contents of [the minified ES6 Promise polyfill](http://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js), and put it in this file.
##### `/styles.css`
```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: solid 1px #DDD;
line-height: 1em;
}
form {
padding: 20px;
border-bottom: solid 1px #DDD;
}
input {
width: 100%;
padding: 6px;
font-size: 1.4em;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 20px;
border-bottom: solid 1px #DDD;
}
button {
float: right;
}
```
Again, this should be quite familiar: just some simple styles to make the to-do list look tidy. You may choose not to
have any styles at all or create your own.
We will leave `application.js` and `offline.appcache` empty for now and return to them later.
## Quick test
Run any simple static web server, for example [node-static](https://github.com/cloudhead/node-static), in the directory
containing these files and verify the website matches the screenshot below.

Warning: you will need to use `static -c 1` rather than `static` on its own in order to see updated files. Once we've added AppCache, try leaving off the `-c 1` and see what happens when you update files.
---
[← Back to *Offline todo with IndexedDB*](../) | [Continue to *opening a database* →](../02-opening-a-database)
================================================
FILE: 03-offline-todo/01-scaffolding/application.js
================================================
================================================
FILE: 03-offline-todo/01-scaffolding/index.html
================================================
Example: Todo
================================================
FILE: 03-offline-todo/01-scaffolding/offline.appcache
================================================
================================================
FILE: 03-offline-todo/01-scaffolding/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
Example: Todo
================================================
FILE: 03-offline-todo/02-opening-a-database/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
Example: Todo
================================================
FILE: 03-offline-todo/04-creating-object-stores/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
Example: Todo
================================================
FILE: 03-offline-todo/06-adding-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
Example: Todo
================================================
FILE: 03-offline-todo/07-getting-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j'+todo.text+'';
}
[…]
```
- `todoToHtml` takes a single todo and converts it to a HTML string.
- `renderAllTodos` takes an array of todos, converts each of them HTML and then sets the `innerHTML` of the `ul` to those strings concatenated together.
- `refreshView` returns a promise then gets all the todos from the local database and passes them to `renderAllTodos`, which converts the todos to HTML and injects that HTML into the web page.
- We have also enhanced another method, the `onSubmit` event handler, so that the todos re-render as new todos are added.
---
[← Back to *getting data*](../07-getting-data) | [Continue to *deleting data* →](../09-deleting-data)
================================================
FILE: 03-offline-todo/08-rendering-todos/application.js
================================================
(function() {
var db, input, ul;
databaseOpen()
.then(function() {
input = document.querySelector('input');
ul = document.querySelector('ul');
document.body.addEventListener('submit', onSubmit);
})
.then(refreshView);
function onSubmit(e) {
e.preventDefault();
var todo = { text: input.value, _id: String(Date.now()) };
databaseTodosPut(todo)
.then(function() {
input.value = '';
})
.then(refreshView);
}
function refreshView() {
return databaseTodosGet().then(renderAllTodos);
}
function renderAllTodos(todos) {
var html = '';
todos.forEach(function(todo) {
html += todoToHtml(todo);
});
ul.innerHTML = html;
}
function todoToHtml(todo) {
return '
'+todo.text+'
';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet() {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
data.push(result.value);
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
}());
================================================
FILE: 03-offline-todo/08-rendering-todos/index.html
================================================
Example: Todo
================================================
FILE: 03-offline-todo/08-rendering-todos/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j'+todo.text+'';
}
[…]
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
}());
```
We’ve made the following enhancements:
- We’ve added a new event handler (`onClick`) that listens to click events and checks whether the target element has an ID attribute. If it has one it calls databaseTodosDelete with that value and, if the item is successfully deleted, re-renders the to-do list following the same approach that we took in step 6.
- We’ve enhanced the `todoToHtml` function so that every to-do item is outputted with a delete button with an ID attribute set to its `_id`.
- We’ve added two new database functions:
- `databaseTodosDelete`, which takes a `todo` (well, a javascript object with an `_id` property) and returns a promise, deletes the item and then resolves the promise, and
- `databaseTodosGetById`, which takes an `_id` and returns a promise, which resolves with the todo keyed by the given `_id`.
Our to-do app is basically feature-complete. We can add and delete items, and it works in any browser that supports WebSQL or IndexedDB (although it could be a lot more efficient).
---
[← Back to *rendering todos*](../08-rendering-todos) | [Continue to *Review: `IDBRequest` and `IDBTransaction`* →](../10-review-requests-transactions)
================================================
FILE: 03-offline-todo/09-deleting-data/application.js
================================================
(function() {
var db, input, ul;
databaseOpen()
.then(function() {
input = document.querySelector('input');
ul = document.querySelector('ul');
document.body.addEventListener('submit', onSubmit);
document.body.addEventListener('click', onClick);
})
.then(refreshView);
function onClick(e) {
e.preventDefault();
if (e.target.hasAttribute('id')) {
databaseTodosGetById(e.target.getAttribute('id'))
.then(function(todo) {
return databaseTodosDelete(todo);
})
.then(refreshView);
}
}
function onSubmit(e) {
e.preventDefault();
var todo = { text: input.value, _id: String(Date.now()) };
databaseTodosPut(todo)
.then(function() {
input.value = '';
})
.then(refreshView);
}
function refreshView() {
return databaseTodosGet().then(renderAllTodos);
}
function renderAllTodos(todos) {
var html = '';
todos.forEach(function(todo) {
html += todoToHtml(todo);
});
ul.innerHTML = html;
}
function todoToHtml(todo) {
return '
'+todo.text+'
';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet() {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
data.push(result.value);
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
}());
================================================
FILE: 03-offline-todo/09-deleting-data/index.html
================================================
Example: Todo
================================================
FILE: 03-offline-todo/09-deleting-data/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j) storeNames, optional IDBTransactionMode mode = "readonly");
```
- `storeNames` - an array of object stores that your transaction needs to access.
- `mode` - there are two modes for transactions: `readwrite` or `readonly`.
#### Warning
Use `transaction.oncomplete` and not `request.onsuccess` as the point where you know the data has been stored.
### Object Stores
You can think of Object Stores as similar to tables in a SQL database. You can have many object stores per database, and they can contain many objects - indexed by at least one index. Object Stores are accessed via transaction objects. For example:-
```js
var transaction = db.transaction['todo'], 'readonly');
var store = transaction.objectStore('todo');
```
### Requests
Requests are the only objects that you can use to _read_ or _write_ data with IndexedDB. You can get to the results of each request using event handlers on the request objects.
Results are created by the methods on object stores such as `IDBObjectStore#add`, `IDBObjectStore#delete` , `IDBObjectStore#createIndex`, `IDBObjectStore#openCursor`, etc. A special type of `IDBRequest` called `IDBOpenDBRequest` that we covered in the previous review is created by `IDBFactory#open` and `IDBFactory#deleteDatabase`.
## Deleting data
```js
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
```
We can now talk through each line and explain what it does:-
- Create an `IDBTransaction` object with `readwrite` access to the `todo` object store, assign it to `transaction`
- Get an `IDBStore` object that represents the object store `todo` and assign it to `store`
- Create a request to `delete` object with ID `todo._id` from `store`
- When complete, `resolve` the promise
## Getting data
```js
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
if (result) {
data.push(result.value);
result.continue();
} else {
resolve(data);
}
};
```
- As we're only getting data we only need to create a `readonly` transaction.
- Then we get an `IDBStore` object that represents the object store `todo` and assigns it to `store` - in the same way deleting works above.
- The next two lines are concerned with creating a cursor (an object that allows us to walk through the database). The line `IDBKeyRange.lowerBound(0)` simply says start from the beginning of the table.
- As data is pulled out of the table `success` events are fired on the cursor. In each callback we must call `continue` to proceed to the next `result`, and if that `result` is empty we know we've reached the end of the dataset.
#### Cursors
[`IDBCursors`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor) can be used to look up data by their index as well as sort data within an object store. Usage of them is normally out of scope for this course - but here are a few links that introduce the key ideas:
- [`IDBCursor` documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor)
- [How to do some magic with indexedDB](http://www.codeproject.com/Articles/744986/How-to-do-some-magic-with-indexedDB)
### Exercises
- Try changing `transaction.oncomplete` to `request.onsuccess` inside `databaseTodosPut` in your todo app. What happens when you create todos? Why?
- Reverse the order of the todos so that they added to the top, not the bottom, in our demo.
- What would be different if we'd have used `add` instead of `put`? What are the benefits of one over the other?
- Aside from `cursorRequest.lowerBound` what other approaches could you take to retrieving all the data from a table?
- _Bonus:_ Write code that selects items from an object store that instead of waiting for all of them to come back before rendering those items onto the page, immediately render each one. What approaches or browser APIs instead of Promises could be more appropriate to implement this?
- _Bonus:_ Support the ability to update existing items on our todo list.
---
[← Back to *deleting todos*](../09-deleting-todos) | [Continue to *truly offline* →](../11-appcache)
================================================
FILE: 03-offline-todo/11-appcache/README.md
================================================
# Truly offline
Have we actually built an offline-first to-do app? Almost, but not quite. While we can now store all data offline, if you switch off your device’s Internet connection and try loading the application, it won’t open. To fix this, we need to use the **HTML5 Application Cache**.
## Warning
While HTML5 Application Cache works reasonably well for a simple single-page application like this, it doesn’t always. Thoroughly research how it works before considering whether to apply it to your website.
- Service Worker might soon replace HTML5 Application Cache, although it is not currently usable in any browser, and neither Apple nor Microsoft have publicly committed to supporting it.
- To enable the application cache, we’ll add a `manifest` attribute to the `html` element of the web page.
We will cover the essential hacks needed to get AppCache to behave itself later in the course.
##### `/index.html`
```html
[…]
```
Then, we’ll create a manifest file, which is a simple text file in which we crudely specify the files to make available offline and how we want the cache to behave.
##### `/offline.appcache`
```
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js
NETWORK:
*
```
The section that begins `CACHE MANIFEST` tells the browser the following:
- When the application is first accessed, download each of those files and store them in the application cache.
- Any time any of those files are needed from then on, load the cached versions of the files, rather than redownload them from the Internet.
The section that begins `NETWORK` tells the browser that all other files must be downloaded fresh from the Internet every time they are needed.
(Yes, the default behaviour is to say any file not listed in the AppCache should never be downloaded - even if the device is online!?)
## Check
Verify the application works offline by loading it up and then switching off the static web server (or if you're using a webserver disconnect your device from the internet) and press refresh.
---
[← Back to *review: `window.indexedDB`*](../11-review-requests-transactions) | [Continue to *first appcache gotcha* →](../12-appcache-gotcha-1)
================================================
FILE: 03-offline-todo/11-appcache/application.js
================================================
(function() {
var db, input, ul;
databaseOpen()
.then(function() {
input = document.querySelector('input');
ul = document.querySelector('ul');
document.body.addEventListener('submit', onSubmit);
document.body.addEventListener('click', onClick);
})
.then(refreshView);
function onClick(e) {
e.preventDefault();
if (e.target.hasAttribute('id')) {
databaseTodosGetById(e.target.getAttribute('id'))
.then(function(todo) {
return databaseTodosDelete(todo);
})
.then(refreshView);
}
}
function onSubmit(e) {
e.preventDefault();
var todo = { text: input.value, _id: String(Date.now()) };
databaseTodosPut(todo)
.then(function() {
input.value = '';
})
.then(refreshView);
}
function refreshView() {
return databaseTodosGet().then(renderAllTodos);
}
function renderAllTodos(todos) {
var html = '';
todos.forEach(function(todo) {
html += todoToHtml(todo);
});
ul.innerHTML = html;
}
function todoToHtml(todo) {
return '
'+todo.text+'
';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet() {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
data.push(result.value);
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
}());
================================================
FILE: 03-offline-todo/11-appcache/index.html
================================================
Example: Todo
================================================
FILE: 03-offline-todo/11-appcache/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js
NETWORK:
*
================================================
FILE: 03-offline-todo/11-appcache/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
- run on the first- and second-most recent versions of all major desktop and mobile browsers.
## Which technologies to use
In an ideal world, we’d use just one client database technology. Unfortunately, we’ll have to use two:
- **IndexedDB** This is the standard for client-side storage and the [only option available on Firefox and Internet
Explorer](http://caniuse.com/indexeddb).
- **WebSQL** This is the deprecated predecessor to WebSQL and the [only option available on current versions of
iOS](http://caniuse.com/sql-storage) (although iOS 8 will finally give us IndexedDB).
Veterans of the offline-first world might now be thinking, “But we could just use
[localStorage](http://caniuse.com/namevalue-storage), which has the benefits of a much simpler API, and we wouldn’t
need to worry about the complexity of using both IndexedDB and WebSQL.” While that is technically true,
[localStorage has number of problems](https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/),
the most important of which is that the amount of storage space available with it is significantly less than IndexedDB
and WebSQL.
Luckily, while we’ll need to use both, we’ll only need to think about IndexedDB. To support WebSQL, we’ll use an
[IndexedDB polyfill](https://github.com/axemclion/IndexedDBShim). This will keep our code clean and easy to maintain,
and once all browsers that we care about support IndexedDB natively, we can simply delete the polyfill.
**Note:** If you’re starting a new project and are deciding whether to use IndexedDB or WebSQL, I strongly advocate
using IndexedDB and the polyfill. In my opinion, there is no reason to write any new code that integrates with WebSQL
directly.
I’ll go through all of the steps using Google Chrome (and its developer tools), but there’s no reason why you couldn’t
develop this application using any other modern browser.
---
[← Back to *Fetch*](../02-new-apis/fetch.md) | [Start by *scaffolding the application* →](./01-scaffolding)
================================================
FILE: 04-offline-todo-with-sync/01-architecture/README.md
================================================
# Architecture
As with the simple offline todo app, we're going to take lots of shortcuts so that we can focus on covering the key ideas.
## Synchronisation
There are many ways to store data so that it can be synchronized and merged in with other changes that have happened since the last synchronisation. For example, **git** stores changes made 'offline' as sequences of **diffs**.
For our todo application we are going to take the simplest possible route use the following algorithm for synchronising with the server:
- download all todos from the server, load all todos from the local database
- loop through all local todos
- if a todo has been deleted locally, delete it from the server
- if a todo isn't in the array of todos returned from the server, assume it's new and create it on the server
- loop through all the remote todos
- if a todo isn't in the array of todos loaded from the local database, assume it's been created by another client since the last synchronisation and create it locally.
This algorithm is clearly extremely inefficient as it requires downloading all the todos in one go, it requires holding all the todos that exist in memory at once, and it requires walking through all the data to figure out what needs updating. We get away with this because the size of the data we expect for our todos is quite small.
### Delete Corollary 1: we can't just delete todos anymore
By choosing this approach to synchronisation if we continue to directly delete todos from the local database as we are at the moment, the synchronisation algorithm isn't going to be able to distinguish todos that have been previously synced with the server but deleted locallly from todos that have been added by other clients but haven't yet been downloaded.
To work around this instead of deleting todos we will *mark todos for deletion* and only delete them once we are sure that the server has successfully deleted them.
### Delete Corollary 2: the server can never delete todos
Again because of the choice to use this approach to synchronisation in order for clients to be able to distinguish between todos that have been just created locally and not synced (and so exist locally but not on the server) and those that have been deleted by other clients (and so exist locally but not on the server) the server can never *actually* delete todos - only mark them for deletion.
Luckily the API we've chosen to use already [does this already](https://github.com/matthew-andrews/offline-todo-api#delete-todosid---delete-a-todo) and returns a `410 Gone` response for all `POST` and `GET` requests keyed for todos with `_id`s that have already been deleted.
## Appropriate `_id`s
With distributed applications where it is possible to create records on clients independently from the server choosing a sensible way to uniquely identify records turns out to be challenging.
### Why you can't just use auto-increment to create a local ID and let the server fill in a remote ID later:
- Say you create a new todo and it was allocated an ID of 2 whilst your browser is offline.
- You have the application open in two tabs when the browser connects to the internet and both begin syncing at the same time.
- Both simulataneously `POST` the new todo to the server.
- Because the server hasn't yet allocated the todo its permanent unique identifier, it cannot distinguish the two todos from each other and so creates it twice.
You could work around this by requiring all todo text to be unique but that would probably be undesireable for a todo app.
#### Other alternatives
- **Hash the todo and use it as an ID.** Also requires todo text to be unique with the additional problem that once you've created and deleted a todo item you can never re-add a todo item with the same text again.
- **Generate a UUID.** The FT solved a similar problem to solve in their article authoring tool by generating a 36 character unique identifier for all articles with a one-in-a-million-like chance of colliding with another. This would work well but adds complexity to the application.
- **Only support the creation of todos online.** Worthy of a mention and useful for existing systems. Afterall, *some* offline behaviour is a huge improvement on *none*.
- Use the **timestamp** at the point the todo was created. Although there are likely to be bugs collisions if two clients create a todo at precisely the same moment. Also relies on the device's clock being set to some extent.
### Conclusion
There is no simple solution that works in all cases. For real world applications (that use this synchronisation algorithm) I would either choose to use some sort of uuid or to try to find a way to make local ids with remote ids lazily filled tab-safe.
For the purpose of this demonstration prototype we will we use the **timestamp** option because it's by far the simplest.
---
[← Back to *offline todo with IndexedDB and sync*](../) | [Continue to *mark for deletion* →](../02-mark-for-deletion)
================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/README.md
================================================
# Marking items for deletion
Rather than directly deleting todo items we need to change our client side code to *mark todos for deletion*.
To achieve this we are going to need to make the following changes to `application.js`:
- change the `onClick` handler, which deletes individual todos, from directly deleting the todo to setting a new `deleted` property to `true`
- enhance the `databaseTodosGet` method so that todos can be filtered by their deleted status.
- update `refreshView`'s use of `databaseTodosGet` so that it only renders undeleted todos.
- we won't delete the `databaseTodosDelete` method, even though it is now unused as the synchronisation logic that we will implement later will make use of it.
##### `application.js`
```js
[…]
function onClick(e) {
e.preventDefault();
if (e.target.hasAttribute('id')) {
databaseTodosGetById(e.target.getAttribute('id'))
.then(function(todo) {
todo.deleted = true;
return databaseTodosPut(todo);
})
.then(refreshView);
}
}
[…]
function refreshView() {
return databaseTodosGet({ deleted: false }).then(renderAllTodos);
}
[…]
function databaseTodosGet(query) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
if (result) {
if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
data.push(result.value);
}
result.continue();
} else {
resolve(data);
}
};
});
}
[…]
```
Assuming nothing has broken the application should continue working in exactly the same way it did before - you should be able to create and delete todos. The difference is an implementation detail that can be checked by opening up dev tools - where you will see that todos don't actually get deleted any more - they only have a `deleted` flag set to `true` against them. See below:

---
[← Back to *architecture*](../01-architecture) | [Continue to *adding ajax* →](../03-adding-ajax)
================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/application.js
================================================
(function() {
var db, input, ul;
databaseOpen()
.then(function() {
input = document.getElementsByTagName('input')[0];
ul = document.getElementsByTagName('ul')[0];
document.body.addEventListener('submit', onSubmit);
document.body.addEventListener('click', onClick);
})
.then(refreshView);
function onClick(e) {
e.preventDefault();
if (e.target.hasAttribute('id')) {
databaseTodosGetById(e.target.getAttribute('id'))
.then(function(todo) {
todo.deleted = true;
return databaseTodosPut(todo);
})
.then(refreshView);
}
}
function onSubmit(e) {
e.preventDefault();
var todo = { text: input.value, _id: String(Date.now()) };
databaseTodosPut(todo)
.then(function() {
input.value = '';
})
.then(refreshView);
}
function refreshView() {
return databaseTodosGet({ deleted: false }).then(renderAllTodos);
}
function renderAllTodos(todos) {
var html = '';
todos.forEach(function(todo) {
html += todoToHtml(todo);
});
ul.innerHTML = html;
}
function todoToHtml(todo) {
return '
'+todo.text+'
';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet(query) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
data.push(result.value);
}
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
}());
================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/index.html
================================================
Example: Todo
================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./application.js
NETWORK:
*
================================================
FILE: 04-offline-todo-with-sync/02-mark-for-deletion/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j
Example: Todo
```
##### `offline.appcache`
```
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js
NETWORK:
*
```
##### `fetch.js`
Download the contents of the [Fetch API polyfill](https://raw.githubusercontent.com/github/fetch/master/fetch.js), and put it in this file.
## Api wrapper methods
Now we have an ajax library installed and available to us in our application we are next going to implement a few helper methods that mirror the API of the database methods:
##### `application.js`
```js
(function() {
var api = 'https://offline-todo-api.herokuapp.com/todos';
[…]
function serverTodosGet(_id) {
return fetch(api + '/' + (_id ? _id : ''))
.then(function(response) {
return response.json();
});
}
function serverTodosPost(todo) {
return fetch(api, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(todo)
})
.then(function(response) {
if (response.status === 410) throw new Error(response.statusText);
return response;
});
}
function serverTodosDelete(todo) {
return fetch(api + '/' + todo._id, { method: 'delete' })
}
}());
```
---
[← Back to *mark for deletion*](../02-mark-for-deletion) | [Continue to *synchronize* →](../04-synchronize)
================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/application.js
================================================
(function() {
var api = 'https://offline-todo-api.herokuapp.com/todos';
var db, input, ul;
databaseOpen()
.then(function() {
input = document.getElementsByTagName('input')[0];
ul = document.getElementsByTagName('ul')[0];
document.body.addEventListener('submit', onSubmit);
document.body.addEventListener('click', onClick);
})
.then(refreshView);
function onClick(e) {
e.preventDefault();
if (e.target.hasAttribute('id')) {
databaseTodosGetById(e.target.getAttribute('id'))
.then(function(todo) {
todo.deleted = true;
return databaseTodosPut(todo);
})
.then(refreshView);
}
}
function onSubmit(e) {
e.preventDefault();
var todo = { text: input.value, _id: String(Date.now()) };
databaseTodosPut(todo)
.then(function() {
input.value = '';
})
.then(refreshView);
}
function refreshView() {
return databaseTodosGet({ deleted: false }).then(renderAllTodos);
}
function renderAllTodos(todos) {
var html = '';
todos.forEach(function(todo) {
html += todoToHtml(todo);
});
ul.innerHTML = html;
}
function todoToHtml(todo) {
return '
'+todo.text+'
';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet(query) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
data.push(result.value);
}
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function serverTodosGet(_id) {
return fetch(api + '/' + (_id ? _id : ''))
.then(function(response) {
return response.json();
});
}
function serverTodosPost(todo) {
return fetch(api, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(todo)
})
.then(function(response) {
if (response.status === 410) throw new Error(response.statusText);
return response;
});
}
function serverTodosDelete(todo) {
return fetch(api + '/' + todo._id, { method: 'delete' });
}
}());
================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/fetch.js
================================================
(function() {
'use strict';
if (window.fetch) {
return
}
function Headers(headers) {
this.map = {}
var self = this
if (headers instanceof Headers) {
headers.forEach(function(name, values) {
values.forEach(function(value) {
self.append(name, value)
})
})
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
self.append(name, headers[name])
})
}
}
Headers.prototype.append = function(name, value) {
var list = this.map[name]
if (!list) {
list = []
this.map[name] = list
}
list.push(value)
}
Headers.prototype['delete'] = function(name) {
delete this.map[name]
}
Headers.prototype.get = function(name) {
var values = this.map[name]
return values ? values[0] : null
}
Headers.prototype.getAll = function(name) {
return this.map[name] || []
}
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(name)
}
Headers.prototype.set = function(name, value) {
this.map[name] = [value]
}
// Instead of iterable for now.
Headers.prototype.forEach = function(callback) {
var self = this
Object.getOwnPropertyNames(this.map).forEach(function(name) {
callback(name, self.map[name])
})
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Body already consumed'))
}
body.bodyUsed = true
}
function Body() {
this.body = null
this.bodyUsed = false
this.arrayBuffer = function() {
throw new Error('Not implemented yet')
}
this.blob = function() {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(new Blob([this.body]))
}
this.formData = function() {
return Promise.resolve(decode(this.body))
}
this.json = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
var body = this.body
return new Promise(function(resolve, reject) {
try {
resolve(JSON.parse(body))
} catch (ex) {
reject(ex)
}
})
}
this.text = function() {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(this.body)
}
return this
}
function Request(url, options) {
options = options || {}
this.url = url
this.body = options.body
this.credentials = options.credentials || null
this.headers = new Headers(options.headers)
this.method = options.method || 'GET'
this.mode = options.mode || null
this.referrer = null
}
function encode(params) {
return Object.getOwnPropertyNames(params).filter(function(name) {
return params[name] !== undefined
}).map(function(name) {
var value = (params[name] === null) ? '' : params[name]
return encodeURIComponent(name) + '=' + encodeURIComponent(value)
}).join('&').replace(/%20/g, '+')
}
function decode(body) {
var form = new FormData()
body.trim().split('&').forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}
function isObject(value) {
try {
return Object.getPrototypeOf(value) === Object.prototype
} catch (ex) {
// Probably a string literal.
return false
}
}
function headers(xhr) {
var head = new Headers()
var pairs = xhr.getAllResponseHeaders().trim().split('\n')
pairs.forEach(function(header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
head.append(key, value)
})
return head
}
Request.prototype.fetch = function() {
var self = this
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: headers(xhr)
}
resolve(new Response(xhr.responseText, options))
}
xhr.onerror = function() {
reject()
}
xhr.open(self.method, self.url)
self.headers.forEach(function(name, values) {
values.forEach(function(value) {
xhr.setRequestHeader(name, value)
})
})
var body = self.body
if (isObject(self.body)) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
body = encode(self.body)
}
xhr.send(body)
})
}
Body.call(Request.prototype)
function Response(body, options) {
this.body = body
this.type = 'default'
this.url = null
this.status = options.status
this.statusText = options.statusText
this.headers = options.headers
}
Body.call(Response.prototype)
window.fetch = function (url, options) {
return new Request(url, options).fetch()
}
})();
================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/index.html
================================================
Example: Todo
================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js
NETWORK:
*
================================================
FILE: 04-offline-todo-with-sync/03-adding-ajax/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j'+todo.text+'';
}
function databaseOpen() {
return new Promise(function(resolve, reject) {
var version = 1;
var request = indexedDB.open('todos', version);
request.onupgradeneeded = function(e) {
db = e.target.result;
e.target.transaction.onerror = reject;
db.createObjectStore('todo', { keyPath: '_id' });
};
request.onsuccess = function(e) {
db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
function databaseTodosPut(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.put(todo);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function databaseTodosGet(query) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
// Get everything in the store
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
// This fires once per row in the store, so for simplicity collect the data
// in an array (data) and send it pass it in the resolve call in one go
var data = [];
cursorRequest.onsuccess = function(e) {
var result = e.target.result;
// If there's data, add it to array
if (result) {
if (!query || (query.deleted === true && result.value.deleted) || (query.deleted === false && !result.value.deleted)) {
data.push(result.value);
}
result.continue();
// Reach the end of the data
} else {
resolve(data);
}
};
});
}
function databaseTodosGetById(id) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readonly');
var store = transaction.objectStore('todo');
var request = store.get(id);
request.onsuccess = function(e) {
var result = e.target.result;
resolve(result);
};
request.onerror = reject;
});
}
function databaseTodosDelete(todo) {
return new Promise(function(resolve, reject) {
var transaction = db.transaction(['todo'], 'readwrite');
var store = transaction.objectStore('todo');
var request = store.delete(todo._id);
transaction.oncomplete = resolve;
request.onerror = reject;
});
}
function serverTodosGet(_id) {
return fetch(api + '/' + (_id ? _id : ''))
.then(function(response) {
return response.json();
});
}
function serverTodosPost(todo) {
return fetch(api, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(todo)
})
.then(function(response) {
if (response.status === 410) throw new Error(response.statusText);
return response;
});
}
function serverTodosDelete(todo) {
return fetch(api + '/' + todo._id, { method: 'delete' });
}
}());
================================================
FILE: 04-offline-todo-with-sync/04-synchronize/fetch.js
================================================
(function() {
'use strict';
if (window.fetch) {
return
}
function Headers(headers) {
this.map = {}
var self = this
if (headers instanceof Headers) {
headers.forEach(function(name, values) {
values.forEach(function(value) {
self.append(name, value)
})
})
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
self.append(name, headers[name])
})
}
}
Headers.prototype.append = function(name, value) {
var list = this.map[name]
if (!list) {
list = []
this.map[name] = list
}
list.push(value)
}
Headers.prototype['delete'] = function(name) {
delete this.map[name]
}
Headers.prototype.get = function(name) {
var values = this.map[name]
return values ? values[0] : null
}
Headers.prototype.getAll = function(name) {
return this.map[name] || []
}
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(name)
}
Headers.prototype.set = function(name, value) {
this.map[name] = [value]
}
// Instead of iterable for now.
Headers.prototype.forEach = function(callback) {
var self = this
Object.getOwnPropertyNames(this.map).forEach(function(name) {
callback(name, self.map[name])
})
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Body already consumed'))
}
body.bodyUsed = true
}
function Body() {
this.body = null
this.bodyUsed = false
this.arrayBuffer = function() {
throw new Error('Not implemented yet')
}
this.blob = function() {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(new Blob([this.body]))
}
this.formData = function() {
return Promise.resolve(decode(this.body))
}
this.json = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
var body = this.body
return new Promise(function(resolve, reject) {
try {
resolve(JSON.parse(body))
} catch (ex) {
reject(ex)
}
})
}
this.text = function() {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(this.body)
}
return this
}
function Request(url, options) {
options = options || {}
this.url = url
this.body = options.body
this.credentials = options.credentials || null
this.headers = new Headers(options.headers)
this.method = options.method || 'GET'
this.mode = options.mode || null
this.referrer = null
}
function encode(params) {
return Object.getOwnPropertyNames(params).filter(function(name) {
return params[name] !== undefined
}).map(function(name) {
var value = (params[name] === null) ? '' : params[name]
return encodeURIComponent(name) + '=' + encodeURIComponent(value)
}).join('&').replace(/%20/g, '+')
}
function decode(body) {
var form = new FormData()
body.trim().split('&').forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}
function isObject(value) {
try {
return Object.getPrototypeOf(value) === Object.prototype
} catch (ex) {
// Probably a string literal.
return false
}
}
function headers(xhr) {
var head = new Headers()
var pairs = xhr.getAllResponseHeaders().trim().split('\n')
pairs.forEach(function(header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
head.append(key, value)
})
return head
}
Request.prototype.fetch = function() {
var self = this
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: headers(xhr)
}
resolve(new Response(xhr.responseText, options))
}
xhr.onerror = function() {
reject()
}
xhr.open(self.method, self.url)
self.headers.forEach(function(name, values) {
values.forEach(function(value) {
xhr.setRequestHeader(name, value)
})
})
var body = self.body
if (isObject(self.body)) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
body = encode(self.body)
}
xhr.send(body)
})
}
Body.call(Request.prototype)
function Response(body, options) {
this.body = body
this.type = 'default'
this.url = null
this.status = options.status
this.statusText = options.statusText
this.headers = options.headers
}
Body.call(Response.prototype)
window.fetch = function (url, options) {
return new Request(url, options).fetch()
}
})();
================================================
FILE: 04-offline-todo-with-sync/04-synchronize/index.html
================================================
Example: Todo
================================================
FILE: 04-offline-todo-with-sync/04-synchronize/offline.appcache
================================================
CACHE MANIFEST
./styles.css
./indexeddb.shim.min.js
./promise.js
./fetch.js
./application.js
NETWORK:
*
================================================
FILE: 04-offline-todo-with-sync/04-synchronize/promise.js
================================================
!function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)"exports"===i[l]?k.push(g={}):k.push(b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;j> test.json # Neat trick if you're using *nix
npm install --save express cookie-parser es6-promise isomorphic-fetch
```
## Online only
To begin with we'll make the application work online only (ie. a normal website) and then discuss the changes that we will have to make to make it work offline. As building normal websites is an assumed prerequisite of this course heavy use of copy-paste is encouragedhere unless anything is unclear:
##### [`public/styles.css`](./public/styles.css)
```css
body {
margin: 0;
padding: 0;
font-family: helvetica, sans-serif;
}
* {
box-sizing: border-box;
}
h1 {
padding: 14px 0 14px 0;
margin: 0;
font-size: 44px;
border-bottom: solid 1px #DDD;
line-height: 1em;
}
nav {
padding: 14px 0 14px 0;
}
main {
padding: 0 14px;
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
padding: 20px 0 20px 0;
border-bottom: solid 1px #DDD;
}
```
Nothing too surprising should jump out here - it's just plain CSS.
##### [`public/templates.js`](./public/templates.js)
```js
(function() {
var exports = {
list: list,
article: article
};
function list(data) {
data = data || [];
var ul = '';
data.forEach(function(story) {
ul += '
'+data.body;
}
if (typeof module == 'object') {
module.exports = exports;
} else {
window.templates = exports;
}
}());
```
As we discussed above, these functions will eventually be used on the client side - which is why they need to go inside `public`.
`list` and `article` are functions that take a JavaScript object containing data that represent a list of stories and a single story, respectively.
The last few lines are potentially a little confusing:-
```js
if (typeof module == 'object') {
module.exports = exports;
} else {
window.templates = exports;
}
```
`if (typeof module == 'object')` is just a way of saying "am I running on the server" - and if that is the case this module will expose its functions via `module.exports`, otherwise it will add them to the `window` object.
##### [`index.js`](./index.js)
```js
require('es6-promise').polyfill();
require('isomorphic-fetch');
var port = Number(process.env.PORT || 8080);
var api = 'https://offline-news-api.herokuapp.com/stories';
var cookieParser = require('cookie-parser');
var express = require('express');
var path = require('path');
var templates = require('./public/templates');
var app = express();
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Manifest returns a 400 unless the AppCache cookie is set
app.get('/offline.appcache', function(req, res) {
if (req.cookies.up) {
res.set('Content-Type', 'text/cache-manifest');
res.send('CACHE MANIFEST'
+ '\n./appcache.js'
+ '\n./application.js'
+ '\n./iframe.js'
+ '\n./indexeddb.shim.min.js'
+ '\n./promise.js'
+ '\n./styles.css'
+ '\n./fetch.js'
+ '\n./templates.js'
+ '\n'
+ '\nFALLBACK:'
+ '\n/ /'
+ '\n'
+ '\nNETWORK:'
+ '\n*');
} else {
res.status(400).end();
}
});
// Add middleware to send this when the appcache update cookie is set
app.get('/', offlineMiddleware);
app.get('/article/:guid', offlineMiddleware);
function offlineMiddleware(req, res, next) {
if (req.cookies.up) res.send(layoutShell());
else next();
}
app.get('/fallback.html', function(req, res) {
res.send(layoutShell());
});
app.get('/article/:guid', function(req, res) {
fetch(api+'/'+req.params.guid)
.then(function(response) {
return response.json();
})
.then(function(data) {
res.send(layoutShell({
main: templates.article(data)
}));
}, function(err) {
res.status(404);
res.send(layoutShell({
main: templates.article({
title: 'Story cannot be found',
body: '
'
+ '\n '+data.main+''
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n';
}
app.listen(port);
console.log('listening on '+port);
```
Now run the application in your favourite web browser and check that both views work by running:-
```js
node index.js
```
And opening `http://localhost:8080` with your favourite browser.
---
[← Back to *building and offline news app, FT style*](../) | [Continue to *single/multi-page app* →](../02-single-multi-page)
================================================
FILE: 05-offline-news/01-scaffolding/index.js
================================================
require('es6-promise').polyfill();
require('isomorphic-fetch');
var api = 'https://offline-news-api.herokuapp.com/stories';
var port = 8080;
var express = require('express');
var path = require('path');
var templates = require('./public/templates');
var app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.get('/article/:guid', function(req, res) {
fetch(api+'/'+req.params.guid)
.then(function(response) {
return response.json();
})
.then(function(data) {
res.send(layoutShell({
main: templates.article(data)
}));
}, function(err) {
res.status(404);
res.send(layoutShell({
main: templates.article({
title: 'Story cannot be found',
body: '
'+data.body;
}
if (typeof module == 'object') {
module.exports = exports;
} else {
window.templates = exports;
}
}());
================================================
FILE: 05-offline-news/02-single-multi-page/README.md
================================================
# Single/Multi page app
Back in the introduction we laid down some basic ground rules about which offline technologies we were going to use to store the different kinds of data we need to.
- We will use **Application Cache** to store application assets, such as CSS, JavaScript and a basic HTML shell.
- We will use **IndexedDB** to store content - in this case, articles.
Because content stored in IndexedDB is not accessible to the Application Cache, in order to display pages offline we the website will need to transform into a single page app - whilst still being a normal website (multi-page app?) for the initial load.
In this step we will implement synchronisation and client-side and server-side rendering.
##### [`/index.js`](./index.js)
On the server side we will need to add a few more JavaScript files to the `layoutShell` function.
```js
[…]
+ '\n '
+ '\n '
+ '\n '+data.main+''
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n '
+ '\n