{"id":15375941,"url":"https://github.com/thepassle/pwa-helpers","last_synced_at":"2025-04-14T13:12:33.867Z","repository":{"id":35148008,"uuid":"212896283","full_name":"thepassle/pwa-helpers","owner":"thepassle","description":"Utilities for common patterns that help you build your Progressive Web App","archived":false,"fork":false,"pushed_at":"2023-01-04T22:10:40.000Z","size":1227,"stargazers_count":49,"open_issues_count":20,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-28T02:21:42.195Z","etag":null,"topics":["pwa","pwa-helpers","recipes","service-workers","web-components"],"latest_commit_sha":null,"homepage":"https://jovial-davinci-20c5fc.netlify.com/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thepassle.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-10-04T20:20:33.000Z","updated_at":"2025-03-10T12:55:13.000Z","dependencies_parsed_at":"2023-01-15T14:48:09.686Z","dependency_job_id":null,"html_url":"https://github.com/thepassle/pwa-helpers","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thepassle%2Fpwa-helpers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thepassle%2Fpwa-helpers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thepassle%2Fpwa-helpers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thepassle%2Fpwa-helpers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thepassle","download_url":"https://codeload.github.com/thepassle/pwa-helpers/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248886325,"owners_count":21177644,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pwa","pwa-helpers","recipes","service-workers","web-components"],"created_at":"2024-10-01T14:05:19.883Z","updated_at":"2025-04-14T13:12:33.728Z","avatar_url":"https://github.com/thepassle.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pwa Helper Components\n\n\u003e This webcomponent follows [open-wc](https://www.open-wc.org/) recommendations.\n\nThese are some utilities for common patterns that help you build your [Progressive Web App](https://developers.google.com/web/progressive-web-apps) (PWA). Not to be confused with [`@polymer/pwa-helpers`](https://www.npmjs.com/package/pwa-helpers).\n\nIf you're new to building Progressive Web Apps, we recommend you read the [Offline Cookbook](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook) by [Jake Archibald](https://twitter.com/jaffathecake), or take the free Udacity course at [Offline Web Applications](https://www.udacity.com/course/offline-web-applications--ud899).\n\nOther useful resources for developing Progressive Web Apps are:\n- [The Mental Gymnastics of Service Worker](https://jovial-davinci-20c5fc.netlify.com/) - A set of visualisations that guide you through the concepts of service worker step by step.\n- [Workbox](https://developers.google.com/web/tools/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.\n- [serviceworke.rs](https://serviceworke.rs/) - Common Service Worker patterns\n- [Service Workies](https://serviceworkies.com/) - Learn Service Workers inside and out with this game of Service Worker mastery\n- [PWA Builder Feature Store](https://www.pwabuilder.com/features) - Common recipes for building PWA's\n\n## Installation\n\nInstallation:\n```bash\nnpm i --save pwa-helper-components\n```\n\nImporting like this will self register the web component:\n```js\nimport 'pwa-helper-components/pwa-install-button.js';\nimport 'pwa-helper-components/pwa-dark-mode.js';\nimport 'pwa-helper-components/pwa-update-available.js';\n```\n\nIf you want more control over the registration of the component, you can import the class and handle registration yourself:\n```js\nimport { PwaInstallButton, PwaUpdateAvailable, PwaDarkMode } from 'pwa-helper-components';\n\ncustomElements.define('my-install-button', PwaInstallButton);\ncustomElements.define('my-update-available', PwaUpdateAvailable);\ncustomElements.define('my-dark-mode', PwaDarkMode);\n```\n\nOr via [unpkg](https://unpkg.com):\n```js\nimport 'https://unpkg.com/pwa-helper-components/pwa-install-button.js';\nimport 'https://unpkg.com/pwa-helper-components/pwa-update-available.js';\nimport 'https://unpkg.com/pwa-helper-components/pwa-dark-mode.js';\n\n// or:\n\nimport { PwaInstallButton, PwaUpdateAvailable, PwaDarkMode } from 'https://unpkg.com/pwa-helper-components/index.js';\n```\n\n## `\u003cpwa-install-button\u003e`\n\n`\u003cpwa-install-button\u003e` is a zero dependency web component that lets users easily add a install button to their PWA.\n\nYou can find a live demo [here](https://unpkg.com/pwa-helper-components@0.2.10/demo/index.html). (Note: it may take a few seconds before the buttons become visible, because the `beforeinstallprompt` may not have fired yet)\n\n`\u003cpwa-install-button\u003e` will have a `hidden` attribute until the [`beforeinstallprompt`](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent) 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`](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent) again. See the FAQ for more information.\n\n### Usage\n\nYou can provide your own button as a child of the `\u003cpwa-install-button\u003e`, or use the default (white-label) fallback button.\n\n```html\n\u003c!-- Will use a slotted default fallback button --\u003e\n\u003cpwa-install-button\u003e\n\u003c/pwa-install-button\u003e\n```\n\n```html\n\u003c!-- Will use the provided button element --\u003e\n\u003cpwa-install-button\u003e\n    \u003cbutton\u003eInstall!\u003c/button\u003e\n\u003c/pwa-install-button\u003e\n```\n\nYou can also use a Web Component:\n```html\n\u003cpwa-install-button\u003e\n    \u003cmwc-button\u003eInstall!\u003c/mwc-button\u003e\n\u003c/pwa-install-button\u003e\n```\n\nInstead of only showing a button, you can also make a custom app listing experience, as long as it contains a button:\n\n```html\n\u003cpwa-install-button\u003e\n  \u003cimg src=\"./app-logo.png\"/\u003e\n  \u003ch1\u003eMyApp\u003c/h1\u003e\n  \u003ch2\u003eKey features:\u003c/h2\u003e\n  \u003cul\u003e\n    \u003cli\u003eFast\u003c/li\u003e\n    \u003cli\u003eReliable\u003c/li\u003e\n    \u003cli\u003eOffline first\u003c/li\u003e\n  \u003c/ul\u003e\n  \u003ch2\u003eDescription:\u003c/h2\u003e\n  \u003cp\u003eMyApp is an awesome Progressive Web App!\u003c/p\u003e\n  \u003cbutton\u003eInstall!\u003c/button\u003e\n\u003c/pwa-install-button\u003e\n```\n\nDo note that you may want to defer the `\u003cpwa-install-button\u003e` 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.\n\n### Events\n\n`\u003cpwa-install-button\u003e` 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.\n\nYou can listen to these events like this:\n\n```js\nconst pwaInstallButton = document.querySelector('pwa-install-button');\n\n// The app is installable\npwaInstallButton.addEventListener('pwa-installable', (event) =\u003e {\n  console.log(event.detail); // true\n});\n\n// User accepted the prompt\npwaInstallButton.addEventListener('pwa-installed', (event) =\u003e {\n  console.log(event.detail); // true\n  // You may want to use this event to send some data to your analytics\n});\n\n// If the user dismisses the prompt\npwaInstallButton.addEventListener('pwa-installed', (event) =\u003e {\n  console.log(event.detail); // false\n});\n```\n\n\n### Requirements\n\nMake sure your PWA meets the installable criteria, which you can find  [here](https://developers.google.com/web/fundamentals/app-install-banners/). You can find a tool to generate your `manifest.json` [here](https://www.pwabuilder.com/generate).\n\n## `\u003cpwa-update-available\u003e`\n\n\u003e 🚨 This web component may require a small addition to your service worker if you're not using workbox 🚨\n\n`\u003cpwa-update-available\u003e` is a zero dependency web component that lets users easily show a 'update available' notification.\n\n`\u003cpwa-update-available\u003e` will have a `hidden` attribute until the [updatefound](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration#Examples) notification is sent, and the new service worker is succesfully installed.\n\nClicking the `\u003cpwa-update-available\u003e` component will [post a message](https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage) 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`.\n\nInstructions on how to catch this message in your service worker are described down below.\n\n### Usage:\n\n```html\n\u003c!-- Will use the default slot fallback button --\u003e\n\u003cpwa-update-available\u003e\n\u003c/pwa-update-available\u003e\n```\n\n```html\n\u003c!-- Will use the provided button element --\u003e\n\u003cpwa-update-available\u003e\n  \u003cbutton\u003eA new update is available! Click here to update.\u003c/button\u003e\n\u003c/pwa-update-available\u003e\n```\n\nThe 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:\n\n#### Using Workbox\n\nIf you're using workbox, no changes are required, as workbox automatically includes the necessary code in your generated service worker.\n\n#### Manual approach\n\nIf 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.\n\n```js\nself.addEventListener('message', (event) =\u003e {\n  if (event.data \u0026\u0026 event.data.type === 'SKIP_WAITING') {\n    self.skipWaiting();\n  }\n});\n```\n\n`skipWaiting` will refresh any open tabs/clients.\n\nPrior art by:\n- [Deanhume](https://github.com/deanhume/pwa-update-available/blob/master/index.html)\n- [Morbidick](https://github.com/morbidick/serviceworker-helpers/blob/master/sw-update-toast.html)\n- [Workbox Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users)\n\n### Events\n\n`\u003cpwa-update-available\u003e` will fire a `pwa-update-available` event when a update is available.\n\nYou can listen to this event like this:\n\n```js\nconst pwaUpdateAvailable = document.querySelector('pwa-update-available');\n\npwaUpdateAvailable.addEventListener('pwa-update-available', (event) =\u003e {\n  console.log(event.detail); // true\n});\n```\n\nIf 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](https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68).\n\n## `addPwaUpdateListener`\n\nExecutes a callback whenever a new update is available. \n\nIf you're using the `\u003cpwa-update-available\u003e` 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.\n\n### Usage\n\nHere's an example:\n\n```js\naddPwaUpdateListener((updateAvailable) =\u003e {\n  /* Using a web component: */\n  this.updateAvailable = updateAvailable;\n\n  /* Using (P)react: */\n  this.setState({\n    updateAvailable\n  })\n});\n```\n\n## `\u003cpwa-dark-mode\u003e`\n\n`\u003cpwa-update-available\u003e` 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`.\n\nWhen 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 `\u003cpwa-dark-mode\u003e` anywhere in your app to toggle the darkmode state.\n\n### Usage\n\nYou can provide your own button as a child of the `\u003cpwa-dark-mode\u003e`, or use the default (white-label) fallback button.\n\n```html\n\u003c!-- Will use a slotted default fallback button --\u003e\n\u003cpwa-dark-mode\u003e\n\u003c/pwa-dark-mode\u003e\n```\n\n```html\n\u003c!-- Will use the provided button element --\u003e\n\u003cpwa-dark-mode\u003e\n  \u003cbutton\u003eToggle dark mode!\u003c/button\u003e\n\u003c/pwa-dark-mode\u003e\n```\n\n## `installDarkModeHandler`\n\nInstalls a `mediaQueryWatcher` that listens for `(prefers-color-scheme: dark)`, and toggles a dark mode class if appropriate. This means that on initial pageload:\n\n- If the user _hasn't_ manually set a dark mode preference yet, it will respect the systems preference\n- If the user _has_ set a preference, it will always respect that preference, because the user has manually opted into it.\n\nDark mode preference is persisted in localstorage. Use with `\u003cpwa-dark-mode\u003e` to easily add a button that lets the user toggle between light and dark mode.\n\n### Usage\n\nSimply import the handler, and call it on pageload (preferably early). The handler should only be installed **once**. \n\n#### Basic\n\n```js\nimport { installDarkModeHandler } from 'pwa-helper-components';\n\n// Basic usage:\ninstallDarkModeHandler();\n```\n\nNow all you have to do is write some css:\n\n```css\n:root {\n  --my-text-col: black;\n  --my-bg-col: white;\n}\n\n.dark {\n  --my-text-col: white;\n  --my-bg-col: black;\n}\n\nbody {\n  background-color: var(--my-bg-col);\n  color: var(--my-text-col);\n}\n```\n\n#### Advanced\n\nYou can also add a callback to execute whenever darkmode is changed to do extra work, like changing favicons.\n\n`./utils/setFavicons.js`:\n```js\nexport function setFavicons(darkMode) {\n  const [iconBig, iconSmall] = [...document.querySelectorAll(\"link[rel='icon']\")];\n  const manifest = document.querySelector(\"link[rel='manifest']\");\n  const theme_color = document.querySelector(\"meta[name='theme-color']\");\n\n  if (darkMode) {\n    manifest.href = '/manifest-dark.json';\n    iconBig.href = 'src/assets/favicon-32x32-dark.png';\n    iconSmall.href = 'src/assets/favicon-16x16-dark.png';\n    theme_color.setAttribute('content', '#303136');\n  } else {\n    manifest.href = '/manifest.json';\n    iconBig.href = 'src/assets/favicon-32x32.png';\n    iconSmall.href = 'src/assets/favicon-16x16.png';\n    theme_color.setAttribute('content', '#ffffff');\n  }\n\n  document.getElementsByTagName('head')[0].appendChild(manifest);\n  document.getElementsByTagName('head')[0].appendChild(iconBig);\n  document.getElementsByTagName('head')[0].appendChild(iconSmall);\n  document.getElementsByTagName('head')[0].appendChild(theme_color);\n}\n```\n\n`./main.js`:\n```js\nimport { setFavicons } from './utils/setFavicons.js';\n\ninstallDarkModeHandler((darkMode) =\u003e {\n  setFavicons(darkMode)\n});\n```\n\nNote 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:\n\n```js\nimport { PwaDarkMode } from 'pwa-helper-components/pwa-dark-mode/PwaDarkMode.js';\nimport { setFavicons } from './utils/setFavicons.js';\n\nclass MyDarkMode extends PwaDarkMode {\n  callback(darkMode) {\n    setFavIcons(darkMode);\n  }\n}\n\ncustomElements.define('my-dark-mode', MyDarkMode);\n```\n\n### Summarize\n\nThe `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.\n\n## FAQ\n\n### Why is my install button not showing up on subsequent visits?\n\nThe [BeforeInstallPromptEvent](https://developer.mozilla.org/en-US/docs/Web/API/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. `\u003cpwa-install-button\u003e` 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.\n\nDifferent browsers may use a different heuristic to fire subsequent BeforeInstallPromptEvents.\n\n### `skipWaiting` doesn't work!\n\nYour 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`](https://open-wc.org/developing/es-dev-server.html#getting-started) in order to reload the page on file changes.\n\nIf you want to test your service worker with your production build, you can remove the `--watch` flag from your [`es-dev-server`](https://open-wc.org/developing/es-dev-server.html#getting-started) script, or you can run a simple http-server with `npx http-server` on your `/dist` folder to make sure everything works as expected.\n\n\n### Single Page Apps\n\nWhen developing single page applications, make sure to have a `\u003cbase href=\"/\"\u003e` element in your index.html, and return your index.html in your service worker. \n\n#### Using Workbox\n\nIf you're using Workbox, you can register a navigation route like so:\n\n```js\nworkbox.routing.registerNavigationRoute(\n  // Assuming '/single-page-app.html' has been precached,\n  // look up its corresponding cache key.\n  workbox.precaching.getCacheKeyForURL('/single-page-app.html')\n);\n```\n\nYou can read more about this approach [here](https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route).\n\n\n#### Manual approach\n\nIf you're not using Workbox, you can use the following code snippet in your service worker's `fetch` handler:\n\n```js\nself.addEventListener('fetch', (event) =\u003e {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(caches.match('/'));\n    return;\n  }\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthepassle%2Fpwa-helpers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthepassle%2Fpwa-helpers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthepassle%2Fpwa-helpers/lists"}