Modern Web Weekly #8
Check if your PWA is installed with getInstalledRelatedApps
One of the most important best practices of a PWA is making it as easy as possible for your users to install it on their devices. A great way to accomplish this is by adding a button that installs the app or shows instructions on how to do this.
But when your app is installed as a PWA you don't want to show that button anymore. I mean, did you ever visit a website that asks you to install their native app after you had already installed it?
Probably not, it's a bad user experience.
Luckily, it's easy to hide it inside the PWA by using a media query that queries display-mode
. This property corresponds to the display
key in manifest.json
and should be either standalone
or minimal-ui
to make your PWA feel app-like.
You can then hide the install button like this in your CSS:
@media all and (display-mode: standalone), (display-mode: minimal-ui) {
#install-button {
display: none;
}
}
But how about when users visit your PWA in a browser after they have installed it?
In Chrome on Android, you can now detect inside a web app if it's installed as a PWA. For this, you need to tell the PWA about your website (actually the "web version" of itself) by adding "related_applications" section to its manifest.json
.
This section holds an array of objects with a platform
and url
key. For a web app, platform
should be "webapp" and the url
should point to the manifest.json
file of the web app:
"related_applications": [{
"platform": "webapp",
"url": "https://whatpwacando.today/manifest.json"
}],
In the app, you then call navigator.getInstalledRelatedApps()
which returns a Promise
that resolves with the array you specified in "related_applications" or an empty array when none were found or manifest.json
doesn't contain this section:
let isInstalled = false;
const manifestUrl = 'https://whatpwacando.today/manifest.json';
if('getInstalledRelatedApps' in navigator) {
const relatedApps = await navigator.getInstalledRelatedApps();
// if there is only one app in "related_applications"
isInstalled = relatedApps.length > 0;
// if there are more apps in "related_applications"
// and you need to find a specific one
isInstalled = !!(relatedApps.find(({url}) => url === manifestUrl));
}
if(isInstalled) {
// hide the button, remove it, whatever to make it go
}
navigator.getInstalledRelatedApps()
is currently only supported in Chrome on Android.
Installing your PWA as an Edge sidebar app
Edge browser now supports sidebar apps. These are web apps that can be added to the Edge sidebar which is, well... a sidebar in the browser window where apps can be added to.
I was skeptical at first, but it's actually quite a handy feature because it enables you to have multiple web apps side by side in the browser window. For example, while browsing the web, you can have the Spotify web app on the side and listen to your favorite music.
Pretty cool!
All you need to do to make your web app suitable to be added to the sidebar is to make sure it supports a minimal width of 376 pixels.
You can then add a "edge_side_panel" key to manifest.json
and set the preferred width for your app:
"edge_side_panel": {
"preferred_width": 480
}
Whenever a user opens your app from the sidebar, the sidebar will be resized to this preferred width.
Here's What PWA Can Do Today running as a sidebar app:
How the Background Sync API really works
The Background Sync API enables web apps to defer tasks when the user is offline and run these when the network is restored. For example, a messaging app could save messages that are sent while the user is offline to IndexedDB and then send them when the user is online again. This is a very powerful API that contributes to a great offline experience and makes a web app feel more like a native app.
There are a lot of confusing articles on the Background Sync API that, at least for me, don't really explain well how to use it. The API seems a bit counter-intuitive at first, but once you understand how it works, it opens up a lot of interesting possibilities.
Let's have a look.
To make use of background sync you need to request a sync
request with the SyncManager
which is available through ServiceWorkerRegistration.sync
so first you need to register a Service Worker and then request a sync with a unique tag:
await navigator.serviceWorker.register('/service-worker.js');
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-messages');
Here we have requested a sync with the tag "sync-messages". Inside the Service Worker, we now register an event handler for the sync
event and check the tag to identify the correct sync
event:
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
A tag is always added to a sync request so you can have multiple sync
events that you can attach different functionality to.
Now I might not be that smart but at first, I understood that this sync
event would only fire when I went offline and then came back online again but to my surprise this sync
event would always fire immediately when I registered it. Then when I went offline and back online again it would never fire.
What was going on?
Well, actually this is a very clever approach that enables you to define functionality that will be deferred when the app is offline until it's back online again without you having to check the network status.
The browser will handle all that for you.
Let's say that you have a button that will send a notification but when the app is offline you want that notification to be saved and then delivered when the app is online again.
Without the Background Sync API, you would:
- check the network status
- if there is no network, save the notification somewhere
- wait for the network to be restored
- check for any notifications that were sent while offline
- send the notifications
With the Background Sync API, you simply register a sync request with the tag "send-notification" (or whatever makes sense to you) and add an event handler for the sync
event to your Service worker that sends the notification when the tag of the sync
event is "send-notification" (or whatever you chose).
When the app is online, the sync
event will immediately fire and the notification will be sent. But when the app is offline, this event won't be fired until the app is back online and then it will fire and send the notification.
// inside the service worker an event handler is registered
// for the 'sync' event that sends the notification
self.addEventListener('sync', e => {
e.waitUntil(
self.registration.showNotification(title, options)
.catch(err => console.log(err))
)
});
// in the browser:
const syncButton = document.querySelector('#sync-button');
const registration = await navigator.serviceWorker.getRegistration();
if('sync' in registration) {
syncButton.addEventListener('click', async () => {
await registration.sync.register('sync-demo');
});
}
The button doesn't actually send the notification but simply requests a sync. It's important to understand that when a sync is requested, it only fires a single sync
event, so the button will request a sync each time it's clicked.
It may be a bit counter-intuitive that the button doesn't send the notification and only registers a sync, but the beauty of this approach is that it decouples checking the network from the code to be run: the browser handles all this for you.
It fires the sync
event immediately when online (so the code in the event handler is run) and defers it when offline until the app is online again.
Another great thing is that the sync event will also be fired when the network is restored and the PWA is not running. Really cool!
I added a demo to What PWA Can Do Today.