In this codelab, you'll learn how to make a progressive web app using Firebase and Polymer Web Components. You'll connect pre-made view components to Firebase data components to build an app that features user authentication, a database backend, offline data availability and the ability to be installed to the home screen of an Android device.

That may sound like a lot to consider, but worry not: Polymer Web Components make it easy to unlock the power of the web platform. Let's get coding!

You'll learn how to

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would rate your experience with building progressive web apps?

Novice Intermediate Proficient

Bower

First, install the Bower package manager using Node.js and NPM. The Bower package manager will be used to install further dependencies for the browser:

$ npm install -g bower

Firebase CLI

Since this is going to be a Firebase project, let's install the Firebase CLI using Node.js:

$ npm install -g firebase-tools

This will install the Firebase command line tool, which will make it easy to initialize and deploy your project with Firebase.

To verify that the Firebase CLI has been installed correctly, run:

$ firebase version
3.x.x

Make sure that the Firebase version is 3.x.x. If this reads 2.x.x, you still have an older firebase CLI version installed and you may need to fix your PATH. It may also help to add an alias to the NPM installed version of firebase-tools:

$ alias firebase="`npm config get prefix`/bin/firebase"

Since this is a Firebase project, let's initialize our project with the correct Firebase configuration. This is really easy to do with the Firebase CLI. Create a new directory to contain your project, then run this command inside of it:

$ firebase init

After you type this command, Firebase will ask you what features you need. You can safely disable the "Storage" feature for this project:

Select the Firebase project that you created for this codelab.

When Firebase asks about the Firebase Database rules, you can choose the default value, "database.rules.json":

Go ahead and use the default option for the public directory as well:

Even though we'll be making a single-page app today, do not configure your app to rewrite all URLs to /index.html:

Next, install the browser dependencies using bower. For this codelab exercise, all of the necessary views have been built in advance, so you only need one direct dependency:

$ cd public
$ bower install -p polymerlabs/note-app-elements firebase/polymerfire
$ cd ../

This will create a folder called bower_components, which contains all of the external components that your progressive web app depends on.

Next, let's make sure that all of the important files for our project are in place. We'll identify and stub out these files one at a time.

manifest.json

The critical feature of any self-respecting progressive web app is its manifest.json. This is a web app manifest that describes how our app looks and feels when installed on the user's device.

$ touch public/manifest.json

sw-import.js

We are making a progressive web app, so we will be using the browser platform feature called Service Worker. We'll talk more about this later. sw-import.js will be the entrypoint that enables us to initialize our service worker correctly:

$ touch public/sw-import.js

note-app.html

Our actual app implementation will be encapsulated as a Polymer custom element. This custom element will be called <note-app>, so we will keep it in an appropriately named HTML file:

$ touch public/note-app.html

At this point, you will need a static web server to preview the files in the public directory as you are working on them. Any static web server will do, so if you have one you like, point it to the public directory and spin it up.

If you don't have one handy, don't panic! The Firebase tools come with one, and you can run it with this command:

$ firebase serve

After you run this command, you should see the message: Server listening at: http://localhost:5000.

Pop open Chrome or Firefox and navigate to http://localhost:5000 (the port may vary if you are using a custom server or configuration). You should be greeted by the following friendly message from Firebase:

Use your text editor of choice to open the Web App Manifest you created at public/manifest.json. Let's fill it out. I will show you how I filled out mine, but feel free to get creative if you want to tweak things like colors and icons.

Basic configuration

Let's get started with a basic configuration. Below, I have started to fill out my manifest.json with a few fields:

{
  "name": "Notes",
  "scope": "./",
  "start_url": "index.html",
  "display": "standalone",
  "orientation": "any",
  "theme_color": "#9C27B0",
  "background_color": "#9C27B0",

Icon configuration

Every great mobile app needs its signature launcher icon. Your Web App Manifest will let you configure icons just like a native app: you can specify different resolutions and styles of icon for different use cases and screen densities.

The note-app package contains some icons that are ready to use, so I'll just use those in the example:

  "icons": [{
    "src": "bower_components/note-app/common/images/icon-0-75x.png",
    "sizes": "36x36",
    "type": "image/png"
  },
  {
    "src": "bower_components/note-app/common/images/icon-1x.png",
    "sizes": "48x48",
    "type": "image/png"
  },
  {
    "src": "bower_components/note-app/common/images/icon-1-5x.png",
    "sizes": "72x72",
    "type": "image/png"
  },
  {
    "src": "bower_components/note-app/common/images/icon-2x.png",
    "sizes": "96x96",
    "type": "image/png"
  },
  {
    "src": "bower_components/note-app/common/images/icon-3x.png",
    "sizes": "144x144",
    "type": "image/png"
  },
  {
    "src": "bower_components/note-app/common/images/icon-4x.png",
    "sizes": "192x192",
    "type": "image/png"
  }]
}

The icon configuration can be a lot to take in at first, but it's really just the same configuration repeated for different scales of icon.

And that's all you need to do to configure your Web App Manifest. Check out the Web App Manifest spec for more configuration options!

In order for our progressive web app to install to a user's home screen and to be accessible offline, we need to use a new browser feature called service worker.

A brief introduction to Service Worker

Service Worker is a type of Web Worker that runs in the background behind a web app, even when the web app that loaded the service worker is not open or running on the user's device. It is able to do a lot of things, but most importantly for us, it has two amazing features:

  1. It can control how files in a web app are cached.
  2. It can make cached files available even when the app is opened while the user is offline.

Polymer + Service Worker = ๐Ÿ’•

Service Worker is a very powerful feature of the web platform. Thankfully, there are some ready-to-go Polymer components that help us use a service worker without getting into the nitty gritty details of configuring it.

In particular, we are going to use the <platinum-sw> component later on in this codelab to initialize and run a service worker with only a few lines of easy-to-read, easy-to-remember HTML. To take full advantage of service worker, we must create an entry point for it that will live at the root of our application. Hence sw-import.js.

The contents of sw-import.js

Use your text editor to open public/sw-import.js. Once it is open, all you need to add is this:

importScripts('bower_components/platinum-sw/service-worker.js');

As we move further along, the above code will be used to load the service worker associated with the <platinum-sw> Polymer component.

Now we are ready to start working on some actual HTML. And as most web developers can tell you, there is no better place for us to get started writing HTML than index.html.

Open public/index.html in your text editor and take a look at the contents. This HTML document contains boilerplate, which was generated for you when you used the firebase init command.

Discard generated boilerplate

Go ahead and delete most of the boilerplate. When you are done, you should be left with a file that looks something like this:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
    <title>Notes</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
  </body>
</html>

Take note of the new title I gave my HTML document: "Notes". Feel free to give yours any title you like!

Declare your Web App Manifest

Next, we need to declare where the Web App Manifest is. So, Add the following code to the <head> section of your document, preferably just after the <title> tag:

<link rel="manifest" href="manifest.json">

This is all we need to tell the browser about our Web App Manifest.

Older Chrome theme configuration

To target older versions of Chrome, there are additional tags we can use to customize our app.. Let's add these right after the <link rel="manifest"> tag:

<!-- Older Chrome configuration -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Notes">
<link rel="icon"
      sizes="192x192"
      href="bower_components/note-app/common/images/icon-4x.png">

Let's continue working on public/index.html. We can start declaring the core dependencies of our app. Let's add them just before the end of <head>, and then we can review what they are:

<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="bower_components/web-animations-js/web-animations.min.js"></script>
<link rel="import" href="bower_components/platinum-sw/platinum-sw-elements.html">
<link rel="import" href="bower_components/polymerfire/firebase-app.html">
<link rel="import" href="note-app.html">

Dependency breakdown

Now we can play with some Polymer Web Components and configure our service worker. As mentioned earlier, adding a service worker using the <platinum-sw> component is easy and can be done entirely with HTML.

Continuing to edit public/index.html, let's add the service worker configuration right after the opening <body> tag:

<platinum-sw-register
    auto-register
    skip-waiting
    clients-claim
    reload-on-install
    href="sw-import.js">
  <platinum-sw-cache
      default-cache-strategy="networkFirst">
  </platinum-sw-cache>
</platinum-sw-register>

Considering the configuration

The configuration above describes a service worker that will cache all HTTP network requests and ensure that the cache is available even if the app is accessed when the user is offline.

The advantage of using Web Components to configure our service worker is that the entire configuration can be done declaratively. Using simple HTML tags and attributes, we can provide to an underlying JavaScript implementation everything it needs to know to configure and launch our service worker.

Our configuration informs the service worker how to approach booting up and upgrading. We provide the path to the sw-import.js script we created earlier so that it can be loaded. Note that we can even declaratively describe our cache validation strategy!

Firebase Projects each have their own unique set of configuration attributes. Let's locate and save the attributes for your project!

Locating your Firebase Project attributes

Open a web browser tab and go to your Firebase Project's page in the Firebase Console. Click the button highlighted in the image below:

The dialog that appears will show you how to add Firebase to a vanilla web app. Do not use the HTML in the instructions! We only need to copy a small portion of the information presented to us by the dialog. The image below shows the information we care about. Copy the configuration details and save them for the next step:

Another component that we will leverage for declarative configuration is <firebase-app>. This component allows us to initialize a named Firebase app for further use by database and authentication components.

Use <firebase-app> for configuration

Immediately after the closing tag of the <platinum-sw-register> service worker configuration in public/index.html, add another declarative configuration:

<firebase-app
    name="notes"
    api-key="YOUR_API_KEY_HERE"
    auth-domain="YOUR_AUTH_DOMAIN_HERE"
    database-url="YOUR_DATABASE_URL_HERE">
</firebase-app>

You can set the name of the <firebase-app> to whatever you like; I named my app "notes". Keep in mind that you will use this name to refer to your Firebase app elsewhere in this project.

After all of our configuration is done, there is one more thing to add to the <body> of public/index.html:

<note-app></note-app>

This tells the document that we want to instantiate our <note-app> custom element, which will produce the bulk of our web app implementation.

And that's it for public/index.html. The only thing left to do is implement the <note-app> custom element.

Polymer is a helpful library that makes implementing custom elements easy and expressive. We will leverage Polymer to quickly implement our <note-app> custom element. Let's get a quick and dirty implementation going so that we can review our progress in our web browser!

A new element is born

Open public/note-app.html in your favorite text editor. Once you have it open, use the following code as a reference for declaring your own <note-app> custom element with Polymer:

<dom-module id="note-app">
  <template>Hello, note app!</template>
  <script>
    Polymer({
      is: 'note-app'
    });
  </script>
</dom-module>

The wonderful thing about using Polymer to declare our custom element is that it really only takes a few lines et voila! You have extended the web browser with your own custom HTML tag.

Review your handiwork

Go ahead and refresh your browser now. In place of the Firebase boilerplate, you should see a somewhat threadbare (but no less awesome) page featuring the message: Hello, note app!

As mentioned earlier in this codelab, all of the views have been pre-made. This is so that the exercise can focus on the integrating ready-made, reusable components to create a progressive web app with little extra effort. Let's start making use of some of these reusable components!

Declaring element dependencies

For the rest of this step, we will continue editing public/note-app.html. First, let's update it with some new dependency declarations. Add the following HTML to the top of the file:

<link rel="import" href="bower_components/note-app-elements/na-elements.html">
<link rel="import" href="bower_components/app-storage/app-indexeddb-mirror/app-indexeddb-mirror.html">
<link rel="import" href="bower_components/polymerfire/polymerfire.html">

These three HTML imports will fetch all of the dependencies needed to build our <note-app> element.

Next, let's fill out the template for the <note-app> element by appending HTML into the body of the <template> tag we added earlier.

Including shared styles

Remove the text that we added to the <template> in the previous step. Then, append a new <style> tag inside of the <template> tag so that public/note-app.html looks like this:

<link rel="import" href="bower_components/note-app/common/note-app/na-elements.html">
<link rel="import" href="bower_components/app-storage/app-indexeddb-mirror/app-indexeddb-mirror.html">
<link rel="import" href="bower_components/polymerfire/polymerfire.html">

<dom-module id="note-app">
  <template>
    <style include="note-app-shared-styles"></style>
  </template>
  <script>
    Polymer({
      is: 'note-app'
    });
  </script>
</dom-module>

This <style> tag has an attribute called include that refers to note-app-shared-styles. This is a special kind of <style> tag that tells Polymer to include and mix-in the shared styles that have been prepared for this codelab.

Now that we have expressed our dependencies and readied our styles, we can start incorporating views into the <note-app> element's template.

Adding a toolbar

Toolbars are a common feature of most modern apps, so we will start by adding a toolbar. Add the following HTML immediately after the <style> tag in the public/note-app.html:

<na-toolbar></na-toolbar>

Easy. Now, refresh the browser and take a look at your app-in-progress. You should see something like this:

Adding a login screen

Our app requires user authentication, so we will add a login screen. This will be very similar to the work we did to add the toolbar. Add the following HTML immediately after the <na-toolbar> element in public/note-app.html:

<na-login></na-login>

That was just as easy. Let's refresh the browser again and see what it looks like! Your app should now look like this:

We have added some views to our app, but right now they don't do very much. They are pretty, but they aren't interactive. This is because the views are pure UI components. They are missing the core business logic to actually perform the act of logging in; all they can do is tell us about user interactions.

Luckily for us, there is a ready-to-go Firebase custom element that implements the business logic we need for authentication: <firebase-auth>. To add it to our app, append the following HTML as a child of the <template> in public/note-app.html:

<firebase-auth
    id="auth"
    app-name="notes"
    provider="google">
</firebase-auth>

Dissecting the tag

We just added a new, non-visible element to our app and configured it with a few attributes. What do these attributes do?

Taken altogether, the <firebase-auth> component will handle the business logic of interacting with the Firebase authentication APIs.

Before we go any further, we need to jump back to the Firebase Console. We configured our <firebase-auth> element to use the Google provider, so we need to enable the Google authentication provider for our project.

Open the Firebase Console and select the appropriate project from the list of projects. Once you are on the project's Overview page, go to the Auth section of the project page:

From within the Auth section of the project page, navigate to the "SIGN IN METHODS" tab:

When you see the list of authentication providers in front of you, hover over the Google provider and click the pencil icon to edit it:

The only thing we need to do to enable Google authentication is click the "Enable" button so that it is enabled:

Once you have enabled the Google provider, click the "SAVE" button. You have now enabled Google authentication for your app. Firebase takes care of generating all of the necessary API keys for you, so you don't need to do any additional configuration.

We've enabled Google authentication and added a few cool visual elements to our app. But, our user interface still doesn't do anything yet. To make the elements work together, we need to apply some basic implementation to <note-app> and start binding things together using Polymer data-binding syntax.

Applying NoteAppBehavior

Polymer supports sharing common implementation using a feature called behaviors. The sample code for this codelab ships with a basic implementation of the <note-app> JavaScript as Polymer.NoteAppBehavior.

Let's apply the basic behavior to our element. Open public/note-app.html in your text editor and modify the <script> tag contents so that it looks like this:

<script>
  Polymer({
    is: 'note-app',

    behaviors: [Polymer.NoteAppBehavior]
  });
</script>

This basic implementation will help us create relationships between the interactive elements of our app where data-binding is not sufficient.

We have our basic implementation available to us, so now we can start creating relationships between our elements with Polymer data-bindings.

<firebase-auth>

We are going to declare data-bindings one at a time, starting with the <firebase-auth> element. Open public/note-app.html in your text editor and modify the <firebase-auth> element so that it looks like this:

<firebase-auth
    id="auth"
    app-name="notes"
    provider="google"
    signed-in="{{signedIn}}"
    user="{{user}}">
</firebase-auth>

The <firebase-auth> element now produces a few useful values for us:

<na-toolbar>

Now we will modify the <na-toolbar> element to respond to the authentication state of the user. In public/note-app.html, modify <na-toolbar> to look like this:

<na-toolbar
    signed-in="[[signedIn]]"
    on-sign-out="signOut">
</na-toolbar>

Here we are doing two significant things:

  1. We are binding signedIn into the <na-toolbar>, so it can be notified when <firebase-auth> considers the user to be signed in.
  2. We are registering an event listener (signOut, part of the Polymer.NoteAppBehavior implementation) so that we can take action when the user attempts to sign out.

<na-login>

Finally, we are going to connect <na-login> to the <firebase-auth> element. Update the <na-login> element to look like this:

<na-login
    on-sign-in="signIn"
    signed-in="[[signedIn]]"
    disabled="[[!online]]">
</na-login>

There are three significant changes being made to <na-login>:

  1. We are registering an event listener - signIn - so that we can take action when the user attempts to sign in. More on this in the next section.
  2. We bind signedIn into the <na-login> element so that it is aware of the authentication state produced by <firebase-auth>.
  3. We bind a Boolean called online - inherited by <note-app> from Polymer.AppNetworkStatusBehavior - into <na-login> so that logging in will be disabled while the user is offline.

If you were following along, you probably noticed that we registered an event listener that doesn't exist yet: signIn. Since not all authentication elements share the same API, the signIn implementation is left as an exercise for <note-app> authors like us.

Thankfully, <firebase-auth> makes implementing our signIn listener trivial. Open public/note-app.html in your text editor and update your element <script> tag to look like this:

<script>
  Polymer({
    is: 'note-app',

    behaviors: [Polymer.NoteAppBehavior],

    signIn: function() {
      this.$.auth.signInWithPopup();
    }
  });
</script>

Now, whenever the sign-in event is fired by the <na-login> element, our signIn listener will be called. Remember when we gave our <firebase-auth> element an ID a few steps back? Now we can reference the <firebase-auth> element with its ID value by using the $ property of the <note-app> element. Whenever the listener is notified, we call the appropriate method on the <firebase-auth> element to initiate the sign-in process.

Try it out!

Your app should now feature user authentication and a functional user interface for logging in and logging out. Woot! Refresh your browser, and you should be able to interact with the app like this:

Go offline even!

Try disconnecting your development computer from the internet. Did you notice what changed? In a previous step, we bound the network state into <na-toolbar>. So, the cloud icon in the <na-toolbar> updates automatically to let the user know if the app is online or offline:

Users can log in to our app, but the app still isn't very useful. The next thing we will build is a flow that will allow users to create notes. We'll start with the view components.

It begins with a button

User interaction takes many forms, but one of the most common is clicking a button. If a user wants to add a new note to their app, they need a button to click to indicate that intent.

Open public/note-app.html in your text editor and add the following HTML to the <template> tag:

<paper-fab
    icon="add"
    on-tap="create"
    disabled="[[!online]]"
    aria-label="Add note">
</paper-fab>

The <paper-fab> element is a Polymer custom element that implements the Floating Action Button prescribed by Material Design. Here is a breakdown of the attributes we configured for this element:

  1. The <paper-fab> element doesn't have a visible text label, opting instead for a minimal, centered icon. So, we used the icon attribute to configure the <paper-fab> element with an "add" icon.
  2. Polymer elements fire a "tap" event whenever a user clicks or touches a button. We declare that the create listener of <note-app> - inherited from Polymer.NoteAppBehavior - should be notified when the user "taps" the <paper-fab>.
  3. When offline, the user data that backs the <note-app> element becomes read-only. We disable the <paper-fab> so that users don't accidentally try to add new notes while offline.
  4. Last, but certainly not least, we provide a text label that describes the button so that screen readers and other media can understand what the button is meant to do without seeing it.

Note authors need an editor

We have a button hooked up so that the user can create a new note, but how does a user actually describe the note to create? Data creation is where the <na-editor> - another ready-made view component - comes in handy.

Continuing to edit public/note-app.html, let's append the HTML for the <na-editor> element to the <template>:

<na-editor
    id="editor"
    note="{{editableNote}}"
    on-close="commitChange">
</na-editor>

The <na-editor> implements a simple form for editing - and committing edits to - plain JavaScript objects that have title and body properties. This is convenient for us, because the models that back our notes will have the same two fields. When configuring the editor we did the following:

  1. We give the editor an ID - "editor" - so that the <note-app> element can refer to it imperatively later using the $ property (just like we did with <firebase-auth> earlier).
  2. Since the <na-editor> operates on note-like JavaScript objects, we bind a value called editableNote, which will become the fulcrum for operating on live Firebase note data in the next step.
  3. When the user is done editing, the <na-editor> fires a "close" event. We declare that the commitChange listener - inherited from Polymer.NoteAppBehavior - should be notified when the editor is closed.

Now that you've added the <paper-fab> element and hooked it up to <na-editor>, refresh your browser and log in to your app. You should be able to open and close a basic note editor:

We have a workable note editor at our disposal, but it doesn't actually do anything. Somehow, the editor needs to store user-provided data in our Firebase database (or be deleted from the database, as the case may be). As you probably guessed, there is an element for that.

Integrating a <firebase-document>

Polymerfire includes an element called <firebase-document> that allows us to tap directly into our Firebase database with ease. With public/note-app.html open in your text editor, add the following HTML to the <template>:

<firebase-document
    id="document"
    app-name="notes"
    path="[[editableNoteId]]"
    data="{{editableNote}}">
</firebase-document>

The <firebase-document> element handles changes to the current note data that is being created or edited. Here is how we configured it:

  1. We give the <firebase-document> an ID attribute so that we can refer to it directly from the $ property of our <note-app>, just like we did for some of our other elements.
  2. We want this <firebase-document> to operate on the same app we named when we configured <firebase-app>, so we specify the app named "notes".
  3. The path attribute represents the path through our Firebase database that points to the note data we want to change. Here we bind editableNoteId to a property that is inherited from Polymer.NoteAppBehavior.
  4. Most importantly, <firebase-document> data is bound to an editableNote value that we can bind to our <na-editor> in order to create and modify data in the Firebase database.

Specialized database qualities

Polymer.NoteAppBehavior is a generalized JavaScript implementation for our <note-app> element created in advance for expediency. It does not have special knowledge of Firebase or how our Firebase database is structured. We need to customize some implementation to describe the shape of our Firebase database for our Firebase-backed <note-app>.

In public/note-app.html, modify the <script> so that the implementation looks like this:

<script>
  Polymer({
    is: 'note-app',

    behaviors: [Polymer.NoteAppBehavior],

    signIn: function() {
      this.$.auth.signInWithPopup();
    },

    get notesPath() {
      return '/notes/' + this.user.uid;
    },

    toEditableId: function(noteId) {
      return this.notesPath + '/' + noteId;
    }
  });
</script>

So how did we specialize our <note-app>? There are two dimensions related to the database structure that we are describing now:

  1. get notesPath(): this accessor describes the root path where all notes will be stored. For our use case, we want notes to be stored in a path that is specific to the type of data ("notes") and the user (via the uid property of the user session object produced by <firebase-auth>).
  2. toEditableId(noteId): this method receives the unique ID of a note in our database, and returns the full path through the database that points to that note data.

Since we have done the hard work of hooking our <na-editor> up to the Firebase database via <firebase-document>, let's take a moment to make sure that it's actually storing data in the database. Go ahead and use the editor to create a note or two:

Don't worry that the notes disappear as soon as you press DONE! If you followed all of the steps correctly, the data is still being saved.

Confirm data in the Firebase Console

In case you don't believe me, you can verify that your database data is available in the Firebase Console. Even if you do believe me, you should do this anyway to make sure everything is hooked up correctly.

Navigate to your project's Overview section, and then on into the Database section:

Under the Database section of the Firebase Console, you should see an expandable tree of data containing the test notes that you created:

We have an element that helps us to create or modify data in the database, but our app should also list notes that have already been created so that the user can review them.

Querying with <firebase-query>

Querying for lists of data is where <firebase-query> comes in really handy. While working on public/note-app.html in your text editor, add a <firebase-query> to our <template>:

<firebase-query
    id="query"
    app-name="notes"
    path="/notes/[[user.uid]]"
    data="{{notes}}">
</firebase-query>

<firebase-query> generates an ordered list of data that corresponds to some path through our Firebase database. Here is how we configure it:

  1. As we have done for several other elements, we give <firebase-query> an ID so that we can reference it directly from the $ property of the <note-app> element.
  2. Similarly to what we did with <firebase-auth> and <firebase-document> we configure the <firebase-query> to use the app named "notes".
  3. We want to query for all of the notes made by the authenticated user, so we set the path accordingly, leveraging the user session object produced by <firebase-auth>.
  4. <firebase-query> produces some data (an array of note data objects), so we bind that out to the notes property.

Persisting Firebase data for offline access

It's great to have access to our notes when we are online, but it can be especially useful to review notes in moments when we do not have any internet access. Unfortunately, if the Firebase SDK can't connect to Firebase servers, it won't be able to access data. That means when the user starts the app in an offline state or refreshes the app while offline, she will lose access to the Firebase data she had loaded while still online.

But you already know what I'm about to say, don't you? Yes: there is a ready-to-go custom element that will solve this problem for us. The element is called <app-indexeddb-mirror>. It is purpose-built to make it easy to cache live data (data queried from a Firebase database, for example) in a local IndexedDB database that will be made available to the user when she uses the app offline.

To incorporate <app-indexeddb-mirror>, open public/note-app.html in your text editor and add the following HTML to the <template>:

<app-indexeddb-mirror
    session="[[user.uid]]"
    key="notes"
    data="{{notes}}"
    persisted-data="{{persistedNotes}}">
</app-indexeddb-mirror>

Configuration for the <app-indexeddb-mirror> element is fairly straightforward:

  1. We provide the current unique user ID as the session attribute of the <app-indexeddb-mirror>. This causes the locally stored IndexedDB database to be erased when the current user logs out. Erasing the data, in turn, ensures that other users cannot spy on the local database to get unauthorized access to data.
  2. Data that is stored in the local IndexedDB database by this element is stored against a specific key. For our current implementation, the key is called "notes".
  3. We bind the notes produced by <firebase-query> to the data observed by <app-indexeddb-mirror>. This tells the element what data is meant to be stored in IndexedDB at the given key.
  4. <app-indexeddb-mirror> produces a new object that represents persisted data that is always available (even when the user is offline). We bind this value to the name persistedNotes.

We are really close to achieving the full-featured note app we always dreamed of. We can create and modify note data. We can also query for a list of that data, and access the queried data when offline. "But wait," you say with trepidation, "why can't I see any of the notes yet?" I'm glad you asked.

Displaying a list of notes

We already have the data we need to present a list of notes. We also have a view ready to go that will present a single note for us called <na-note>. All we need to do is loop over our list of note data and create a <na-note> element to match each note datum.

Open public/note-app.html with your text editor, and append the following HTML to the <template> element:

<div class="notes">
  <template is="dom-repeat" items="[[persistedNotes]]" as="note">
    <na-note
        id$="[[note.$key]]"
        title="[[note.title]]"
        body="[[note.body]]"
        on-tap="edit">
    </na-note>
  </template>
</div>

I'm delighted to inform you that this is the last HTML you will need to write for this codelab. We used <template is="dom-repeat"> to iterate over our list of persistedNotes. Then, we configured a <na-note> element for each note datum:

  1. Each <na-note> receives an ID that matches the key of the note datum in our Firebase database. This allows us to associate the <na-note> view and the note ID during user interactions.
  2. The title of the <na-note> is bound directly to the title property of the note datum.
  3. Similarly, the body of the <na-note> is bound directly to the body property of the note datum.
  4. When the user "taps" the <na-note> element, we will notify the edit listener so that we can edit the corresponding note datum.

Disabling edit when offline

There is one small detail remaining before our app is ready to be deployed. Polymer.NoteAppBehavior has a built-in check to test whether notes can be edited each time the user attempts to edit one. We need to specialize this check for our app, so that the user does not accidentally attempt to edit a note while offline.

Update the <script> tag in public/note-app.html to look like this:

<script>
  Polymer({
    is: 'note-app',

    behaviors: [Polymer.NoteAppBehavior],

    signIn: function() {
      this.$.auth.signInWithPopup();
    },

    get notesPath() {
      return '/notes/' + this.user.uid;
    },

    toEditableId: function(noteId) {
      return this.notesPath + '/' + noteId;
    },

    get isEditable() {
      return this.online;
    }
  });
</script>

In our specialized implementation, we override the get isEditable() accessor to return true only if the app is online. This means that the app will stop being editable when it goes offline.

This concludes the programming portion of this codelab. Refresh the browser and bask in the glory of your newly minted note-keeping app!

You've built a cool app, but before you can become internet famous, you absolutely must find a place to host it. Firebase has your back, with free hosting available on a special domain named specifically for your app. Open a terminal, navigate to your project directory and type:

$ firebase deploy

The Firebase CLI will automatically upload and deploy your whole app to a live internet domain for you to share with the world. Check out my deployed app on Firebase here!

There is nothing left to do but add this sweet puppy to our Android device's home screen. If you have an Android device, launch Chrome and navigate to the URL where Firebase deployed your app.

Once you have loaded your app, pop open the Chrome menu:

Select the menu item "Add to Home screen":

You will be prompted to confirm that you want to add the app to your homescreen. You can optionally change the name of the app before you add it:

Head back to the home screen and marvel at the awesome product of your hard work:

And that's all there is to it. Try launching your app from the home screen; progressive web apps launched from the home screen look and feel just like native apps:

You've built a full-fidelity, offline-capable progressive web app by leveraging the power of reusable Web Components and Firebase. Why bother with a native app when you know how to do all that?!

Oh look, a wild Polymon just wandered into the code lab:

What we've covered

Next steps