How to Build Offline-smart Web Apps with Hoodie

 

In this tutorial, I’m going to show you how to build an application that works both online and offline (when the user is disconnected from the internet) with Hoodie.

Introduction to Hoodie

Hoodie is an open-source library that provides out-of-the-box syncing and offline capability. With Hoodie, you can quickly build web apps without worrying about the back-end, and out-of-the-box offline capabilities. Hoodie abstracts away the back-end and provides a friendly API for you to use within your apps. You can think of it as the backend for your app. Its offline-first capability allows users’ data to be available with or without an internet connection. Users’ data are stored locally by default, and synced to the server later, making your data be, eventually, consistent. This is handled by Hoodie automatically and intelligently, so your apps are accessible and usable irrespective of users’ connection status.

This is good for rapidly building offline responsive web apps without worrying about the backend or data management. If you dread backend work as a frontend dev, then Hoodie is your pal 😉

Behind the scenes, Hoodie makes this possible using CouchDB for storing data on the server, PouchDB for in-browser data storage, and Node.js, which is the actual server code that handles synchronisation of data between the client and server. Data is sent to the client data store through the hoodie.store API and then to CouchDB. And if there are other clients which the user has the apps open on, data update are gotten from them, synced with the server, and then push updates to other connected clients. For a deeper look into how this works, follow this link.

What do I need to develop on top of Hoodie

To work with Hoodie, you need to install Node.js. If you plan to deploy your app for production use, it is recommended to use CouchDB, but it is not required for development.

Other tools either come as packages which you install via npm or work off the client device (e.g. browser) like localStorage/PouchDB.

Building an app

Now let’s get started with building an app. This way, I’ll walk you through some basic concepts of Hoodie while learning how to work with it. We’ll be building a very simple phonebook web application that allows users to save contacts and see a list of contacts saved.

Creating the project and adding Hoodie

To get started, open the command line and navigate to the directory you want to store the files for this application.

$ cd ~/apps/hoodie-phonebook

Now we need a package.json file and we’ll create this using npm, which comes with Node by default. This will ask us questions and then create a package.json for us. If you already have a package.json file, it’ll read that first, and default to the options in there. So run the following command and press enter when asked any question just to leave the answer to the default:

npm init

Now we have this, let’s move on to installing Hoodie using npm. Run the following command:

npm install hoodie --save

While we watch this command execute, we see all the things that get installed.

The install will also set the “start” script to “hoodie”. This tells npm to start the Hoodie server when we run the npm start command. Doing so, our package.json file should be similar to the following:

{
"name": "hoodie-phonebook",
"version": "1.0.0",
"description": "a simple phonebook application for managing contacts",
"main": "index.js",
"scripts": {
"start": "hoodie",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Peter Mbanugo",
"license": "ISC",
"dependencies": {
"hoodie": "^24.2.5"
}
}

And the project directory should just have node_modules folder (and in it are the packages installed from npm), and the package.json.

Now let’s run start the app by running npm start. You’ll notice that a page shows up telling us that we have a missing folder and some other details. This leads us to the next step, which is understanding how Hoodie apps should be structured. So stop the server now and let’s move on to this.

Structure of a Hoodie project

Let’s take a look at how a Hoodie project is organized in terms file and directory organization.

public (folder)

This is where the app lives. It includes the usual index.html and all of the app’s assets. If you’re into task runners such as Grunt or Gulp, this is where you’d put your compiled code. Hoodie doesn’t care about what else you add to this folder, so feel free to throw in your source folder, tests, more documentation, or whatever you like.

For now, go ahead and add the public folder and within it a folder called assets.

node_modules (folder)

This is where npm keeps all its files. Hoodie uses it to manage its plugins and dependencies and you could use it too to manage dependencies in your app. This folder was added when we npm-installed Hoodie. It’s just a folder that holds the binaries for packages added via npm. There are important points to note about this folder.

  1. Do not edit the content of this folder manually because if you do, whenever you install or update dependencies, files in there will be overwritten. Use npm to make changes to this file if needed.

  2. Do not add this folder to source control. Packages can be restored by anyone by simply running the npm install command.

package.json

Every Node.js application needs this file. It contains important information for a Node.js application, e.g specifying the dependencies for the app, its content also determines what goes into the node_modules folder we talked about previously. If you remember, we already have this in the root directory of our app. It is used to install and declare the used versions of the Hoodie Server and Hoodie plugins that the app uses. See a portion of our file:

.......
"dependencies": {
"hoodie": "^24.2.5"
}

.hoodie (folder)

The Hoodie Client is stored here after it gets dynamically created, as well as database files or your app’s configuration. With this type of setup, we can move the app and it’s data to another system and still have it working. Or if you delete this folder, you’ll get a new one recreated automatically.

Adding custom files and folder

Having sorted the basic needs and looking through some important things, we move on to adding files and customizing the app the way we want it.

Let’s move on to adding an index.html file to the public folder and add a reference to jQuery and Bootstrap. The content should look similar to what I have below:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Hoodie App</title>
<link rel="stylesheet" href="assets/vendor/bootstrap/bootstrap.min.css">
</head>
<body>

<a href="http://assets/vendor/jquery-2.1.0.min.js">http://assets/vendor/jquery-2.1.0.min.js</a>
<a href="http://assets/vendor/bootstrap/bootstrap.js">http://assets/vendor/bootstrap/bootstrap.js</a>
</body>
</html>

Inside the assets folder, add a js folder and in it have a file called index.js. This file will contain our JavaScript code for manipulating the DOM. Add a reference to this file inside the &lt;body&gt; of the html file.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Hoodie App</title>
<link rel="stylesheet" href="assets/vendor/bootstrap/bootstrap.min.css">
</head>
<body>

<a href="http://assets/vendor/jquery-2.1.0.min.js">http://assets/vendor/jquery-2.1.0.min.js</a>
<a href="http://assets/vendor/bootstrap/bootstrap.js">http://assets/vendor/bootstrap/bootstrap.js</a>
<a href="http://assets/js/index.js">http://assets/js/index.js</a>
</body>
</html>

Then, add a reference to the Hoodie client.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Hoodie App</title>
<link rel="stylesheet" href="assets/vendor/bootstrap/bootstrap.min.css">
</head>
<body>

<a href="http://assets/vendor/jquery-2.1.0.min.js">http://assets/vendor/jquery-2.1.0.min.js</a>
<a href="http://assets/vendor/bootstrap/bootstrap.js">http://assets/vendor/bootstrap/bootstrap.js</a>
<a href="/hoodie/client.js">/hoodie/client.js</a>
<a href="http://assets/js/index.js">http://assets/js/index.js</a>
</body>
</html>

The /hoodie/client.js is loading the dynamic Hoodie Client for your Hoodie Server. It’s generated automatically by the backend and made available to your app. Its content depends on your Hoodie app and is generated based on the configuration in the Hoodie server. When you start the app, it is generated and placed in ./.hoodie/ folder. The contents in this folder are generated by the Hoodie backend, these also include other hoodie plugins that can be used to extend the Hoodie core functionalities. The hoodie client is a way for us to interact with Hoodie in the client, through the Hoodie client API. This client also talks to the Hoodie server and they both know how they can magically rub hands together to keep all your data in sync. To talk to Hoodie in the client, there is a global object you can access through window.hoodie or simply hoodie and with this, you access the Hoodie client API.

Let’s update the content of our index to include markup for adding and viewing contacts. I’ll have a section for adding new contacts, and another for viewing lists of contacts in a tabular form, and all will reside on the same page. I’m keeping things to the barest minimum and not wanting to do some CSS tricks :). Below is my markup which could be similar to yours:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>My Hoodie App</title>
<link rel="stylesheet" href="assets/vendor/bootstrap/bootstrap.min.css">
</head>

<body>

<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">

<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>

<a class="navbar-brand" href="#"> Hoodie</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active"><a href="./">Default <span class="sr-only">(current)</span></a></li>
<li><a href="../navbar-static-top/">Static top</a></li>
<li><a href="../navbar-fixed-top/">Fixed top</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">

<div class="row">
<div class="col-md-10">
<h2>Add new contact </h2>
<hr />

<div class="form-group">
Name
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
Mobile
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
Email
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
Save Contact
</div>
</div>
</form>
<hr />
</div>
</div>

<div class="row">
<div class="col-md-10">
<h2>Contact List</h2>
<hr />
<table id="contactList" class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Mobile</th>
<th>Email</th>
</tr>
</thead>
<tbody>

</tbody>
</table>
</div>
</div>
<a href="http://assets/vendor/jquery-2.1.0.min.js">http://assets/vendor/jquery-2.1.0.min.js</a>
<a href="http://assets/vendor/bootstrap/bootstrap.js">http://assets/vendor/bootstrap/bootstrap.js</a>
<!-- Load the dynamic version of hoodie.js that includes all the plugin code-->
<a href="/hoodie/client.js">/hoodie/client.js</a>
<a href="http://assets/js/index.js">http://assets/js/index.js</a>
</body>

</html>

Now we get to the fun part where we actually interact with Hoodie. Let’s open up the index.js and add code to collect the contact details and save it to the Hoodie store. The hoodie store is where data is stored for each user. In Hoodie, each user has their own database, and this is because Hoodie operates the one database per user model. This way, each user has their own database on their device and another on the server. Then the Hoodie client and the hoodie server communicates with each other to keep data in both places in sync, all happening under the hood without your users noticing if the server is active or not.

Let’s take a step back and look at the Hoodie client API in summary. The Hoodie client API is sub-categorized into sets focused differently focused. We have:

  • The core API: Which defines the functionality of the base Hoodie object, has a couple of useful helper methods, and sets up endpoints which it uses to interact with the Hoodie backend.

  • The account API: This lets you manage user account. Having said that it works on the one database per user model, it is through this API that it recognizes users and manage data for each logged in user. You have APIs to sign up, sign in, and sign out users. We now know that Hoodie lets your app work offline, but in order to save each user’s synced data across devices, they have to be signed in. If you use the app without signing up and/or in, you’re an anonymous user, your data will get saved locally in your browser and they’ll still be there when you reload the page. But they won’t get saved to the server, and you won’t be able to see them from anywhere else. They’ll also vanish forever when you clear your browser store. Once you’re signed in a Hoodie app, Hoodie will constantly try to keep your user data in sync between the server and any client that a user may be signed in to.

  • The store API:
    This provides means to store and retrieve data for each individual user.

  • The connectionStatus API: This provides helpers for connectivity. With this, you can tell when the app is connected to the server, disconnected, or trying to reconnect. There are events available which you can listen to and react accordingly based on your app’s requirement. In many cases, users simply do not care if they’re connected or not as long as the app works.

  • The log API: This provides a nice API for logging all the things

Now let’s move on to saving the contact to the Hoodie store when the user clicks the button. Open the index.js and add the following:

$('#contactForm').submit(function(event) {
// prevent page reload
event.preventDefault();

var name = $('#name').val();
var email = $('#email').val();
var mobile = $('#mobile').val();

// Save the contact to the database with Hoodie
hoodie.store.add({
name: name,
mobile: mobile,
email: email
});

var newContact = '<tr><td>' + name + '</td><td>' + mobile + '</td><td>' + email + '</td></tr>'
$("#contactList tbody").append(newContact);

$('#contactForm')[0].reset();
});

With this code, we get the values in our input fields, and save them to the database using the add method from the Hoodie store API. Then we emit the newly collected data to the page and reset the form. Start the server by running npm start and open the page in a browser and add a few records. To test if this works and actually saves to the database, we’ll query the store directly using the browser console window. Enter the following code and see that it returns the names of contacts you added previously.

hoodie.store.findAll().then(function(contacts) {
contacts.forEach(function (contact) { console.log(contact.name) }); });

The findAll method is part of hoodie client store API (check this page for a list of other methods available) which retrieves all the records in the store. This method, as well as others, return promises much like to what you’re used to if you’ve been programming in JS. To show that the data are saved offline even when the server is down, we’ll stop the server and add a few contacts and then run the query to retrieve all contacts. You’ll notice that the contacts were saved and are still available on the client. But the problem is when we clear the browser store or move to another browser within the same device, the data is not available, and this is because to Hoodie, the current user is anonymous and it’s data is not persisted to the server or synced both ways when we’re on multiple devices.

To allow for this sort of capability, we need to sign in (or sign up, if you haven’t already) to Hoodie through the Hoodie account. There are API methods to do these things and they’re available through hoodie.account. Moving on, we’ll modify our nav-bar to include a button that when clicked, will create a new account, sign in, or sign out. We’ll update the nav-bar element with the following markup

<div id="navbar" class="navbar-collapse collapse">
<div id="authenticated" class="nav navbar-form navbar-right">
Sign out
</div>
<div class="nav navbar-form navbar-right">
Sign up
Sign in
</div>
</div>

And unto our script file, we add the code to make it show the sign out button when the user is signed in, and a sign up or sign in button when they’re not.

if (hoodie.account.isSignedIn()) {
$('#username').text('signed in as ' + hoodie.account.username);
$('#authenticated').show();
$('#anonymous').hide();
} else {
$('#authenticated').hide();
$('#anonymous').show();
}

We will simply check to know if the user is signed in, know their name, and a button (which when clicked, signs them out) is displayed when they’re already signed in, and also buttons for sign up and sign in when they’re not. We hereby move on to reacting to the user’s request based on which button they clicked. Add the following code to handle this scenario:

$('#signup').click(function (event) {
signUp();
});

$('#signin').click(function (event) {
signIn();
});

$('#signout').click(function (event) {
signOut();
});

if (hoodie.account.isSignedIn()) {
showAuthenticated();
} else {
showAnonymous();
}

function signUp() {
var username = prompt('username');
var password = prompt('password');
hoodie.account.signUp({
username: username,
password: password
})
.then(function() {
return hoodie.account.signIn({
username: username,
password: password
});
})
.then(function() {
showAuthenticated();
})
.catch(function(errror) {
alert('Ooops, something went wrong: ' + error.message);
})
}

function signIn(){
var username = prompt('username');
var password = prompt('password');

hoodie.account.signIn({
username: username,
password: password
})
.then(function() {
showAuthenticated();
})
.catch(function(error) {
alert('ooops: ' + error.message);
});
}

function signOut(){
hoodie.account.signOut()
.then(function() {
showAnonymous();
})
.catch(function(error) {
alert('ooops: ' + error.message);
});
}

function showAuthenticated(){
$('#username').text('signed in as ' + hoodie.account.username);
$('#authenticated').show();
$('#anonymous').hide();
}

function showAnonymous(){
$('#authenticated').hide();
$('#anonymous').show();
}

Looking through the code above, you can see how I’ve used the account API to handle authentication to the app. Now if we try to sign in with the wrong user, we get an error. And when we signup, it creates an account and signs us in. Now go ahead and sign up. Once done you get signed in. Open a new browser and sign in to that browser. At the momen, we won’t see anything on the page but if we open the console and use the find method in the Hoodie store like we did before, we will get the data we added from the other browser. But if we sign out, we get no data.

Let’s go ahead and modify our code to load contacts once the page is loaded for a signed in user.

// when the site loads in the browser,
// we load all previously saved contacts from hoodie
loadContacts();

function loadContacts() {
hoodie.store.findAll().then(function(contacts) {

var tbody = '';
$.each(contacts, function (i, contact) {
var row = '<tr><td>' + contact.name + '</td><td>' + contact.mobile + '</td><td>' + contact.email + '</td></tr>';
tbody += row;
});

$("#contactList tbody").html('').html(tbody);
});
}

Now we have the authentication working and users can see the same data in any device when they sign in, but they will not see newly added data from other clients unless they reload the page. To overcome this headache, we tap into the awesome syncing capability of Hoodie. Once you’re signed in to a Hoodie app, Hoodie will constantly try to keep your user data in sync between the server and any client that user may be signed in to. To do this, I’ll use the event mechanism in Hoodie to dynamically modify the page when a new data is added to the store, and this will respond to both local changes from the client and incoming changes from the server during sync.

Let’s open up the script and remove the code section that adds a new row to the HTML table. It should now look like this:

$('#contactForm').submit(function(event) {
// prevent page reload
event.preventDefault();

var name = $('#name').val();
var email = $('#email').val();
var mobile = $('#mobile').val();

// Save the contact to the database with Hoodie
hoodie.store.add({
name: name,
mobile: mobile,
email: email
});

$('#contactForm')[0].reset();
});

So it’s only calling the store API to save data. Then we create a new method that takes a contact object as parameter, and this we’ll use to modify our page to add a new contact to the page. Also, we’ll listen in to Hoodie for when data is being added and then call this new method we’re adding. Add the following code:

// when a new entry is added to the database, run the corresponding function
hoodie.store.on('add', addNewContactToList);

function addNewContactToList(contact) {
var newContact = '<tr><td>' + contact.name + '</td><td>' + contact.mobile + '</td><td>' + contact.email + '</td></tr>'
$("#contactList tbody").append(newContact);
}

With hoodie.store.on('add', addNewContactToList) we subscribe to the add event, and there other types of event like remove. You can listen to different types of events, check out the doc for more. The account store also has the same concept and you can see the events you can listen on here and also methods available for the account API.

All together our files now look like this in terms of content:
index.html

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>My Hoodie App</title>
<link rel="stylesheet" href="assets/vendor/bootstrap/bootstrap.min.css">
</head>

<body>

<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#"> Hoodie</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<div id="authenticated" class="nav navbar-form navbar-right">
<span id="username"></span>
Sign out
</div>
<div id="anonymous" class="nav navbar-form navbar-right">
Sign up
Sign in
</div>
</div>
</div>
</nav>
<div class="container">

<div class="row">
<div class="col-md-10">
<h2>Add new contact </h2>
<hr />

<div class="form-group">
Name
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
Mobile
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
Email
<div class="col-sm-10">

</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
Save Contact
</div>
</div>
</form>
<hr />
</div>
</div>

<div class="row">
<div class="col-md-10">
<h2>Contact List</h2>
<hr />
<table id="contactList" class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Mobile</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>

</tbody>
</table>
</div>
</div>
<a href="http://assets/vendor/jquery-2.1.0.min.js">http://assets/vendor/jquery-2.1.0.min.js</a>
<a href="http://assets/vendor/bootstrap/bootstrap.min.js">http://assets/vendor/bootstrap/bootstrap.min.js</a>
<!-- Load the dynamic version of hoodie.js that includes all the plugin code-->
<a href="/hoodie/client.js">/hoodie/client.js</a>
<a href="http://assets/js/index.js">http://assets/js/index.js</a>
</body>

</html>

index.js

$('#contactForm').submit(function(event) {
// prevent page reload
event.preventDefault();

var name = $('#name').val();
var email = $('#email').val();
var mobile = $('#mobile').val();

// Save the contact to the database with Hoodie
hoodie.store.add({
name: name,
mobile: mobile,
email: email
});

$('#contactForm')[0].reset();
});

$('#signup').click(function (event) {
signUp();
});

$('#signin').click(function (event) {
signIn();
});

$('#signout').click(function (event) {
signOut();
});

if (hoodie.account.isSignedIn()) {
showAuthenticated();
} else {
showAnonymous();
}

// when the site loads in the browser,
// we load all previously saved contacts from hoodie
loadContacts();

//when a new entry is added to the database, run the corresponding function
hoodie.store.on('add', addNewContactToList);

function loadContacts() {
hoodie.store.findAll().then(function(contacts) {
var tbody = '';
$.each(contacts, function (i, contact) {
var row = '<tr><td>' + contact.name + '</td><td>' + contact.mobile + '</td><td>' + contact.email + '</td></tr>';
tbody += row;
});

$("#contactList tbody").html('').html(tbody);
});
}

function addNewContactToList(contact) {
var newContact = '<tr><td>' + contact.name + '</td><td>' + contact.mobile + '</td><td>' + contact.email + '</td></tr>'
$("#contactList tbody").append(newContact);
}

function signUp() {
var username = prompt('username');
var password = prompt('password');
hoodie.account.signUp({
username: username,
password: password
})
.then(function() {
return hoodie.account.signIn({
username: username,
password: password
});
})
.then(function() {
showAuthenticated();
})
.catch(function(errror) {
alert('Ooops, something went wrong: ' + error.message);
})
}

function signIn(){
var username = prompt('username');
var password = prompt('password');

hoodie.account.signIn({
username: username,
password: password
})
.then(function() {
showAuthenticated();
})
.catch(function(error) {
alert('ooops: ' + error.message);
});
}

function signOut(){
hoodie.account.signOut()
.then(function() {
showAnonymous();
})
.catch(function(error) {
alert('ooops: ' + error.message);
});
}

function showAuthenticated(){
$('#username').text('signed in as ' + hoodie.account.username);
$('#authenticated').show();
$('#anonymous').hide();
}

function showAnonymous(){
$('#authenticated').hide();
$('#anonymous').show();
}

To see our syncing in action, open two different browsers, sign in to both of them and add data from each. You’ll notice that the changes get replicated to both. Now to see the magic of offline, shutdown the server and add new data from each client. Start the server up again and watch what happens. There you go! I’m sure you’ve seen the magic 🙂 Now we have an app that’s cross-device and works offline, all with less hassle.

Wrapping up

I’ll leave you with the task of adding the functionality of deleting and updating the contacts. You can find more functions of the store API here and account API.

You can find a sample of what we’ve built so far in this tutorial on GitHub, and you can use it as a starting point to add functionality (update and delete contact) and play around with the code.

With Hoodie, apps shouldn’t break just because they’re offline and Hoodie has made this easy for you to implement. If you have any questions, leave a comment, tweet @hoodiehq, join the community (coders, editors, event organisers, etc) or contribute.

Until next time, do have a superb moment!


 

Note: this post was originally posted on codementor

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s