Progressive Web Apps: Empowering Your PWA

1. Welcome

In this lab, you'll take an existing web application and add advanced capabilities to it. This is the sixth in a series of companion codelabs for the Progressive Web App workshop. The previous codelab was Prompting & Measuring Install. There are two more codelabs in this series.

What you'll learn

  • Open and save files from the user's file system using the File System Access API
  • Register your installed PWA as a file handler with the File Handling API
  • Choose the right screen to open a window with using the Multi-Screen Window Placement API
  • Prevent a screen from falling asleep using the Screen Wake Lock API

What you should know

  • JavaScript

What you will need

  • A browser that supports the above APIs. For some APIs, you may need to use a browser with an active Developer Trial or Origin Trial to complete.

2. Get Set Up

Start by either cloning or downloading the starter code needed to complete this codelab:

If you clone the repo, make sure you're on the pwa05--empowering-your-pwa branch. The zip file contains the code for that branch, too.

This codebase requires Node.js 14 or higher. Once you have the code available, run npm ci from the command line in the code's folder in order to install all of the dependencies you'll need. Then, run npm start to start the development server for the codelab.

The source code's README.md file provides an explanation for all distributed files. In addition, the following are the key existing files you'll be working with throughout this codelab:

Key Files

  • js/lib/actions.js - Provides a base class for the menu

Important Architectural Note

Throughout this codelab, you'll be editing js/lib/action.js which manages actions for the different buttons in the app's menu. You can access any property in the initialized menu's constructor, which will include this.editor for an instance of the main text editor. Two important editor methods you'll be using throughout this codelab are:

  • this.editor.setContent(content) - Sets the content of the editor to the provided content argument
  • this.editor.content() - Gets the current content of the editor

3. Manage Files

Opening, saving, and creating new files on a user's computer is now possible thanks to the File System Access API. Combined with the File Handling API, allowing users to open files directly in your PWA, your PWA can feel seamlessly integrated into your user's every day lives.

Open from within the app

The first action to hook up is being able to open a file from the user's filesystem from within the app. In js/lib/actions.js, in the open method of the Actions class, write code that does the following:

  • Open a file picker that will take text/markdown file with extensions .md or .markdown
  • Set the page's title to the open files name, plus PWA Edit
  • Store the file handler under this.handler
  • Set the content of the editor to the text content of the file
  • Save the handler to the settings object store in the settings-store IndexedDB database.

Positive : Remember: class constructors can't be async functions but you can call Promises inside them.

Now that you can open a file and save what file is open between loads, there are two more things you need to do: set the handler back up when the app loads, and unset it when the user resets the app.

To accomplish the first, in the constructor of the Actions class in js/lib/actions.js, do the following:

  • Open the settings-store database
  • Get the saved handler from the settings object store
  • Set this.handler to the retrieved value and the title of page to the handler's file name (plus PWA Edit) if there is a saved handler

In order to reset the state of the app (which can be accomplished with CTRL/CMD+Shift+R), update the reset method of the Actions class in js/lib/actions.js to do the following:

  • Set the document title to PWA Edit
  • Set the editor's content to an empty string
  • Set this.handler to null
  • Delete the saved handler from the settings object store

Open from the user's file system

Now that you can open a file from your app, you should let users open your app with their file! Registering as a file handler for a device will let a user open files in your app from anywhere in their file system.

Negative : You may need to enable a Developer or Origin Trial for this to work. If you need to enable a Developer Trial, it's recommended you do so in a copy of Chrome Canary instead of your normal browser. If you need to enable an Origin Trial, you should register for it as normal and add the tag to index.html

To start, in manifest.json, add a file_handlers entry that does the following:

  • Opens /
  • Accepts text/markdown with .md or .markdown file extensions.

That will allow users to open files with your app, but won't actually open the files in your app. To do so, in the Actions class in js/lib/actions.js, do the following:

  • Add a window.launchQueue consumer in the constructor, calling this.open with the handler, if there are any.
  • Update this.open to accept an optional launch handler
    • If it exists and is an instance of FileSystemFileHandle, use that as the file handler for the function
    • If it doesn't, then open the file picker

After doing both of the above, install your PWA and try opening a file with it from the file system!

Saving a file

There are two different save paths a user may want to take: saving changes to a file already open, or saving to a new file. With the File System Access API, saving to a new file is really creating a new file and getting a file handler back, so to start, let's save from an existing handler.

In the save method in the Actions class in js/lib/actions.js, do the following:

  • Get the handler either from this.handler or, if that doesn't exist, get the saved handler from the database
  • Create the file handler's FileSystemWritableFileStream
  • Write the content of the editor to the stream
  • Close the stream

Once you can save a file, it's time to implement save as. To do so, in the saveAs method in the Actions class in js/lib/actions.js, do the following:

  • Show the save file picker, describing it as a Markdown File and have it accept text/markdown files with a .md extension
  • Set this.handler to the returned handler
  • Save the handler to the settings object store
  • Wait for this.save to finish to save the content to the newly created file

Once you've done that, go back to the save method, check to see if the handler exists before trying to write to it and, if it doesn't, instead wait for this.saveAs to finish.

4. Show a Preview

With a Markdown editor, users want to see a preview of the rendered output. Using the Window Management API, you'll open a preview of the rendered content on the user's primary screen.

Before starting, make a file js/preview.js, and add the following code to it to it to have it display a preview when loaded:

import { openDB } from 'idb';
import { marked } from 'marked';

window.addEventListener('DOMContentLoaded', async () => {
  const preview = document.querySelector('.preview');
  const db = await openDB('settings-store');
  const content = (await db.get('settings', 'content')) || '';

  preview.innerHTML = marked(content);
});

The preview should behave in the following ways:

  • When a user clicks the preview button and a preview isn't open, it should open the preview
  • When a user clicks the preview button and a preview is open, it should close the preview
  • When the user closes or refreshes the PWA, the preview should close

Taking these in order, start by editing the preview method in the Actions class in js/lib/actions.js to do the following:

  • Get the available screens using the Window Management API
  • Filter the screens to find the primary screen
  • Open a window for /preview with a title of Markdown preview that takes up half of the available width, and the whole available height, of the primary screen, positioned so it takes up the full available right half of that screen. The available dimensions exclude reserved areas of the screen, such as a system menubar, toolbar, status, or location.
  • Save this open window to this.previewWindow
  • At the top of the method, check to see if this.previewWindow exists and, if it does, close the window and unset this.previewWindow instead of opening a window preview

Finally, do the following at the end of the constructor of the Actions class in js/lib/actions.js:

  • Close this.previewWindow during the beforeunload event

5. Focus

Finally, we want to offer users a distraction-free writing mode. Distraction free not only means no clutter from other apps, but preventing the user's screen from falling asleep. To do this, you'll use the Screen Wake Lock API.

The wake lock button will work just like the preview button, toggling between the on and off state. To do so, in the focus method of the Actions class in js/lib/actions.js, do the following:

  • Check to see if the document has a full-screen element
  • If it does:
    • Exit full screen
    • If this.wakeLock exists, release the wake lock and reset this.wakeLock
  • If it doesn't:
    • Request a wake lock sentinel and set it to this.wakeLock
    • Request that the document's body go full screen.

6. Congratulations!

You've learned how to manage system files and integrate your PWA with a system using the File System Access API and File Handling API, open windows across different screens with the Window Management API, and prevent a screen from falling asleep with the Screen Wake Lock API.

The next codelab in the series is Service Worker Includes