HTML5

HTML5 Offline Web Application Example

In this example we will talk about writing an Offline Web Application. Offline ? Sounds weird, doesn’t it ?

HTML5 allows us to create a “list” of files accessible after the browser is disconnected.

The browser will always have to download the main HTML file and all the files in this “list” at least once.

Let’s see how this works.
 
 
 

1. The Manifest

The “list” is called the manifest file. This manifest is declared with an attribute of the <html> Tag like this:

&lt;!DOCTYPE html&gt;
&lt;html manifest=&quot;cache.manifest&quot;&gt;
&lt;body&gt;

&lt;/body&gt;
&lt;/html&gt;

The filename does not matter, but as a convention you may use names like : manifest.appcache, myapplicaiton.manifest, …

The important thing is the Content-Type return by the server, it must be: text/cache-manifest.

In Apache you will have to add a directive: AddType text/cache-manifest .manifest to force the server to serve all .manifest file with the text/cache-manifest Content-Type.

In NGINX, you will have to edit the mime.types file and add the following line: text/cache.manifest     manifest;

For IIS, yous will have to add the mime type. Have a look at the answer on this StackOverflow.

And its first line must be: CACHE MANIFEST. For example:

CACHE MANIFEST
# This is a comment
index.html
css/style.css
js/script.js

This file will tell the browser to keep three files in cache: index.html, style.css and script.js.

So now, if you are disconnected and you refresh your browser, you will always be able to use your web page.

2. Manifest Sections

We did not see it in the previous section, but the manifest file is structured with sections:

  • CACHE: Files listed in this section are explicitly cached after they are downloaded.
  • NETWORK: Files listed in this section are explicitly NOT cached.

    “All requests to such resources bypass the cache, even if the user is offline”

  • FALLBACK: This section will specifies fallback pages the browser will use when a resource is not accessible. Entries in this section lines of mapping urls: URL_REQUESTED URL_FALLBACK.

Note : All the files listed outside any sections are considered listed in the explicit CACHE section.

So let see a more complex Manifest file:

CACHE MANIFEST
# This is a comment
CACHE:
index.html
css/style.css
js/script.js

NETWORK:
# All requests to the Api need a network connection
/api

FALLBACK:
add.html offline.html

This manifest file will caches 3 files, tell the browser to always try to download resources inside /api, and if the add.html file is requested, the display the offline.html file.

One side effect using application cache is that the browser won’t try to download updates cached files until the manifest file itself is updated …

This can be very disturbing during development, or when files are frequently updated.

To solve this problem we can add a version number in a comment line at the top of the manifest file to force the browser to update its cache.

For example:

CACHE MANIFEST
# V1.0.1 &lt;-- update the version to force the browser to update its cache.

# This is a comment
CACHE:
index.html
css/style.css
js/script.js

NETWORK:
# All requests to the Api need a network connection
/api

FALLBACK:
add.html offline.html

3. Application Cache API

The HTML5 Application Cache comes with a JavaScript Api, that will allow us (developers) to interact with the cache.

We can access the Application cache by the window.applicationCache object.

3.1 Events

Here are the events introduced by the Application Cache:

  • checking: Fired when the user agent is looking for updates, or is trying to download the manifest file for the first time. (This is always the first event in the sequence.)
  • error: The manifest file is not accessible (404), or any other errors when trying to downlod the manifest.
  • noupdate: The manifest did not change … nothing to update.
  • downloading: The manifest was found or updated, so the browser starts to download resources listed in it.
  • progress: Fired during download progress.
  • updateready: Fired when resources in the manifest have been redownloaded.
  • cached: Fired when resources have been cached. (Last event in sequence.)

3.2 Properties

The window.applicationCache object has only one property: status
This property returns the current status of the applicationCache.
The value of the property is one of this constant:

  • UNCACHED: 0 A special value that indicates that an application cache object is not fully initialized.
  • IDLE: 1 The application cache is not currently in the process of being updated.
  • CHECKING: 2 The manifest is being fetched and checked for updates.
  • DOWNLOADING: 3 Resources are being downloaded to be added to the cache, due to a changed resource manifest.
  • UPDATEREADY: 4 There is a new version of the application cache available. There is a corresponding updateready event, which is fired instead of the cached event when a new update has been downloaded but not yet activated using the swapCache() method.
  • OBSOLETE: 5 The application cache group is now obsolete.

3.3 Methods

Here are the methods of the window.applicationCache object.

  • update(): Invoke the cache download process, not really necessary, the browser, will generally take care of updating the cache.
  • abort(): Cancel the cache download process.
  • swapCache(): Switches to the most recent application cache, if there is a newer one. If there isn’t, throws an InvalidStateError exception.

4. The online / offline events

The API also add two events for Online status detection. These events are: online and offline.

Let see a simple example:

window.addEvenListener('online', function(){
    console.log('online');
}, false)
window.addEvenListener('offline', function(){
    console.log('offline');
}, false)

Now we know the necessary for a “real world” example …

5. A Working Example

As an example we can imagine a simple web application, to manage your todo list.
You must not be able to add a task when you’re offline, we will do that by disabling submit button.

NOTE: The example will use the Zepto.js library to facilitate, the ajax calls and DOM manipulation..

5.1. Project structure

So the project structure will be:

 /
 --/css/
 --/css/style.css
 --/js/
 --/js/script.js
 --/server/server.php
 --/server/tasks.json
 --/index.html

The goal is to write an application that will allow you to add / remove / edit tasks, tasks will be saved in a static json file (tasks.json) on the server (simulated here by the server.php file).

5.2 The server.php file

As I said this will simulate a server, so do not use this file in production, this is only for testing purpose.

// We we only manage POST Requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $message = array();
    $file  = dirname(__FILE__).&quot;/tasks.json&quot;;
    $content = file_get_contents($file);
    $tasks = json_decode($content, true);
    switch($_POST['action']){
        case &quot;list&quot;:
            // create the response
            $message[&quot;type&quot;] = &quot;OK&quot;;
            $message[&quot;data&quot;] = $tasks;
            break;
        case &quot;add&quot;:
            // create the task
            $task = array('id'=&gt; uniqid(), &quot;task&quot;=&gt;$_POST[&quot;task&quot;]);
            // push it to the array pf tasks
            array_push($tasks, $task);

            // save the file :
            file_put_contents($file, json_encode($tasks));

            // create the response
            $message[&quot;type&quot;] = &quot;OK&quot;;
            $message[&quot;data&quot;] = $task;

            break;
        case &quot;remove&quot;:
            // Get the task id
            $id = $_POST[&quot;id&quot;];
            $tmpTasks = array();
            $found = false;
            foreach($tasks as $task){
                if( $task[&quot;id&quot;] !== $id ){
                    array_push($tmpTasks, $task);
                } else {
                    $found = true;
                }
            }
            if( $found ){
                // Ok task was deleted
                $message[&quot;type&quot;] = &quot;OK&quot;;
                $message[&quot;data&quot;] = $id;
                file_put_contents($file, json_encode($tmpTasks));
            } else {
                $message[&quot;type&quot;] = &quot;KO&quot;;
                $message['message'] = &quot;Task No Found&quot;;
            }
            break;
        case &quot;update&quot;:
        case &quot;edit&quot;:
            // Get the task id
            $id = $_POST[&quot;id&quot;];
            $taskPosted = $_POST[&quot;task&quot;];
            $tmpTasks = array();
            $found = false;
            foreach($tasks as $task){
                if( $task[&quot;id&quot;] === $id ){
                    $task[&quot;id&quot;] = $id;
                    $task[&quot;task&quot;] = $taskPosted;
                    $found = true;
                }
                array_push($tmpTasks, $task);
            }
            if( $found ){
                // Ok task was deleted
                $message[&quot;type&quot;] = &quot;OK&quot;;
                $message[&quot;data&quot;] = array('id'=&gt;$id, 'task'=&gt;$taskPosted);
                file_put_contents($file, json_encode($tmpTasks));
            } else {
                $message[&quot;type&quot;] = &quot;KO&quot;;
                $message['message'] = &quot;Task No Found&quot;;
            }
            break;
        case 'reset':
            // save the file :

            file_put_contents($file, json_encode(array()));
            $message[&quot;type&quot;] = &quot;OK&quot;;
            $message[&quot;data&quot;] = &quot;&quot;;
            break;
        default:
            $message = array(
                &quot;type&quot;=&gt;&quot;ERR&quot;,
                &quot;message&quot; =&gt;&quot;Action unknown : &quot;.$_POST[&quot;action&quot;].&quot;.&quot;
            );
            break;
    }
    // Print the result
    echo json_encode($message);
} else {
    $message = array(
        &quot;type&quot;=&gt;&quot;ERR&quot;,
        &quot;message&quot; =&gt;&quot;Request should be post, nothing to do ...&quot;
    );
    echo json_encode($message);
}

This little php script allow you to use some actions to manage the task list, some of the actions needs some extra post datas, here is the API definition:

actionparametersdescription
listnoneGet the full list of tasks.
addtask : STRINGAdd the task, will generate a unique id, and return a task :

            {
                &quot;type&quot;:&quot;OK&quot;,
                &quot;data&quot;:{
                    &quot;id&quot;:&quot;54e390fbc8823&quot;,
                    &quot;task&quot;:&quot;rere&quot;
                }
            }
            
removeid : INTRemove the task identified by the ID, will return the id as message, or an error message :

            {
                &quot;type&quot;:&quot;OK&quot;,
                &quot;data&quot;:&quot;54e390fbc8823&quot;
            }
            

or

            {
                &quot;type&quot;:&quot;KO&quot;,
                &quot;message&quot;:&quot;Task No Found&quot;
            }
            
update or editid : INT
task : STRING
Update the task identified by the ID with the Task sent, will return the id as message, or an error message :

            {
                &quot;type&quot;:&quot;OK&quot;,
                &quot;data&quot;:&quot;54e390fbc8823&quot;
            }
            

or

            {
                &quot;type&quot;:&quot;KO&quot;,
                &quot;message&quot;:&quot;Task No Found&quot;
            }
            
resetnoneReset the task list (empty list), return an empty message :

            {
                &quot;type&quot;:&quot;OK&quot;,
                &quot;message&quot;:&quot;&quot;
            }
            
defaultnoneShould not append, return an error message :

            {
                &quot;type&quot;:&quot;KO&quot;,
                &quot;message&quot;:&quot;Task No Found&quot;
            }
            

5.3 The main HTML file

Here is the index.html source file:

&lt;!DOCTYPE html&gt;
&lt;html manifest=&quot;app.manifest&quot;&gt;
&lt;head lang=&quot;en&quot;&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;Task List Example&lt;/title&gt;
    &lt;link href=&quot;css/style.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;wrapper&quot;&gt;
    &lt;form id=&quot;taskform&quot;&gt;
        &lt;input type=&quot;hidden&quot; id=&quot;task_id&quot; value=&quot;-1&quot; /&gt;
        &lt;input type=&quot;text&quot; id=&quot;task&quot; placeholder=&quot;Add a Task ...&quot; /&gt;
        &lt;input type=&quot;submit&quot; value=&quot;Add&quot; /&gt;
    &lt;/form&gt;
    &lt;ul id=&quot;tasklist&quot;&gt;&lt;/ul&gt;
    &lt;button id=&quot;reset&quot;&gt;Clear All Tasks&lt;/button&gt;
&lt;/div&gt;

&lt;script src=&quot;js/zepto.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;js/script.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;

Here is the app.manifest file:

CACHE MANIFEST
# version 1
# This is a comment
CACHE:
index.html
css/style.css
js/script.js
js/zepto.min.js

NETWORK:
server/server.php

5.4 The JavaScript

I won’t show the full JS file as you can download it, I will only explain some parts of the code.

First of all we need to detect if the browser is onLine or offLine, as the whole code is cached (via the app.manifest file), the application can be opened without being onLine (don’t need to do the first request if it was done once before).

if(navigator.onLine){
    // browser is online so I can load the list from the server
    server.list(init);
    // set the connection status to true
    setConnected(true);
} else {
    // Not online, set the connection status to false
    setConnected(false);
}

Here is the function setConnected

function setConnected(isConnected){
    var buttons = 'input[type=&quot;submit&quot;], button';
    if( isConnected ){
        $(buttons).removeAttr('disabled', 'disabled');
    } else {
        $(buttons).attr('disabled', 'disabled');
    }
}

This function only enable or disable the buttons.

Now we need to listen to the online and offline events to manage the connection status:

$(window).bind('online', function(){
    // If the list was never loaded
    if( ! loaded ){
        // we load the list from the server
        server.list(init);
    }
    // set the connection status to true
    setConnected(true);
});
$(window).bind('offline', function(){
    // set the connection status to false
    setConnected(false);
});

I think the above code, is documented enough to understand it.

Here is what the application looks like in “online” mode:

HTML5 Offline Web Application Example OnLine
HTML5 Offline Web Application Example OnLine

Here is what the application looks like in “offline” mode:

HTML5 Offline Web Application Example OffLine
HTML5 Offline Web Application Example OffLine

6. Download

Download
You can download the full source code of this example here: HTML5 Offline Web Application Example

Remi Goyard

I'm a senior web architect. Passionated by new technologies, programming, devops tools, and agile methodologies. I really enjoy to coach, or teach, people who wants to learn programming, from beginners to skilled programmers. Involved in local developer communities, Java User Group, PHP User Group, or Javascript User Group, i like to share with others about experiences, news or business. Coding is FUN !
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ulysses
Ulysses
8 years ago

Hey great example but you have left out the reference to your manifest file in the index.html of your example.
So it does not work!

Back to top button