 March 2003, Nick Fink and Steve Champion stunned the web design world with the concept of progressive enhancement, a strategy for web design that emphasizes core web page content first and that then progressively adds more nuance and technically rigorous layers of presentation and features on top of the web content. While in 2003, progressive enhancement was about using, at the time, modern CSS features, unobtrusive JavaScript, or even scalable vector graphics, progressive enhancement, and 2020 is about using modern browser capabilities. My name is Thomas Diner. I'm a developer advocate based out of the Google Hamburg office. Today, I want to talk about progressively enhancing like it's 2003, building for modern browsers. Since we all can be here together in person due to the coronavirus, I've converted my talk into an online trip that I want to take you on with me. For this trip, you need a solid understanding of JavaScript, talking of JavaScript. The browser support for the latest core JavaScript features is great. Promises, modules, classes, template literals, arrow functions, you name them, all supported. Async functions work across the board in all modern browsers. And even super recent language additions like optional chaining and knowledge coalescing reach support really quickly. When it comes to core JavaScript features, the grass couldn't be much greener than it is today. For the trip that we are going on, we likewise should have a good understanding of progressive web apps. For this talk, I work with a simple PWA called FuguGreetings. The name of this app is a hat tip to Project Fugu, where we work on giving the web all the powers of native applications. You can read more about the project at web.dev.fugu-status. FuguGreetings is a drawing app that allows you to create virtual greeting cards. Just imagine you actually had traveled to Google I.O. and wanted to send a greeting card to your loved ones. Let me recall some of the PWA concepts. FuguGreetings is reliable and fully offline enabled. So even if you don't have network, you can still use it. It can be installed to the home screen of the device and it integrates seamlessly into the operating system as a standalone application. With this out of the way, let's dive into the actual topic of this talk, progressive enhancement. Starting each greeting card from scratch can be really cumbersome. So why not have a feature that allows users to import an image and start from there? With a traditional approach, you'd have used an input type file element to make this happen. First, you'd create the element, set its type and the to be accepted mind types, and then programmatically click it and listen for changes. And it works perfectly fine. The image is imported straight onto the canvas. When there is an input feature, there probably should also be an export feature so users can save their greeting cards locally. Similar to before, the traditional way to saving files is to create an anchor link with the download attribute and with the blob URL as its href. You would then programmatically click it to trigger the download and to prevent memory leaks from happening, hopefully make sure not to forget to revoke the blob URL. But wait a minute. Mentally, you haven't downloaded a greeting card. You have saved it. Rather than showing you a saved dialog that lets you choose where to put the file, the browser instead has directly downloaded the greeting card without interaction and has put it straight into your downloads folder. This isn't great. What if there were a better way? What if you could just open a local file, edit it and then save the modifications, either to a new file or back to the original file that you had initially opened? Turns out, there is a better way. The native file system API allows you to open and create files and directories, make modifications and save them back. Let's see how it can feature detect if the API exists. The native file system API exposes a new method, choose file system entries. I can use this to conditionally load import imageMJS and export imageMJS if the API exists, and if it isn't available, load the files with the legacy approaches from the earlier slides. But before I dive into the native file system API, let me just quickly highlight the progressive enhancement pattern here. On browsers that don't support the native file system API, I load the legacy scripts. You can see the network tabs of Firefox and Safari here. However, on Chrome, only the new scripts are loaded. This is made elegantly possible thanks to dynamic imports that all modern browsers support. As I said earlier, the brass is pretty green these days. Let's look at the actual native file system API based implementation. For importing an image, I call window.chooseFileSystemEnteries and pass it an accept option parameter where I say I want image files. Both file extensions as well as mime types are supported. This results in a file handle. From the file handle, I can obtain the actual file by calling its getFile method. Exploding an image is almost the same, but this time I need to pass a type parameter of saveFile to the chooseFileSystemEnteries method. So I get a file save dialog. Before, this wasn't necessary since openFile is the default. I set the accept parameter similar as before, but this time limited to just PNG images. Again, I get back a file handle, but rather than getting the file, this time I'm creating a writable stream by calling createWritable. Next, I write the block which is my greeting card image to the file. Finally, I close the writable stream. Everything can always fail. The disk could be out of space, there could be a write or read error, or maybe simply the user cancels the file dialog. This is why I always write the calls in a try catch statement. I can now open the file as before. The imported file is drawn right onto the canvas. I can make my edits and finally save them with the real save dialog where I can choose the name and storage location of the file. Now the file is ready to be preserved for the eternity. Apart from storing files for the eternity, maybe I actually want to share my greeting card. This is something that the web share and web share target APIs allow me to do. Mobile and more recently, also desktop operating systems have gained native sharing mechanisms. For example, here's Safari's share sheet on macOS. Triggered from an article on my site, blog.tomeac.com. When you click the share button, you can share a link to the article with a friend. For example, via the native messages app. The code to make this happen is pretty straightforward. I call navigator.share and pass it an optional title, text and URL. But what if I want to attach an image? Level one of the web share API that you can see on the screen doesn't support this yet. The good news is that web share level two has added file sharing capabilities. Let me show you how to make this work with a full greeting card application. First, I need to prepare a data object with a files array consisting of one block and then a title and a text. Next, as a best practice, I make use of the new navigator.canshare method that does what its name suggests. It tells me if the data object I'm trying to share can technically be shared by the browser. If navigator.canshare tells me the data can be shared, I'm in the final step ready to call navigator.share as before. Again, everything can fail in the simplest way when the user cancels the sharing operation. So it's all wrapped and try catch blocks. As before, I use a progressive enhancement loading strategy. If both share and can share exist on a navigator object, only then I go forward and load share.mjs via dynamic import. On browsers like mobile Safari that only fulfill one of the two conditions, I don't load the full functionality. If I tap the share button on a supporting browser, the native share sheet opens. I can, for example, choose Gmail and the email composer widget pops up with the image attached. Up next, I want to talk about contacts. And when I say contacts, I mean contacts as in the device's address book. When you write a greeting card, it may not always be easy to correctly write someone's name. For example, I have a friend who prefers to name to be spelled in Cyrillic letters. I'm using a German Quartz keyboard and I have no idea how to type their name. This is a problem that the contact picker API solves. Since I have my friends stored in my phone's contacts app, where the contacts picker API, I can tap into my contacts from the web. First, I need to specify the list of properties I want to access. In this case, I only want the names, but for other use cases, I might be interested in telephone numbers, emails, avatar icon, or physical addresses. Next, I configure an options object and set multiples to true so I can select more than one account. Finally, I can call navigator.contacts.select, which results in the desired properties once the user selects one or multiple of their contacts. In FuguGreetings, when I tap the contacts button and select my two best pals, Sergey Mikhailovich Brin and Laurence Edward Larry Page, I can see how the contacts picker is limited to only show their names, but not their email addresses or other information like their phone numbers. Their names are then drawn onto my greeting card. And by now, they've probably learned the pattern. I only load the file when the API is actually supported. Up next is copying and pasting. One of our favorite operations as software developers is copy and paste. As greeting card authors at times, I may want to do the same. Either paste an image into a greeting card I'm working on or the other way around, copy my greeting card so I can continue editing it from somewhere else. The async clipboard API, about from text, also supports images. Let me walk you through how I have added copy and paste to the Fugu Greetings app. In order to copy something onto the system's clipboard, I need to write to it. The navigator.clipboard.write method takes an area of clipboard items as a parameter. Each clipboard item essentially is an object with a blob as a value and a blob's type as the key. To paste, I need to loop over the clipboard items that are obtained by calling navigator.clipboard.read. The reason for this is that multiple clipboard items might be on the clipboard in different representations. Each clipboard item has a types field that tells me in which mind type the resource is available. I simply take the first one and call the clipboard items get type method, passing the mind type I obtained before. And almost needless to say by now, I only do this on supporting browsers. So how does this work? Here, I have an image open in the macOS preview app and copy it to the clipboard. When I click paste, the Fugu Greetings app then asks me whether I want to allow the app to see text and images on the clipboard. Finally, after accepting the permission, the images then paste it into the application. The other way around works too. Let me copy a greeting card to the clipboard. When I then open preview and click file and then new from clipboard, the greeting card gets pasted into a new untitled image. Another useful API is the badging API. As an installable PWA, Fugu Greetings of course does have an app icon that users can place on the app dock or the home screen. Something fun to do with it in the context of Fugu Greetings is to use it as a pen stroke counter. With a badging API, it is a straightforward task to do this. I've added an event listener that on pointed down increments the pen strokes counter and sets the icon. Whenever the canvas gets cleared, the counter resets and the badge is removed. In this example, I've drawn the numbers from one to seven using one pen stroke for each number. The badge counter on the icon is now at seven. This features a progressive enhancement so the loading logic is as usual. Want to start each day fresh with something new? A neat feature of the Fugu Greetings app is that it can inspire you each morning with a new background image to start your greeting card. The app uses the periodic background sync API to achieve this. The first step is to register a periodic sync event in the service worker registration. It listens for a sync tag called image of the day and has a minimum interval of one day so the user can get a new background image every 24 hours. The second step is to listen for the periodic sync event in the service worker. If the event tag is the one that was registered a slide ago, the image of the day is retrieved via the get image of the day function and the result propagates it to all clients so they can update their camera assistant caches. Again, this is truly a progressive enhancement so the code is only loaded when the API is supported by the browser. This applies to both the client code and the service worker code. On non-supporting browsers, neither of them is loaded. Note how in the service worker, instead of a dynamic import, I used a classic import script function to the same effect. Sometimes, even with a lot of inspiration, you need a notch to finish a started greeting card. This is a feature that is enabled by the notification triggers API. As a user, I can enter a time when I want to be nudged to finish my greeting card and when that time has come, I will get a notification that my greeting card is waiting. After prompting for the target time, the application schedules the notification with the show trigger. This can be a timestamp trigger with the previously selected target date. The reminder notification will be triggered locally. No network or server side is necessary. As everything else I've shown so far, this is a progressive enhancement so the code is only conditionally loaded. I also want to talk about the Wakelock API. Sometimes you need to just stare long enough on the screen until the inspiration kisses you. The worst that can happen is for the screen to turn off. The Wakelock API can prevent this from happening. In full readings, there's an insomnia checkbox that when checked keeps your screen awake. In the first step, I obtain a Wakelock with the navigator.wakelock.request method. I pass it the string screen to obtain a screen Wakelock. I then add an event listener to be informed when the Wakelock is released. This can happen, for example, when the tab visibility changes. If this happens, I can when the tab becomes visible again, re-obtain the Wakelock. Yes, this is a progressive enhancement so we only need to load it when the browser supports the API. At times, when you stare at the screen for hours, it's just useless. The idle detection API allows the app to detect user idle time. If the user is detected to be idle for too long, the app resets to the initial state and clears the campus. This API is currently gated behind the notification permission. Since a lot of production use cases of idle detection are notification related. For example, to only send a notification to a device, the user is currently actively using. After making sure that the notifications permission is granted, I then instantiate the idle detector. I register an event listener that listens for idle changes, which includes the user and the screen state. The user can be active or idle, and a screen can be unlocked or locked. If the user is detected to be idle, the canvas clears. I give the idle detector a threshold of 60 seconds. And as always, I own it out this code when the browser supports it. Phew, what a ride. So many APIs and just one sample app. And reminder, we never make the user pay the download cost for a feature that their browser doesn't support. By using progressive enhancement, I make sure only the relevant code gets loaded. And since with HTTP tool requests are cheap, this pattern should work well for a lot of applications. Although, at times, you might still want to consider a bundler for really large apps. This has been a short overview of many of the APIs we're working on in the context of project through. Definitely check out our landing page where I can find links to detailed articles for each API that I've talked about. If you're interested in Fugu greetings, go find and fork it on GitHub. And with that, thank you very much for watching my talk. You can find me as Tomajak on GitHub, Twitter and the web in general.