Giter Club home page Giter Club logo

pwa-helpers's Introduction

Pwa Helper Components

This webcomponent follows open-wc recommendations.

These are some utilities for common patterns that help you build your Progressive Web App (PWA). Not to be confused with @polymer/pwa-helpers.

If you're new to building Progressive Web Apps, we recommend you read the Offline Cookbook by Jake Archibald, or take the free Udacity course at Offline Web Applications.

Other useful resources for developing Progressive Web Apps are:

  • The Mental Gymnastics of Service Worker - A set of visualisations that guide you through the concepts of service worker step by step.
  • Workbox - Workbox is a set of libraries and Node modules that make it easy to cache assets and take full advantage of features used to build Progressive Web Apps.
  • serviceworke.rs - Common Service Worker patterns
  • Service Workies - Learn Service Workers inside and out with this game of Service Worker mastery
  • PWA Builder Feature Store - Common recipes for building PWA's

Installation

Installation:

npm i --save pwa-helper-components

Importing like this will self register the web component:

import 'pwa-helper-components/pwa-install-button.js';
import 'pwa-helper-components/pwa-dark-mode.js';
import 'pwa-helper-components/pwa-update-available.js';

If you want more control over the registration of the component, you can import the class and handle registration yourself:

import { PwaInstallButton, PwaUpdateAvailable, PwaDarkMode } from 'pwa-helper-components';

customElements.define('my-install-button', PwaInstallButton);
customElements.define('my-update-available', PwaUpdateAvailable);
customElements.define('my-dark-mode', PwaDarkMode);

Or via unpkg:

import 'https://unpkg.com/pwa-helper-components/pwa-install-button.js';
import 'https://unpkg.com/pwa-helper-components/pwa-update-available.js';
import 'https://unpkg.com/pwa-helper-components/pwa-dark-mode.js';

// or:

import { PwaInstallButton, PwaUpdateAvailable, PwaDarkMode } from 'https://unpkg.com/pwa-helper-components/index.js';

<pwa-install-button>

<pwa-install-button> is a zero dependency web component that lets users easily add a install button to their PWA.

You can find a live demo here. (Note: it may take a few seconds before the buttons become visible, because the beforeinstallprompt may not have fired yet)

<pwa-install-button> will have a hidden attribute until the beforeinstallprompt event is fired. It will hold on to the event, so the user can click the button whenever they are ready to install your app. It will also hold on to the event even if the user has declined the initial prompt. If they decline to install your app, and leave your page it may take some time before the browser sends another beforeinstallprompt again. See the FAQ for more information.

Usage

You can provide your own button as a child of the <pwa-install-button>, or use the default (white-label) fallback button.

<!-- Will use a slotted default fallback button -->
<pwa-install-button>
</pwa-install-button>
<!-- Will use the provided button element -->
<pwa-install-button>
    <button>Install!</button>
</pwa-install-button>

You can also use a Web Component:

<pwa-install-button>
    <mwc-button>Install!</mwc-button>
</pwa-install-button>

Instead of only showing a button, you can also make a custom app listing experience, as long as it contains a button:

<pwa-install-button>
  <img src="./app-logo.png"/>
  <h1>MyApp</h1>
  <h2>Key features:</h2>
  <ul>
    <li>Fast</li>
    <li>Reliable</li>
    <li>Offline first</li>
  </ul>
  <h2>Description:</h2>
  <p>MyApp is an awesome Progressive Web App!</p>
  <button>Install!</button>
</pwa-install-button>

Do note that you may want to defer the <pwa-install-button> becoming visible if you choose a pattern like this, as it may be obstructive to your user. You can do this by overriding the default [hidden] styling, and listening for the pwa-installable event.

Events

<pwa-install-button> will fire a pwa-intallable event when it becomes installable, and a pwa-installed event when the user has installed your PWA. If the user has dismissed the prompt, a pwa-installed event will be fired with a false value.

You can listen to these events like this:

const pwaInstallButton = document.querySelector('pwa-install-button');

// The app is installable
pwaInstallButton.addEventListener('pwa-installable', (event) => {
  console.log(event.detail); // true
});

// User accepted the prompt
pwaInstallButton.addEventListener('pwa-installed', (event) => {
  console.log(event.detail); // true
  // You may want to use this event to send some data to your analytics
});

// If the user dismisses the prompt
pwaInstallButton.addEventListener('pwa-installed', (event) => {
  console.log(event.detail); // false
});

Requirements

Make sure your PWA meets the installable criteria, which you can find here. You can find a tool to generate your manifest.json here.

<pwa-update-available>

๐Ÿšจ This web component may require a small addition to your service worker if you're not using workbox ๐Ÿšจ

<pwa-update-available> is a zero dependency web component that lets users easily show a 'update available' notification.

<pwa-update-available> will have a hidden attribute until the updatefound notification is sent, and the new service worker is succesfully installed.

Clicking the <pwa-update-available> component will post a message to your service worker with a {type: 'SKIP_WAITING'} object, which lets your new service worker call skipWaiting and then reload the page on controllerchange.

Instructions on how to catch this message in your service worker are described down below.

Usage:

<!-- Will use the default slot fallback button -->
<pwa-update-available>
</pwa-update-available>
<!-- Will use the provided button element -->
<pwa-update-available>
  <button>A new update is available! Click here to update.</button>
</pwa-update-available>

The next thing to do is update your service worker to listen for the message event. To add this snippet of code to your service worker, you can do the following:

Using Workbox

If you're using workbox, no changes are required, as workbox automatically includes the necessary code in your generated service worker.

Manual approach

If you're manually writing your service worker, you can simply copy the code snippet down below anywhere in the global scope of your service worker.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

skipWaiting will refresh any open tabs/clients.

Prior art by:

Events

<pwa-update-available> will fire a pwa-update-available event when a update is available.

You can listen to this event like this:

const pwaUpdateAvailable = document.querySelector('pwa-update-available');

pwaUpdateAvailable.addEventListener('pwa-update-available', (event) => {
  console.log(event.detail); // true
});

If you're interested in reading more about this subject, you can check out this blog: How to Fix the Refresh Button When Using Service Workers.

addPwaUpdateListener

Executes a callback whenever a new update is available.

If you're using the <pwa-update-available> component, it can happen that you're dynamically rendering the component, and have no way to listen to the pwa-update-available event, because your component is not actually in the DOM yet. But sometimes you may want to show a subtle indicator that an update is available, and need some way to find out that an update actually is available.

Usage

Here's an example:

addPwaUpdateListener((updateAvailable) => {
  /* Using a web component: */
  this.updateAvailable = updateAvailable;

  /* Using (P)react: */
  this.setState({
    updateAvailable
  })
});

<pwa-dark-mode>

<pwa-update-available> is a zero dependency web component that lets users toggle a 'dark' class on the html element, and effectively toggle darkmode on and off. It will also persist the preference in local storage. This web component should be used in combination with installDarkModeHandler.

When used in combination with installDarkModeHandler, you can very easily implement darkmode in your PWA. Just call the installDarkModeHandler whenever the page loads to respect either the systems darkmode preference, or if a visitor has already manually set a preference; use that instead, and use the <pwa-dark-mode> anywhere in your app to toggle the darkmode state.

Usage

You can provide your own button as a child of the <pwa-dark-mode>, or use the default (white-label) fallback button.

<!-- Will use a slotted default fallback button -->
<pwa-dark-mode>
</pwa-dark-mode>
<!-- Will use the provided button element -->
<pwa-dark-mode>
  <button>Toggle dark mode!</button>
</pwa-dark-mode>

installDarkModeHandler

Installs a mediaQueryWatcher that listens for (prefers-color-scheme: dark), and toggles a dark mode class if appropriate. This means that on initial pageload:

  • If the user hasn't manually set a dark mode preference yet, it will respect the systems preference
  • If the user has set a preference, it will always respect that preference, because the user has manually opted into it.

Dark mode preference is persisted in localstorage. Use with <pwa-dark-mode> to easily add a button that lets the user toggle between light and dark mode.

Usage

Simply import the handler, and call it on pageload (preferably early). The handler should only be installed once.

Basic

import { installDarkModeHandler } from 'pwa-helper-components';

// Basic usage:
installDarkModeHandler();

Now all you have to do is write some css:

:root {
  --my-text-col: black;
  --my-bg-col: white;
}

.dark {
  --my-text-col: white;
  --my-bg-col: black;
}

body {
  background-color: var(--my-bg-col);
  color: var(--my-text-col);
}

Advanced

You can also add a callback to execute whenever darkmode is changed to do extra work, like changing favicons.

./utils/setFavicons.js:

export function setFavicons(darkMode) {
  const [iconBig, iconSmall] = [...document.querySelectorAll("link[rel='icon']")];
  const manifest = document.querySelector("link[rel='manifest']");
  const theme_color = document.querySelector("meta[name='theme-color']");

  if (darkMode) {
    manifest.href = '/manifest-dark.json';
    iconBig.href = 'src/assets/favicon-32x32-dark.png';
    iconSmall.href = 'src/assets/favicon-16x16-dark.png';
    theme_color.setAttribute('content', '#303136');
  } else {
    manifest.href = '/manifest.json';
    iconBig.href = 'src/assets/favicon-32x32.png';
    iconSmall.href = 'src/assets/favicon-16x16.png';
    theme_color.setAttribute('content', '#ffffff');
  }

  document.getElementsByTagName('head')[0].appendChild(manifest);
  document.getElementsByTagName('head')[0].appendChild(iconBig);
  document.getElementsByTagName('head')[0].appendChild(iconSmall);
  document.getElementsByTagName('head')[0].appendChild(theme_color);
}

./main.js:

import { setFavicons } from './utils/setFavicons.js';

installDarkModeHandler((darkMode) => {
  setFavicons(darkMode)
});

Note that if you do this, you should also extend the PwaDarkMode web component and add a callback method, to make sure these changes also get applied if a user manually changes the preference, rather than only changing the systems preference:

import { PwaDarkMode } from 'pwa-helper-components/pwa-dark-mode/PwaDarkMode.js';
import { setFavicons } from './utils/setFavicons.js';

class MyDarkMode extends PwaDarkMode {
  callback(darkMode) {
    setFavIcons(darkMode);
  }
}

customElements.define('my-dark-mode', MyDarkMode);

Summarize

The installDarkModeHandler will fire on pageload: if a user has not manually set a preference, it will fire when the user changes their system preference, if a user has manually set a preference, it will always respect that preference.

FAQ

Why is my install button not showing up on subsequent visits?

The BeforeInstallPromptEvent may not immediately be fired if the user has initially declined the prompt. This is intentional behavior left that way to avoid web pages annoying the users to repeatedly prompt the user for adding to home screen. <pwa-install-button> will hold on to the event, even if the user declined to install the app; if they change their mind, they will still be able to click the button. The button may not be immediately visible on subsequent visits though; this is intended browser behavior.

Different browsers may use a different heuristic to fire subsequent BeforeInstallPromptEvents.

skipWaiting doesn't work!

Your service worker may not call skipWaiting if there are tasks that are still running, like for example Event Sources, which are used by, for example, the --watch mode of es-dev-server in order to reload the page on file changes.

If you want to test your service worker with your production build, you can remove the --watch flag from your es-dev-server script, or you can run a simple http-server with npx http-server on your /dist folder to make sure everything works as expected.

Single Page Apps

When developing single page applications, make sure to have a <base href="/"> element in your index.html, and return your index.html in your service worker.

Using Workbox

If you're using Workbox, you can register a navigation route like so:

workbox.routing.registerNavigationRoute(
  // Assuming '/single-page-app.html' has been precached,
  // look up its corresponding cache key.
  workbox.precaching.getCacheKeyForURL('/single-page-app.html')
);

You can read more about this approach here.

Manual approach

If you're not using Workbox, you can use the following code snippet in your service worker's fetch handler:

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(caches.match('/'));
    return;
  }
});

pwa-helpers's People

Contributors

dependabot[bot] avatar lpellegr avatar thepassle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

pwa-helpers's Issues

PwaUpdateAvailable does not work with open-wc service worker

I gave a quick try to your PwaUpdateAvailable component in combination with an open-wc managed app that uses the new service worker implementation you made.

The service worker that is generated is working great and your button is displayed on an update. However, when I click on the update button nothing is happening (i.e. the new service worker is not activated and as a consequence, the page is not reloaded).

After investigating, I noticed the service worker defines a message listener as follows:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

However, your component implementation post the following message:

this._newWorker.postMessage('skipWaiting');

I made a quick test by changing your compoment message payload to:

this._newWorker.postMessage({type: 'SKIP_WAITING'});

This fixed the issue.

PwaUpdateAvailable button not displayed on page reload

I really like the PwaUpdateAvailable component idea. Unfortunately, the message is displayed only once. Let me explain:

When you deploy a new app version, the browser detects a new service worker and PwaUpdateAvailable reacts by displaying a button to suggest updating the page. At this stage, suppose the user closes the browser tab, the browser, or it's device turns off for whatever reason. Upon page reload, the new service worker remains in waiting to activate state until skipWaiting is called. Unfortunately, the PwaUpdateAvailable component does not redisplay the update button whereas I think it should.

No page reload after skipWaiting

Thanks for this great plug-in
I have a open-wc application. I have added your plug-in to handle sw updates.

Here is how it looks like
import { addPwaUpdateListener } from 'pwa-helper-components';

constructor() {
    super();
    this.updateAvailable = false;
  }

connectedCallback() {
    super.connectedCallback();
    addPwaUpdateListener(updateAvailable => {
      this.updateAvailable = updateAvailable;
    });
  }

When truthy, this.updateAvailable shows a panel that has a 'install update' button. Button click method is as follows:

<button
@click=${() => this.skipWaiting()}>
Install update
</button>


async skipWaiting() {
    const reg = await navigator.serviceWorker.getRegistration();
    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
  }

When button is clicked, the method is called and reg is loaded (checked in console) no error is thrown but nothing happens and the page never reloads. Any idea ?

here is my rollup.config.js

import merge from 'deepmerge';
import { createSpaConfig } from '@open-wc/building-rollup';
import copy from 'rollup-plugin-copy';
import replace from '@rollup/plugin-replace';
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';

const baseConfig = createSpaConfig({
  developmentMode: process.env.ROLLUP_WATCH === 'true',

  workbox: {
    skipWaiting: false,
    clientsClaim: false,
  },
  injectServiceWorker: true,
});

export default merge(baseConfig, {
  input: './index.html',

  plugins: [
    replace({
      ___appNamePrefix___: 'autoScrib_',
      ___appName___: 'autoScrib',
      __dev__: 'prod',
      __color1__: '#314d8a',
      delimiters: ['', ''],
    }),
    dynamicImportVars({
      exclude: 'views/template-page/template-page.js',
    }),
    copy({
      targets: [
        {
          src: 'assets/**/*.jpg',
          dest: 'dist/assets',
        },
        {
          src: 'assets/**/*.png',
          dest: 'dist/assets',
        },
        {
          src: 'manifests',
          dest: 'dist',
        },
      ],
      hook: 'buildStart',
      flatten: false,
    }),
  ],
});

Add types support

To avoid TS errors:

Screenshot 2020-04-26 at 13 48 09

Could not find a declaration file for module 'pwa-helper-components/pwa-install-button.js'. '/Users/abdonrd/.../pwa-starter/node_modules/pwa-helper-components/pwa-install-button.js' implicitly has an 'any' type.
  Try `npm install @types/pwa-helper-components` if it exists or add a new declaration (.d.ts) file containing `declare module 'pwa-helper-components/pwa-install-button.js';`ts(7016)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.