Shopping List is an Offline First demo Progressive Web App built using Polymer and PouchDB. Mult-user / multi-device capabilities are enabled by Hoodie. This app is part of a series of Offline First demo apps, each built using a different stack. This app is a built using the Polymer App Toolbox and the Polymer CLI. This tutorial will walk you through the steps necessary to transform the Starter Kit (generated by the Polymer CLI) into an Offline First Shopping List Progressive Web App that uses PouchDB (an open source JavaScript database that syncs) and Hoodie (an open source backend framework for Offline First applications). If you simply want to try out a completed version of the Shopping List app then read the Quick Start section of this README. If you want to jump to the end of the tutorial and view the completed code, then check out the tutorial
branch of this project (note that the tutorial
branch contains a simplified version of the reference app with a clean commit history and is different than what you will find in the master
branch).
- Prerequisite Knowledge & Skills
- Key Concepts
- Tutorial Outline
- Initial Set Up
- Creating the Shopping List Polymer App
- Building the Basic Components
- Adding the Shopping List Domain Model
- Adding a PouchDB Database
- Completing the App
- Syncing Data
- Configure a Database
- Option 1: Apache CouchDB
- Option 2: IBM Cloudant
- Option 3: Cloudant Developer Edition
- Enable Live Replication with a Remote Database
- Configure a Database
- Adding Multi-User / Multi-Device Features with Hoodie
- Installing Hoodie
- Configuring Hoodie
- Using the Hoodie Store API
- Using Hoodie Account API
- Testing Offline Sync
- Adding Geolocation Features
- What's next?
- Other Features
- Get Involved in the Offline First Community!
- Further Reading and Resources
- Ability to write JavaScript at a novice level, at minimum.
- A basic understanding of JavaScript promises.
- A basic understanding of HTML.
- Ability to work with an application programming interface (API).
- Progressive Web Apps: A Progressive Web App provides both the discoverability of a web app and the reliable, fast, and engaging user experience of a native mobile app. See Pokedex.org for a fun example of a Progressive Web App and check out PWA Stats for a community-driven list of stats and news related to Progressive Web Apps.
- Polymer: Libraries, tools, and patterns for building Progressive Web Apps using web platform features such as Web Components, Service Workers, and HTTP/2.
- Web Components: Open standard for components and widgets that are customizable, reusable, and encapsulated
- Polymer App Toolbox: Components, tools, and templates for building Progressive Web Apps with Polymer and Web Components.
- Polymer App Toolbox - Starter Kit: A starter kit for building Polymer apps.
- Material Design: A visual language for building apps.
- Offline First: Progressive Web Apps must be Offline First in order to provide a reliable, fast, and engaging user experience regardless of network connectivity.
- Service Workers: Use the Cache API (part of the Service Workers specification) to make URL addressable resources and content available while offline.
- IndexedDB: Use IndexedDB or localForage (a polyfill that uses WebSQL or localStorage if IndexedDB is not supported) to make application data available while offline.
- PouchDB: An open source JavaScript database that syncs with anything that implements the CouchDB Replication Protocol.
- Apache CouchDB: An open source document database featuring an HTTP API, JSON documents, clustering capabilities for horizontal scalability, and peer-to-peer replication.
- IBM Cloudant: A fully-managed database-as-a-service (DBaaS) based on Apache CouchDB with additional full text and geospatial search capabilities
- Hoodie: An open source backend framework for Offline First applications, leveraging Apache CouchDB on the server and PouchDB on the client
TBD
In a terminal, check for Node.js version 6 or higher:
$ node -v
v8.6.0
Note: The $
in the above instruction (and in all subsequent examples) indicates the start of a command prompt in a terminal. Do not type the leading $
into your command prompt. Multiple lines beginning with a $
in subsequent instructions indicate the start of a new command (i.e. hit "enter" after the previous command and then type the new command). Subsequent lines not beginning with a $
in examples like the one above indicate output from the previous command. You should not type these lines.
Install Node.js if it is not already installed (or upgrade Node.js if you have a version earlier than 6):
Install Bower:
$ npm install -g bower
Note: If the above command results in an EACCES
error then read the documentation on fixing npm permissions.
Install Polymer CLI:
$ npm install -g polymer-cli
[diff]
Create a new directory for your project:
$ mkdir shopping-list-polymer-pouchdb
Change into your new directory:
$ cd shopping-list-polymer-pouchdb
Run the following command for initializing a new Polymer project using the polymer-2-starter-kit
template:
$ polymer init polymer-2-starter-kit
info: Running template polymer-init-polymer-2-starter-kit:app...
info: Finding latest ^3.0.0 release of PolymerElements/polymer-starter-kit
info: Downloading v3.1.0 of PolymerElements/polymer-starter-kit
info: Unpacking template files
info: Finished writing template files
I'm all done. Running bower install for you to install the required dependencies. If this fails, try running the command yourself.
Note: As mentioned previously, subsequent lines not beginning with a $
in examples like the one above indicate output from the previous command. You should not type these lines.
You will see a bunch of additional output as the Polymer CLI installs Bower dependencies.
Note: Polymer 2.0 uses Bower to manage frontend dependencies even though Bower is deprecated. Polymer 3.0 is moving from Bower to npm.
Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Starter Kit app. The Starter Kit app:
- Uses a responsive, drawer-based app layout
- Uses modular, client-side routing
- Uses a Service Worker to cache content and assets for offline access
Close the browser tab containing the Starter Kit app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
[diff]
Our first step in transforming this app from the Starter Kit into a Shopping List app is to update the app title, description, and related metadata. In manifest.json
change:
"name": "My App",
"short_name": "My App",
"description": "My App description",
to:
"name": "Shopping List",
"short_name": "Shopping List",
"description": "Shopping List is an Offline First demo Progressive Web App built using Polymer and PouchDB.",
Note: When viewing and editing files, you will want to use a text editor such as Sublime Text or Atom.
The manifest.json
file provides basic metadata about your app to web browsers.
In index.html
change:
<title>My App</title>
<meta name="description" content="My App description">
to:
<title>Shopping List</title>
<meta name="description" content="Shopping List is an Offline First demo Progressive Web App built using Polymer and PouchDB.">
Also in index.html
change:
<meta name="application-name" content="My App">
to:
<meta name="application-name" content="Shopping List">
Still in index.html
change:
<meta name="apple-mobile-web-app-title" content="My App">
to:
<meta name="apple-mobile-web-app-title" content="Shopping List">
The index.html
file serves as the app entrypoint, which is responsible for instantiating the app shell.
In src/my-app.html
change:
<div main-title>My App</div>
to:
<div main-title>Shopping List</div>
Note: A forward slash (/
) in a file reference indicates that the file or directory following the forward slash is within the preceding directory. For example, src/my-app.html
means that the my-app.html
file is within the src
directory.
The src/my-app.html
file serves as the app shell, which is responsible for routing within your app and may also include the main navigation elements for your app.
[diff]
The Starter Kit comes with three example views (view1
, view2
, and view3
). We will delete these views (and a corresponding test) as we will not be using them:
$ rm src/my-view1.html
$ rm src/my-view2.html
$ rm src/my-view3.html
$ rm test/my-view1.html
Note: As mentioned previously, the four $
instances above indicate four separate commands for you to type (but do not type the $
at the beginning of each line), hitting enter after typing each of the four separate commands.
Remove the following three lines from polymer.json
:
"src/my-view1.html",
"src/my-view2.html",
"src/my-view3.html",
The polymer.json
file stores information about your project structure and your desired build configuration(s). The fragments
property (the property from which we are removing the references to the deleted views) is a way to specify components that may be lazy-loaded.
Remove the following three lines from src/my-app.html
:
<link rel="lazy-import" href="my-view1.html">
<link rel="lazy-import" href="my-view2.html">
<link rel="lazy-import" href="my-view3.html">
The above lines lazy-load the referenced components on demand.
Also in src/my-app.html
remove the following three lines:
<a name="view1" href="[[rootPath]]view1">View One</a>
<a name="view2" href="[[rootPath]]view2">View Two</a>
<a name="view3" href="[[rootPath]]view3">View Three</a>
The above lines represent navigational links to the referenced routes.
Still in src/my-app.html
remove the following three lines:
<my-view1 name="view1"></my-view1>
<my-view2 name="view2"></my-view2>
<my-view3 name="view3"></my-view3>
The above lines represent the pages for the referenced components.
Finally in src/my-app.html
change:
// Deault to 'view1' in that case.
this.page = page || 'view1';
to:
// Deault to 'view404' in that case.
this.page = page || 'view404';
[diff]
Install the paper-card
element:
$ bower install --save PolymerElements/paper-card#^2.0.0
Install the paper-fab
element:
$ bower install --save PolymerElements/paper-fab#^2.0.0
Install the iron-icons
element:
$ bower install --save PolymerElements/iron-icons#^2.0.0
Create a new file named my-lists.html
in the src
directory (src/my-lists.html
). This will be a new component called MyLists
. Here is the content for this new file:
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<link rel="import" href="../bower_components/paper-card/paper-card.html">
<link rel="import" href="../bower_components/paper-fab/paper-fab.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<dom-module id="my-lists">
<template>
<style>
:host {
display: block;
padding: 8px 8px;
}
paper-card {
width: 100%;
}
paper-fab {
position: fixed;
right: 16px;
bottom: 16px;
}
</style>
<paper-card heading="Groceries">
</paper-card>
<paper-fab mini icon="add"></paper-fab>
</template>
<script>
class MyLists extends Polymer.Element {
static get is() { return "my-lists"; }
}
window.customElements.define(MyLists.is, MyLists);
</script>
</dom-module>
This new MyLists
component:
- Is a Polymer element
- Is a Web Component by extension
- Contains a
paper-card
(Material Design card) element representing a stubbed out shopping list titled "Groceries" - Contains a
paper-fab
(Material Design floating action button) element that will be used later for adding a new shopping list
Add the following line to the fragments
property of the polymer.json
before the line containing "src/my-view404.html"
:
"src/my-lists.html",
Add the following line to the src/my-app.html
file before the line containing <link rel="lazy-import" href="my-view404.html">
(this will allow for lazy loading of our new MyLists
component):
<link rel="lazy-import" href="my-lists.html">
Add the following line to the src/my-app.html
file between the opening and closing <iron-selector>
tags (this will add a link to our new MyLists
component in the app's navigation):
<a name="index" href="[[rootPath]]lists">Lists</a>
Add the following line to the src/my-app.html
file before the line containing <my-view404 name="view404"></my-view404>
(this will add our new MyLists
component to the app):
<my-lists name="lists"></my-lists>
Finally, let's change the default view from view404
to lists
. In the src/my-app.html
file change:
// Deault to 'view404' in that case.
this.page = page || 'view404';
to:
// Deault to 'lists' in that case.
this.page = page || 'lists';
[diff]
Install the paper-listbox
element:
$ bower install --save PolymerElements/paper-listbox#^2.0.0
Install the paper-item
element:
$ bower install --save PolymerElements/paper-item#^2.0.0
Install the paper-checkbox
element:
$ bower install --save PolymerElements/paper-checkbox#^2.0.0
Create a new file named my-items.html
in the src
directory (src/my-items.html
). This will be a new component called MyItems
. Here is the content for this new file:
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<link rel="import" href="../bower_components/paper-listbox/paper-listbox.html">
<link rel="import" href="../bower_components/paper-item/paper-item.html">
<link rel="import" href="../bower_components/paper-item/paper-item-body.html">
<link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="../bower_components/paper-fab/paper-fab.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<dom-module id="my-items">
<template>
<style>
:host {
display: block;
}
paper-item[data-checked] {
text-decoration: line-through;
color: var(--paper-item-disabled-color, var(--disabled-text-color));
}
paper-fab {
position: fixed;
right: 16px;
bottom: 16px;
}
</style>
<paper-listbox>
<paper-item data-checked>
<paper-checkbox checked></paper-checkbox>
<paper-item-body>
<div>Mangos</div>
</paper-item-body>
</paper-item>
<paper-item>
<paper-checkbox ></paper-checkbox>
<paper-item-body>
<div>Oranges</div>
</paper-item-body>
</paper-item>
<paper-item>
<paper-checkbox></paper-checkbox>
<paper-item-body>
<div>Pears</div>
</paper-item-body>
</paper-item>
</paper-listbox>
<paper-fab mini icon="add"></paper-fab>
</template>
<script>
class MyItems extends Polymer.Element {
static get is() { return "my-items"; }
}
window.customElements.define(MyItems.is, MyItems);
</script>
</dom-module>
This new MyItems
component:
- Is a Polymer element
- Is a Web Component by extension
- Contains a
paper-listbox
element withpaper-item
elements (Material Design lists) representing stubbed out shopping list items, one of which is checked - Contains a
paper-fab
(Material Design floating action button) element that will be used later for adding a new shopping list item
Add the following line to the fragments
property of the polymer.json
after the line containing "src/my-lists.html",
:
"src/my-items.html",
Add the following line to the src/my-app.html
file after the line containing <link rel="lazy-import" href="my-lists.html">
(this will allow for lazy loading of our new MyItems
component):
<link rel="lazy-import" href="my-items.html">
Add the following line to the src/my-app.html
file after the line containing <my-lists name="lists"></my-lists>
(this will add our new MyItems
component to the app while binding its route to the app's subroute):
<my-items name="items" route="{{subroute}}"></my-items>
Now let's take a look at our work so far! Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app with a stubbed out shopping list and shopping list items (you will need to manual navigate to the /items
route for now to preview this page). When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
Note: Consider testing your work in Google Chrome as Chrome tends to have good support for web platform features used by Progressive Web Apps, plus Chrome has several useful developer tools.
A domain model for the Shopping List app has already been implemented for you in JavaScript. Rather than writing the domain logic and persistence logic yourself, you can instead use this domain model implementation in your Shopping List app. The domain model includes the following:
- Shopping List Factory (
ShoppingListFactory
)newShoppingList(values)
: Makes a new Shopping List entity (an Immutable.js Record) based on the supplied valuesnewListOfShoppingLists(shoppingLists)
: Makes a new List of Shopping Lists (an Immutable.js List) out of the supplied collection-like objectnewShoppingListItem(values, shoppingList)
: Makes a new Shopping List Item entity (an Immutable.js Record) based on the supplied valuesnewListOfShoppingListItems(shoppingListItems)
: Makes a new List of Shopping List Items (an Immutable.js List) out of the supplied collection-like object
- Shopping List Repository for PouchDB (
ShoppingListRepositoryPouchDB
)constructor(db)
: Constructs a new Shopping List Repository for PouchDB that uses the supplied PouchDB databaseensureIndexes()
: Returns a Promise that resolves with an assurance that indexes needed for Mango queries are in place- Methods for Persisting Shopping Lists
put(shoppingList)
: Returns a Promise that resolves to a Shopping List entity persisted to PouchDBputBulk(shoppingLists)
: Returns a Promise that resolves to a List of Shopping Lists persisted to PouchDBget(shoppingListId)
: Returns a Promise that resolves to a Shopping List entity retrieved from PouchDB matching the supplied identifierfind(request)
: Returns a Promise that resolves to a List of Shopping Lists retrieved from PouchDB matching the supplied Mango query requestdelete(shoppingList)
: Returns a Promise that resolves to a Shopping List entity deleted from PouchDB
- Methods for Persisting Shopping List Items
putItem(shoppingListItem)
: Returns a Promise that resolves to a Shopping List Item entity persisted to PouchDBputItemsBulk(shoppingListItems)
: Returns a Promise that resolves to a List of Shopping List Items persisted to PouchDBgetItem(shoppingListItemId)
: Returns a Promise that resolves to a Shopping List Item entity retrieved from PouchDB matching the supplied identifierfindItems(request)
: Returns a Promise that resolves to a List of Shopping List Items retrieved from PouchDB matching the supplied Mango query requestfindItemsCountByList(request, fields)
: Returns a Promise that resolves to the count of a List of Shopping List Items grouped by Shopping List retrieved from PouchDB matching the supplied Mango query requestdeleteItem(shoppingListItem)
: Returns a Promise that resolves to a Shopping List Item entity deleted from PouchDBdeleteItemsBulk(shoppingListItems)
: Returns a Promise that resolves to a List of Shopping List Items deleted from PouchDBdeleteItemsBulkByFind(request)
: Returns a Promise that resolves to a List of Shopping List Items deleted from PouchDB matching the supplied Mango query request.
[diff]
The shopping list domain model is published to npm. However, we are using Bower to install our app's frontend dependencies. Fortunately there is a Bower npm resolver that we can use to bridge this gap. Install the Bower npm resolver:
npm install -g bower-npm-resolver
Configure Bower to use the npm resolver by creating a .bowerrc
file and adding the following content:
{
"resolvers": [
"bower-npm-resolver"
]
}
You should now be able to install the shopping list model using Bower:
bower install --save npm:ibm-shopping-list-model
[diff]
The easiest way to use the domain model in your app is by encapsulating it in its own component. This will allow you to import the component wherever it is needed. Create a shopping-list-model.html
file in the src
directory (src/shopping-list-model.html
) and add the following content:
<dom-module id="shopping-list-model">
<script src="../bower_components/ibm-shopping-list-model/dist/bundle.es6.js"></script>
</dom-module>
[diff]
Now we will replace the stubbed out shopping list in src/my-lists.html
with a one-way data binding to a List of Shopping Lists.
In src/my-lists.html
add the following line to import the shopping list model component after <link rel="import" href="../bower_components/iron-icons/iron-icons.html">
:
<link rel="import" href="shopping-list-model.html">
Also in src/my-lists.html
replace:
<paper-card heading="Groceries">
</paper-card>
with:
<template is="dom-repeat" items="[[listOfShoppingListsArray]]">
<a name="index" href="[[rootPath]]items/[[item._id]]">
<paper-card heading="[[item.title]]">
</paper-card>
</a>
</template>
An explanation:
- The first line is a template repeater (
dom-repeat
) which binds to thelistOfShoppingListsArray
property and creates a new instance of the template contents for each element in the array, creating anitem
and anindex
property which can be used in each instance - The second line provides a hyperlink to view the List of Shopping List Items for the current Shopping List, using the
item._id
property - The third line creates a
paper-card
with aheading
property having the value of the currentitem.title
property - Note that use of double square brackets (
[[ ]]
) which indicates one-way data binding, versus double curly brackets ({{ }}
) which are used for two-way data binding
Next we need to declare the listOfShoppingListsArray
property in order for the above template repeater to have something to which to bind. We will be declaring the listOfShoppingListsArray
property as a computed property (a property that has its value computed based on a function). We will compute the listOfShoppingListsArray
property's value using the toArray()
method of the List of Shopping Lists Immutable.js List object. We will use the Shopping List Factory to create an empty List of Shopping Lists for now.
First let's set up our property declarations. In src/my-lists.html
after static get is() { return "my-lists"; }
add:
static get properties() {
return {
};
}
Add the following property declaration:
shoppingListFactory: {
type: Object,
readOnly: true,
notify: false,
value: function() {
return new ShoppingListModel.ShoppingListFactory();
}
},
An explanation:
- The property name is
shoppingListFactory
- The property type is
Object
- The property is read only, meaning it only produces data and never consumes data
- Since we don't intend to change the property value, we don't need to be notified with an event when the property value changes
- Earlier we imported a bundle from the shopping list domain model that makes a library named
ShoppingListModel
available in the global context - The initial value of the
shoppingListFactory
property will be a newShoppingListFactory
object (accessed via theShoppingListModel
library in the global context)
Add another property declaration:
listOfShoppingLists: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return this.shoppingListFactory.newListOfShoppingLists();
}
},
An explanation:
- The above property represents the current List of Shopping Lists for the
MyLists
component - The property's initial value is an empty List of Shopping Lists, but its value can be changed using a setter method named
_setListOfShoppingLists
generated by Polymer (see the Polymer documentation on read-only properties)
Add the listOfShoppingListsArray
property declaration (the property to which our template repeater is bound):
listOfShoppingListsArray: {
type: Array,
readOnly: true,
notify: true,
computed: "_listOfShoppingListsArray(listOfShoppingLists)"
},
An explanation:
- The
computed
field indicates the name of the method to be called in order to compute the value oflistOfShoppingListsArray
(we still need to write this method) - The
_listOfShoppingListsArray
method takes alistOfShoppingLists
parameter, which means the value of thelistOfShoppingLists
property will be passed to this method and thelistOfShoppingListsArray
value will be re-computed whenever thelistOfShoppingLists
property value changes
Finally let's add the _listOfShoppingListsArray
method after the property declarations block:
_listOfShoppingListsArray(listOfShoppingLists) {
return listOfShoppingLists.toArray();
}
[diff]
When the List of Shopping Lists is empty we should display an empty state indicator. Add the following style to the <style>
section of the MyLists
component (src/my-lists.html
) in order to style the empty state indicator:
div.empty-state {
text-align: center;
margin-top: 120px;
}
Add the empty state indicator before <template is="dom-repeat" items="[[listOfShoppingListsArray]]">
:
<template is="dom-if" if="[[listOfShoppingListsIsEmpty]]">
<div class="empty-state">You have no shopping lists</div>
</template>
An explanation:
- The first line is a conditional templates (
dom-if
) which binds to thelistOfShoppingListsIsEmpty
property and only displays the template contents if the property's value istrue
. - Note that use of double square brackets (
[[ ]]
) again which indicates one-way data binding, versus double curly brackets ({{ }}
) which are used for two-way data binding
Add the listOfShoppingListsIsEmpty
property declaration (the property to which our conditional template is bound):
listOfShoppingListsIsEmpty: {
type: Boolean,
readOnly: true,
notify: true,
computed: "_listOfShoppingListsIsEmpty(listOfShoppingLists)"
},
An explanation:
- The
computed
field indicates the name of the method to be called in order to compute the value oflistOfShoppingListsIsEmpty
(we still need to write this method) - The
_listOfShoppingListsIsEmpty
method takes alistOfShoppingLists
parameter, which means the value of thelistOfShoppingLists
property will be passed to this method and thelistOfShoppingListsIsEmpty
value will be re-computed whenever thelistOfShoppingLists
property value changes
Finally let's add the _listOfShoppingListsIsEmpty
method after our _listOfShoppingListsArray(listOfShoppingLists)
method :
_listOfShoppingListsIsEmpty(listOfShoppingLists) {
return listOfShoppingLists.isEmpty();
}
All the _listOfShoppingListsIsEmpty
method does is return the result of calling the isEmpty()
method on the Immutable.js List object that represents the List of Shopping Lists.
Let's take a look at our empty state indicator. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app with an empty state indicator on the shopping lists page. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
[diff]
Let's add some stub data to the MyLists
component so that we can see the data binding in action. Add the following to the src/my-lists.html
file after the _listOfShoppingListsIsEmpty(listOfShoppingLists)
method:
ready() {
super.ready();
let shoppingList01 = this.shoppingListFactory.newShoppingList({
title: "Groceries"
});
let shoppingList02 = this.shoppingListFactory.newShoppingList({
title: "Camping Supplies"
});
let listOfShoppingLists = this.shoppingListFactory.newListOfShoppingLists([
shoppingList01,
shoppingList02
]);
this._setListOfShoppingLists(listOfShoppingLists);
}
An explanation:
- The
ready
method is a Polymer lifecycle callback which is used for one-time configuration of the component - Since we are overriding an existing method, we need to call the parent method with
super.ready()
to ensure that the component is properly initialized - We create two different Shopping List entities (
shoppingList01
andshoppingList02
) using thenewShoppingList(values)
method of the Shopping List Factory - We create a new List of Shopping Lists using the
newListOfShoppingLists(shoppingLists)
method of the Shopping List Factory - We change the value of the
listOfShoppingLists
property using the_setListOfShoppingLists(value)
setter method generated by Polymer (see the Polymer documentation on read-only properties) which will cascade to also change the values of thelistOfShoppingListsArray
and thelistOfShoppingListsIsEmpty
properties
Let's take a look at our MyLists
component with stub data. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app with a "Groceries" and a "Camping Supplies" shopping list on the shopping lists page. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
[diff]
We should display a loading spinner when the MyLists
component is loading its List of Shopping Lists. This will become more important when we are loading data from a database. Even then, since this is an Offline First app it should load data very fast as all data access happens locally.
Install the paper-spinner
element:
$ bower install --save PolymerElements/paper-spinner#^2.0.0
In src/my-lists.html
add the following line to import the paper-spinner
component after <link rel="import" href="../bower_components/iron-icons/iron-icons.html">
:
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
Add the following style to the <style>
section of src/my-lists.html
in order to style the loading spinner:
#paperSpinnerContainer {
text-align: center;
margin-top: 120px;
}
Within src/my-lists.html
add the loading spinner wrapped in a conditional template bound to the loading
property (which we still need to create):
<template is="dom-if" if="[[loading]]">
<div id="paperSpinnerContainer">
<paper-spinner active></paper-spinner>
</div>
</template>
Within src/my-lists.html
wrap the empty state indicator and the template repeater in a second conditional template bound to the negated state of the loading
property (only the first and last lines in the below code snippet are new):
<template is="dom-if" if="[[!loading]]">
<template is="dom-if" if="[[listOfShoppingListsIsEmpty]]">
<div class="empty-state">You have no shopping lists</div>
</template>
<template is="dom-repeat" items="[[listOfShoppingListsArray]]">
<a name="index" href="[[rootPath]]items/[[item._id]]">
<paper-card heading="[[item.title]]">
</paper-card>
</a>
</template>
</template>
Within src/my-lists.html
add the loading
property declaration (the property to which our conditional templates are bound):
loading: {
type: Boolean,
notify: true,
value: true
},
Let's take a look at our MyLists
component with a loading spinner. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app with a loading spinner on the shopping lists page that will continue to spin indefinitely. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
Let's stop the loading indicator once we've set our List of Shopping Lists. Within src/my-lists.html
add the following as the last line of the ready()
method:
this.loading = false;
In the next steps we will install and use a PouchDB database to persist data within the frontend app. PouchDB is a JavaScript database that syncs. PouchDB stores its data in IndexedDB (other adapters, such as WebSQL, are available for environments that don't support IndexedDB). Data will be persisted in the browser when the app is closed, and available again when the app is re-opened. Data stored in a PouchDB database can be sync'ed to and from any other database that implements the CouchDB Replication Protocol.
[diff]
Install PouchDB:
$ bower install --save pouchdb
Install the pouchdb-find plugin which will give us the ability to perform Mango queries (a query language inspired by MongoDB):
$ bower install --save pouchdb-find
[diff]
The easiest way to use PouchDB and pouchdb-find in your app is by encapsulating the two libraries in their own component. This will allow you to import the component wherever it is needed. Create a pouchdb.html
file in the src
directory (src/pouchdb.html
) and add the following content:
<dom-module id="pouchdb">
<script src="../bower_components/pouchdb/dist/pouchdb.js"></script>
<script src="../bower_components/pouchdb-find/dist/pouchdb.find.js"></script>
</dom-module>
[diff]
Rather than create a PouchDB database instance in each of our app's components, we will create one database instance that is shared between components.
In src/my-app.html
add the following line to import the PouchDB component (created in the previous step) after <link rel="import" href="my-icons.html">
:
<link rel="import" href="pouchdb.html">
In src/my-app.html
add the following property declaration:
db: {
type: Object,
readOnly: true,
notify: false,
value: function() {
return new PouchDB("shopping-list", { storage: "persistent" });
}
},
Bind the db
property to the MyLists
and MyItems
components using one-way data binding (which will add a db
property to each of these components) by changing:
<my-lists name="lists"></my-lists>
<my-items name="items" route="{{subroute}}"></my-items>
to:
<my-lists name="lists" db="[[db]]"></my-lists>
<my-items name="items" route="{{subroute}}" db="[[db]]"></my-items>
The only difference between the two is the addition of a db="[[db]]"
attribute/value pair.
[diff]
In src/my-lists.html
declare a shoppingListRepository
property for our Shopping List Repository instance:
shoppingListRepository: {
type: Object,
readOnly: true,
notify: false,
value: function() {
return new ShoppingListModel.ShoppingListRepositoryPouchDB(this.db);
}
},
In src/my-lists.html
create a _findListOfShoppingLists()
method for finding a List of Shopping Lists in PouchDB (via the Shopping List Repository) and updating the listOfShoppingLists
property accordingly:
_findListOfShoppingLists() {
this.loading = true;
this.shoppingListRepository.find().then(listOfShoppingLists => {
this._setListOfShoppingLists(listOfShoppingLists);
this.loading = false;
});
}
In src/my-lists.html
remove the ready()
method that creates stub data and replace it with the following ready()
method that ensures that the indexes needed for Mango queries are in place and triggers a database query when the component is ready:
ready() {
super.ready();
this.shoppingListRepository.ensureIndexes();
this._findListOfShoppingLists();
}
Let's test our work so far. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app. You may notice the loading spinner displayed briefly, after which you should see the empty state indicator. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
[diff]
We should update our List of Shopping Lists displayed when data changes within PouchDB after the initial load of the MyLists
component. Fortunately PouchDB provides an API for listening to database changes. In src/my-lists.html
declare a dbChanges
property that we can use if we want to cancel our listeners or add listeners:
dbChanges: {
type: Object,
notify: false
},
In src/my-lists.html
at the end of the ready()
method add the following code:
this.dbChanges = this.db.changes({
live: true,
selector: {
type: "list"
}
}).on("change", change => {
this._findListOfShoppingLists();
});
An explanation:
- The
live
option tells PouchDB we want to be notified of all future changes until cancelled - The
selector
option is a Mango selector that allows us to filter by documents matching this selector - Whenever a change is received we call the
_findListOfShoppingLists()
method to refresh our List of Shopping Lists
[diff]
We obviously need the ability to create new Shopping List entities. First we will create a dialog and a form for creating a new Shopping List. Then in the next section we will write the code for handling the submission of this form.
Install the paper-dialog
element:
$ bower install --save PolymerElements/paper-dialog#^2.0.0
Install the iron-form
element:
$ bower install --save PolymerElements/iron-form#^2.0.0
Install the paper-input
element:
$ bower install --save PolymerElements/paper-input#^2.0.0
Install the paper-button
element:
$ bower install --save PolymerElements/paper-button#^2.0.0
In src/my-lists.html
add the following lines to import the paper-dialog
, iron-form
, paper-input
, and paper-button
components after <link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
:
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../bower_components/iron-form/iron-form.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
Add the following style to the <style>
section of the MyLists
component (src/my-lists.html
) in order to style the dialog:
paper-dialog {
width: 332px;
padding: 8px;
}
In src/my-lists.html
add the the following code for the dialog after <paper-fab mini icon="add"></paper-fab>
:
<paper-dialog id="listAddDialog">
<h2>New List</h2>
<paper-dialog-scrollable>
<iron-form id="listAddForm">
<paper-input id="newListTitle" name="title" label="Title" value="{{newList.title}}" required autofocus></paper-input>
</iron-form>
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-dismiss raised>Cancel</paper-button>
<paper-button dialog-confirm raised>Save</paper-button>
</div>
</paper-dialog>
In src/my-lists.html
declare a newList
property to represent the new Shopping List entity as entered by the app user:
newList: {
type: Object,
notify: false,
value: {}
},
In src/my-lists.html
add an on-click
handler to the floating action button, replacing:
<paper-fab mini icon="add"></paper-fab>
with:
<paper-fab mini icon="add" on-click="_listAdd"></paper-fab>
In src/my-lists.html
add the _listAdd()
method to open the dialog that will be called when the floating action button is clicked:
_listAdd() {
this.$.listAddDialog.open();
}
[diff]
The final step to be able to create a new Shopping List entity is to wire up the form submission. In src/my-lists.html
add an on-click
handler to the "Save" button in the form, replacing:
<paper-button dialog-confirm raised>Save</paper-button>
with:
<paper-button dialog-confirm raised on-click="_listAddFormSubmit">Save</paper-button>
In src/my-lists.html
add the _listAddFormSubmit()
method to handle the form submission when the "Save" button is clicked:
_listAddFormSubmit() {
let shoppingList = this.shoppingListFactory.newShoppingList({
title: this.newList.title
});
this.shoppingListRepository.put(shoppingList).then(shoppingList => {
this.$.listAddForm.reset();
this.newList = {};
this.$.listAddDialog.close();
});
}
Let's try it out! Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app. Click the floating action button on the shopping lists page. Enter a title for your new shopping list. Click "Save" and the dialog should close. You should now see your new Shopping List in the List of Shopping Lists rather than the empty state indicator. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
The instructions in this section are for the MyItems
component and are similar to the previous steps completed for the MyLists
component. New concepts covered in this section include observing route changes in Polymer (in order to display the List of Shopping List Items for the currently-selected Shopping List) and updating a document in the PouchDB database (via the Shopping List Repository) based on user action.
[diff]
Now we will replace the stubbed out shopping list items in src/my-items.html
with a one-way data binding to a List of Shopping List Items.
In src/my-items.html
add the following line to import the shopping list model component after <link rel="import" href="../bower_components/iron-icons/iron-icons.html">
:
<link rel="import" href="shopping-list-model.html">
Also in src/my-items.html
replace:
<paper-listbox>
<paper-item data-checked>
<paper-checkbox checked></paper-checkbox>
<paper-item-body>
<div>Mangos</div>
</paper-item-body>
</paper-item>
<paper-item>
<paper-checkbox ></paper-checkbox>
<paper-item-body>
<div>Oranges</div>
</paper-item-body>
</paper-item>
<paper-item>
<paper-checkbox></paper-checkbox>
<paper-item-body>
<div>Pears</div>
</paper-item-body>
</paper-item>
</paper-listbox>
with:
<template is="dom-if" if="[[!listOfShoppingListItemsIsEmpty]]">
<paper-listbox>
<template is="dom-repeat" items="[[listOfShoppingListItemsArray]]">
<paper-item data-checked$="[[item.checked]]">
<paper-checkbox data-id$="[[item._id]]" checked="[[item.checked]]"></paper-checkbox>
<paper-item-body>
<div>[[item.title]]</div>
</paper-item-body>
<iron-icon icon="more-vert"></iron-icon>
</paper-item>
</template>
</paper-listbox>
</template>
Next we need to declare the listOfShoppingListItemsArray
property in order for the above template repeater to have something to which to bind. We will be declaring the listOfShoppingListItemsArray
property as a computed property (a property that has its value computed based on a function). We will compute the listOfShoppingListItemsArray
property's value using the toArray()
method of the List of Shopping List Items Immutable.js List object. We will use the Shopping List Factory to create an empty List of Shopping List Items for now.
First let's set up our property declarations. In src/my-items.html
after static get is() { return "my-items"; }
add:
static get properties() {
return {
};
}
Add the following property declaration:
shoppingListFactory: {
type: Object,
readOnly: true,
notify: false,
value: function() {
return new ShoppingListModel.ShoppingListFactory();
}
},
Add another property declaration:
listOfShoppingListItems: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return this.shoppingListFactory.newListOfShoppingListItems();
}
},
Add the listOfShoppingListItemsArray
property declaration (the property to which our template repeater is bound):
listOfShoppingListItemsArray: {
type: Array,
readOnly: true,
notify: true,
computed: "_listOfShoppingListItemsArray(listOfShoppingListItems)"
},
Add the _listOfShoppingListItemsArray
method after the property declarations block:
_listOfShoppingListItemsArray(listOfShoppingListItems) {
return listOfShoppingListItems.toArray();
}
Add the listOfShoppingListItemsIsEmpty
property declaration (the property to which our conditional template is bound):
listOfShoppingListItemsIsEmpty: {
type: Boolean,
readOnly: true,
notify: true,
computed: "_listOfShoppingListItemsIsEmpty(listOfShoppingListItems)"
},
Finally let's add the _listOfShoppingListItemsIsEmpty
method after the _listOfShoppingListItemsArray
method:
_listOfShoppingListItemsIsEmpty(listOfShoppingListItems) {
return listOfShoppingListItems.isEmpty();
}
[diff]
When the List of Shopping List Items is empty we should display an empty state indicator. Add the following style to the <style>
section of the MyItems
component (src/my-items.html
) in order to style the empty state indicator:
div.empty-state {
text-align: center;
margin-top: 120px;
}
Add the empty state indicator before <template is="dom-if" if="[[!listOfShoppingListItemsIsEmpty]]">
:
<template is="dom-if" if="[[listOfShoppingListItemsIsEmpty]]">
<div class="empty-state">You have no shopping list items</div>
</template>
[diff]
Let's add some stub data to the MyItems
component so that we can see the data binding in action. Add the following to the src/my-items.html
file after the _listOfShoppingListItemsIsEmpty(listOfShoppingListItems)
method:
ready() {
super.ready();
let shoppingList = this.shoppingListFactory.newShoppingList({
title: "Groceries"
});
let shoppingListItem01 = this.shoppingListFactory.newShoppingListItem({
title: "Mangos",
checked: true
}, shoppingList);
let shoppingListItem02 = this.shoppingListFactory.newShoppingListItem({
title: "Oranges"
}, shoppingList);
let shoppingListItem03 = this.shoppingListFactory.newShoppingListItem({
title: "Pears"
}, shoppingList);
let listOfShoppingListItems = this.shoppingListFactory.newListOfShoppingListItems([
shoppingListItem01,
shoppingListItem02,
shoppingListItem03
]);
this._setListOfShoppingListItems(listOfShoppingListItems);
}
Let's take a look at our MyItems
component with stub data. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app. Create a Shopping List if one hasn't already been created. Click on any Shopping List and you should see "Mangos", "Oranges," and "Pears" on the shopping list items page. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
[diff]
We should display a loading spinner when the MyItems
component is loading its List of Shopping List Items. This will become more important when we are loading data from a database. Even then, since this is an Offline First app it should load data very fast as all data access happens locally.
In src/my-items.html
add the following line to import the paper-spinner
component after <link rel="import" href="../bower_components/iron-icons/iron-icons.html">
:
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
Add the following style to the <style>
section of src/my-items.html
in order to style the loading spinner:
#paperSpinnerContainer {
text-align: center;
margin-top: 120px;
}
Within src/my-items.html
add the loading spinner wrapped in a conditional template bound to the loading
property (which we still need to create):
<template is="dom-if" if="[[loading]]">
<div id="paperSpinnerContainer">
<paper-spinner active></paper-spinner>
</div>
</template>
Within src/my-items.html
wrap the empty state indicator and the conditional template for displaying a List of Shopping List Items in another conditional template bound to the negated state of the loading
property (only the first and last lines in the below code snippet are new):
<template is="dom-if" if="[[!loading]]">
<template is="dom-if" if="[[listOfShoppingListItemsIsEmpty]]">
<div class="empty-state">You have no shopping list items</div>
</template>
<template is="dom-if" if="[[!listOfShoppingListItemsIsEmpty]]">
<paper-listbox>
<template is="dom-repeat" items="[[listOfShoppingListItemsArray]]">
<paper-item data-checked$="[[item.checked]]">
<paper-checkbox data-id$="[[item._id]]" checked="[[item.checked]]"></paper-checkbox>
<paper-item-body>
<div>[[item.title]]</div>
</paper-item-body>
<iron-icon icon="more-vert"></iron-icon>
</paper-item>
</template>
</paper-listbox>
</template>
</template>
Within src/my-items.html
add the loading
property declaration (the property to which our conditional templates are bound):
loading: {
type: Boolean,
notify: true,
value: true
},
Let's stop the loading indicator once we've set our List of Shopping List Items. Within src/my-items.html
add the following as the last line of the ready()
method:
this.loading = false;
[diff]
When a user selects a Shopping List from a List of Shopping Lists in the app this will trigger a route change. We need to observe route changes in our MyItems
component and update our List of Shopping List Items to match the currently-selected Shopping List.
In src/my-items.html
after static get is() { return "my-items"; }
(and before the property declarations block) declare the following observer:
static get observers() {
return [
"_routeChanged(route.*)"
]
}
In src/my-items.html
add a shoppingListId
property declaration the value of which will represent the identifier for the currently-selected Shopping List entity:
shoppingListId: {
type: String,
readOnly: true,
notify: true
},
In src/my-items.html
add the _routeChanged(route)
method which will be called whenever the route changes in Polymer and will set the value of the shoppingListId
property accordingly:
_routeChanged(route) {
if (route.base.prefix !== this.rootPath + "items") {
return;
}
this._setShoppingListId(route.base.path.replace(/\//g, ""));
}
[diff]
In src/my-items.html
declare a shoppingListRepository
property for our Shopping List Repository instance:
shoppingListRepository: {
type: Object,
readOnly: true,
notify: false,
value: function() {
return new ShoppingListModel.ShoppingListRepositoryPouchDB(this.db);
}
},
In src/my-items.html
declare a shoppingList
property to represent the currently-selected Shopping List entity:
shoppingList: {
type: Object,
readOnly: true,
notify: true,
},
In src/my-items.html
create a _findShoppingList()
method for finding the currently-selected Shopping List in PouchDB (via the Shopping List Repository) and updating the shoppingList
property accordingly:
_findShoppingList() {
if (this.shoppingListId === undefined) {
this._setShoppingList(undefined);
return;
}
this.shoppingListRepository.get(this.shoppingListId).then(shoppingList => {
this._setShoppingList(shoppingList);
});
}
In src/my-items.html
create a _findListOfShoppingListItems()
method for finding a List of Shopping List Items for the currently-selected Shopping List in PouchDB (via the Shopping List Repository) and updating the listOfShoppingListItems
property accordingly:
_findListOfShoppingListItems() {
this.loading = true;
if (this.shoppingListId === undefined) {
this._setListOfShoppingListItems(this.shoppingListFactory.newListOfShoppingListItems());
return;
}
this.shoppingListRepository.findItems({
selector: {
type: "item",
list: this.shoppingListId
}
}).then(listOfShoppingListItems => {
this._setListOfShoppingListItems(listOfShoppingListItems);
this.loading = false;
});
}
In src/my-items.html
add an observer to the shoppingListId
property (this is a method that will be called whenever the property's value changes) (only the observer: "_shoppingListIdChanged"
part of the code below is new):
shoppingListId: {
type: String,
readOnly: true,
notify: true,
observer: "_shoppingListIdChanged"
},
In src/my-items.html
add the _shoppingListIdChanged(newshoppingListId, oldshoppingListId)
method which will call the _findShoppingList()
and _findListOfShoppingListItems()
methods when the shoppingListId
property value has changed:
_shoppingListIdChanged(newshoppingListId, oldshoppingListId) {
this._findShoppingList();
this._findListOfShoppingListItems();
}
In src/my-items.html
remove the ready()
method that creates stub data and replace it with the following ready()
method that ensures that the indexes needed for Mango queries are in place and triggers the database queries when the component is ready:
ready() {
super.ready();
this.shoppingListRepository.ensureIndexes();
this._findShoppingList();
this._findListOfShoppingListItems();
}
[diff]
We should update our List of Shopping List Items displayed when data changes within PouchDB after the initial load of the MyItems
component. Fortunately PouchDB provides an API for listening to database changes. In src/my-items.html
declare a dbChanges
property that we can use if we want to cancel our listeners or add listeners:
dbChanges: {
type: Object,
notify: false
},
In src/my-items.html
at the end of the ready()
method add the following code:
this.dbChanges = this.db.changes({
live: true,
selector: {
type: "item"
}
}).on("change", change => {
this._findListOfShoppingListItems();
});
[diff]
We will now add the ability to create new Shopping List Item entities. First we will create a dialog and a form for creating a new Shopping List Item. Then in the next section we will write the code for handling the submission of this form.
In src/my-items.html
add the following lines to import the paper-dialog
, iron-form
, paper-input
, and paper-button
components after <link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
:
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../bower_components/iron-form/iron-form.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
Add the following style to the <style>
section of the MyItems
component (src/my-items.html
) in order to style the dialog:
paper-dialog {
width: 332px;
padding: 8px;
}
In src/my-items.html
add the the following code for the dialog after <paper-fab mini icon="add"></paper-fab>
:
<paper-dialog id="listItemAddDialog">
<h2>New List Item</h2>
<paper-dialog-scrollable>
<iron-form id="listItemAddForm">
<paper-input id="newListItemTitle" name="title" label="Title" value="{{newListItem.title}}" required autofocus></paper-input>
</iron-form>
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-dismiss raised>Cancel</paper-button>
<paper-button dialog-confirm raised>Save</paper-button>
</div>
</paper-dialog>
In src/my-items.html
declare a newListItem
property to represent the new Shopping List Item entity as entered by the app user:
newListItem: {
type: Object,
notify: false,
value: {}
},
In src/my-items.html
add an on-click
handler to the floating action button, replacing:
<paper-fab mini icon="add"></paper-fab>
with:
<paper-fab mini icon="add" on-click="_listItemAdd"></paper-fab>
In src/my-items.html
add the _listItemAdd()
method to open the dialog that will be called when the floating action button is clicked:
_listItemAdd() {
this.$.listItemAddDialog.open();
}
[diff]
The final step to be able to create a new Shopping List Item entity is to wire up the form submission. In src/my-items.html
add an on-click
handler to the "Save" button in the form, replacing:
<paper-button dialog-confirm raised>Save</paper-button>
with:
<paper-button dialog-confirm raised on-click="_listItemAddFormSubmit">Save</paper-button>
In src/my-items.html
add the _listItemAddFormSubmit()
method to handle the form submission when the "Save" button is clicked:
_listItemAddFormSubmit() {
let shoppingListItem = this.shoppingListFactory.newShoppingListItem({
title: this.newListItem.title
}, this.shoppingList);
this.shoppingListRepository.putItem(shoppingListItem).then(shoppingListItem => {
this.$.listItemAddForm.reset();
this.newListItem = {};
this.$.listItemAddDialog.close();
});
}
[diff]
Whenever a Shopping List Item is checked or unchecked we want to update the corresponding PouchDB document (via the Shopping List Repository). In src/my-items.html
add an on-checked-changed
handler to the paper-checkbox
element, replacing:
<paper-checkbox data-id$="[[item._id]]" checked="[[item.checked]]"></paper-checkbox>
with:
<paper-checkbox data-id$="[[item._id]]" checked="[[item.checked]]" on-checked-changed="_checkedChanged"></paper-checkbox>
In src/my-items.html
add the _checkedChanged(event)
method to handle the event for checking or unchecking a Shopping List Item and update the corresponding document in PouchDB:
_checkedChanged(event) {
let id = event.currentTarget.dataset.id;
let checked = event.detail.value;
let listItem = this.listOfShoppingListItems.find(item => {
return item._id === id;
});
if (listItem && listItem.checked !== checked) {
listItem = listItem.set("checked", checked);
this.shoppingListRepository.putItem(listItem);
}
}
Install CouchDB 2.1. Instructions are available for installing CouchDB 2.1 on Unix-like systems, on Windows, on Mac OS X, on FreeBSD, and via other methods.
Configure CouchDB for a single-node setup, as opposed to a cluster setup. Once you have finished setting up CouchDB, you should be able to access CouchDB at http://127.0.0.1:5984/
. Ensure that CouchDB is running and take note of your admin username and password.
Sign up for an IBM Cloud account, if you do not already have one.
Once you are logged in to Bluemix, create a new Cloudant instance on the Cloudant NoSQL DB Bluemix Catalog page. This should take you to a page representing the newly-created service instance. Click the "service credentials" link. Click the "New credential" button. Click the "Add" button (you do not need to change the value for the "Name" field). Click "view credentials" next to the newly-created credentials. This should display a JSON object containing your service credentials. Copy the value for the url
key to your clipboard (the value will be in the form of https://username:password@uniqueid-bluemix.cloudant.com
).
Download and install Docker (version 1.9 or above is recommended). Once Docker is installed, download the Cloudant Developer Edition from Docker Hub (this is a fairly large image, so the download may take some time):
$ docker pull ibmcom/cloudant-developer
Start the Docker container:
docker run --detach --volume cloudant:/srv --name cloudant-developer --publish 8080:80 --hostname cloudant.dev ibmcom/cloudant-developer
Note: Instructions are available on the Cloudant Developer Edition for starting the container via Docker Compose.
If you want to stop the Docker container, first list the containers:
$ docker ps --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1b4030e0f6b6 ibmcom/cloudant-developer "supervisord -c /e..." About an hour ago Up 2 minutes 0.0.0.0:8080->80/tcp cloudant
Note: Your output will appear different than the example above.
Find the container ID corresponding to the ibmcom/cloudant-developer
image and run the following command to stop the container (replacing the container ID with your container ID):
$ docker stop 1b4030e0f6b6
1b4030e0f6b6
To start the container again run (replacing the container ID with your container ID):
$ docker start 1b4030e0f6b6
1b4030e0f6b6
[diff]
Install the paper-toast
element:
$ bower install --save PolymerElements/paper-toast#^2.0.0
In src/my-app.html
add the following line to import the paper-toast
component after <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
:
<link rel="import" href="../bower_components/paper-toast/paper-toast.html">
In src/my-app.html
add the the following code for the toast right before the closing </template>
tag:
<paper-toast id="toast"></paper-toast>
In src/my-app.html
declare a remoteDb
and a replicationHandler
property:
remoteDb: {
type: Object,
readOnly: true,
notify: true,
value: function() {
// Uncomment and change parameter value to enable replication (don't forget to enable CORS)
//return new PouchDB("http://admin:password@127.0.0.1:5984/shopping-list");
}
},
replicationHandler: {
type: Object,
notify: false
},
In src/my-app.html
add a ready()
method that will start bi-directional replication between the local PouchDB database and the remote database (if defined) and open the toast with text indicating that live replication with remote database has started:
ready() {
super.ready();
if (this.remoteDb) {
this.replicationHandler = this.db.sync(this.remoteDb, {
live: true,
retry: true
});
this.$.toast.text = "Live replication with remote database started";
this.$.toast.open();
}
}
To enable replication, uncomment the return new PouchDB("…")
line and replace the value passed to the PouchDB constructor with the URL for your remote database. Be sure to create a database named shopping-list
first.
If you are using a local CouchDB database (replace admin
and password
with the values noted in the "Configure a Database" section):
remoteDb: {
type: Object,
readOnly: true,
notify: true,
value: function() {
// Uncomment and change parameter value to enable replication (don't forget to enable CORS)
return new PouchDB("http://admin:password@127.0.0.1:5984/shopping-list");
}
},
If you are using an IBM Cloudant database (replace username
, password
, and uniqueid
with the values noted in the "Configure a Database" section):
remoteDb: {
type: Object,
readOnly: true,
notify: true,
value: function() {
// Uncomment and change parameter value to enable replication (don't forget to enable CORS)
return new PouchDB("https://username:password@uniqueid-bluemix.cloudant.com/shopping-list");
}
},
If you are using a Cloudant Developer Edition database (replace admin
and pass
with the values noted in the "Configure a Database" section):
remoteDb: {
type: Object,
readOnly: true,
notify: true,
value: function() {
// Uncomment and change parameter value to enable replication (don't forget to enable CORS)
return new PouchDB("http://admin:pass@localhost:8080/shopping-list");
}
},
Let's take a look at our finished Shopping List app, with database replication. Start the Polymer development server:
$ polymer serve
info: Files in this directory are available under the following URLs
applications: http://127.0.0.1:8081
reusable components: http://127.0.0.1:8081/components/polymer-starter-kit/
You should now be able to browse to http://127.0.0.1:8081
in your web browser and see the Shopping List app. When you open the app, a toast should open indicating that database replication has started. Open the app in another browser window and place both app instances side-by-side. As you create Shopping Lists, create Shopping List Items, or check or uncheck Shopping List Items you should see the data flow between the two app instances. When you're done, close the browser tab containing the Shopping List app. Back in your terminal, use Ctrl-C
to cancel the polymer serve
command and return you to the command prompt.
Congratulations! You have built an Offline First Progressive Web App with Polymer and PouchDB that replicates its data with a remote database.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
This section of the tutorial is not yet completed.
Additional features to add to the app:
- Handle replication conflicts
- Ability to edit and delete Shopping List and Shopping List Items entities
- Deploy to IBM Cloud
Ways that you can get involved:
- Join the Offline First Slack team
- Follow @OfflineCamp on Twitter
- Read the Offline Camp Medium publication
- Join us at an upcoming Offline Camp
- Offline Sync for Progressive Web Apps – IBM Watson Data Lab
- Voice of InterConnect – IBM Watson Data Lab
- Deploying the Hoodie Tracker demo app to IBM Bluemix
- Hoodie documentation on storing data with IBM Cloudant
- Offline Camp Medium publication
- Offline First resources
- Offline First on YouTube
- Maureen McElaney Presents Go Offline First to Save The World at JSConf EU 2017
- Make&Model (consultancy specializing in user experience design for Offline First apps)
- Neighbourhoodie Software (IBM Business Partner specializing in architecting Offline First apps)