{"id":18790598,"url":"https://github.com/mvneerven/pure-pwa","last_synced_at":"2025-04-07T12:01:38.340Z","repository":{"id":219929843,"uuid":"750029273","full_name":"mvneerven/pure-pwa","owner":"mvneerven","description":"Ultra-lean, Web Components enhanced, no-build, no-dependencies boilerplate for PWAs","archived":false,"fork":false,"pushed_at":"2024-03-07T07:37:34.000Z","size":1079,"stargazers_count":288,"open_issues_count":6,"forks_count":18,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-31T11:02:42.660Z","etag":null,"topics":["html-css-javascript","no-build","no-dependencies","no-framework","progressive-web-app","pwa","webdevelopment"],"latest_commit_sha":null,"homepage":"https://purepwa.z6.web.core.windows.net/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mvneerven.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-29T21:14:27.000Z","updated_at":"2025-03-26T12:55:36.000Z","dependencies_parsed_at":"2024-02-03T10:26:43.537Z","dependency_job_id":"2522cdb3-b96e-48c5-ab0c-f581485cec1d","html_url":"https://github.com/mvneerven/pure-pwa","commit_stats":null,"previous_names":["mvneerven/pure-pwa"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvneerven%2Fpure-pwa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvneerven%2Fpure-pwa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvneerven%2Fpure-pwa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvneerven%2Fpure-pwa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mvneerven","download_url":"https://codeload.github.com/mvneerven/pure-pwa/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247648874,"owners_count":20972944,"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":["html-css-javascript","no-build","no-dependencies","no-framework","progressive-web-app","pwa","webdevelopment"],"created_at":"2024-11-07T21:12:56.478Z","updated_at":"2025-04-07T12:01:38.309Z","avatar_url":"https://github.com/mvneerven.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# PurePWA - Power \u0026 Purity\r\n\r\n\u003e PurePWA is a **Radical Experiment** in Simplifying Boilerplate for PWAs, in which we force ourselves to only use the **Modern Web** standards, and nothing else.\r\n\r\n![mobile-view.webp](./md/mobile-view.webp)\r\n\r\n# Table of Contents\r\n\r\n- [Setup](#Setup)\r\n- [Project Structure](#Project_Structure)\r\n- [Usage](#usage)\r\n- [New in](#New_In)\r\n- [More Info](#more_info)\r\n\r\n# Setup\r\n\r\n\u003e There is no setup.\r\n\r\n💡 This is a vanilla, accessible, SEO-friendly, ultra-lean, no-build, no dependencies `PWA` project, using only what's included in the Web Standards and has been implemented by Modern Browsers.\r\n\r\nThe two most important technologies used are:\r\n\r\n- Web Components\r\n- ES Modules (loaded and linked at runtime)\r\n\r\n## Tooling\r\n\r\nI have developed the code using these tools:\r\n\r\n- Microsoft VSCode\r\n  - VSCode extensions:\r\n    - ritwickdey.LiveServer (Easy to use built-in web server)\r\n    - glenn2223.live-sass (Live SCSS compilation)\r\n    - PWABuilder.pwa-studio (optional - Loads of PWA goodies)\r\n\r\nUsing the tools mentioned above, the PWA is directly served using `LiveServer`, and CSS is compiled from `.scss` files by `live-sass`.\r\n\r\n**Nothing else is needed.**\r\n\r\n# Project Structure\r\n\r\n![Folder Structure](./md/project-structure.webp)\r\n\r\n## Web Root: `public/`\r\n\r\nThis is a `hybrid MPA`, which means that we have multiple app pages, each one in its own folder.\r\n\r\nThis provides a number of undeniable benefits:\r\n\r\n- No need for SPA routing 🤯\r\n- Code readability and maintenance\r\n- Smaller, optimized downloads,\r\n\r\nAlthough base routing is handled by the web server, each route can still setup sub-routes and handle them, through a web standards-based routing mechanism ([Navigator.navigate](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate)).\r\n\r\nThe `public/` folder is the one getting served as root (see `.vscode/settings.json`).\r\n\r\nWithin `public/`, there's one system folder (`assets/`), and a folder for each `MPA` page.\r\n\r\n![App folder example](./md/public-folder.webp)\r\n\r\n### Assets\r\n\r\nThe `assets/` folder holds shared images, css (compiled from `/scss/app.scss` and its `@import`-ed `.scss` files).\r\n\r\n![App folder example](./md/assets.webp)\r\n\r\nThe `assets/js/` folder holds:\r\n\r\n#### 1. `services`\r\n\r\nService Worker and related\r\n\r\n#### 2. `shared`\r\n\r\nWeb Components (each in a seperate folder)\r\n\r\n#### 3. `app.js`\r\n\r\nMain entry point\r\n\r\n```html\r\n\u003cscript defer type=\"module\" src=\"assets/js/app.js\"\u003e\u003c/script\u003e\r\n```\r\n\r\n#### 4. `common.js`\r\n\r\nShared, exported symbols used elsewhere.\r\n\r\n```js\r\nimport { CustomElement } from \"../assets/js/common.js\";\r\n```\r\n\r\n### MPA Pages\r\n\r\nEach MPA Page folder can consist of `HTML`, `SCSS` and `JS` files (and other files that are related to the page).\r\n\r\n![App folder example](./md/app-folder.webp)\r\n\r\nThe `SCSS` files are imported into the `index.scss` file in the public root, and are compiled automatically by the Live Sass Compiler, into `public/assets/css/app.css.\r\n\r\n\u003e The '\\_' in `_index.scss` means the stylesheet is going to be imported (@import) to a main stylesheet i.e. styles.scss.\r\n\r\nThis is a choice I made, to have just one CSS file served. Suit yourself if you would like to implement a different approach.\r\n\r\nIf there is a `.js` file in an app folder (such as in the example), the `index.html` file will include it, like this:\r\n\r\n```html\r\n\u003cscript defer type=\"module\" src=\"todo-app.js\"\u003e\u003c/script\u003e\r\n```\r\n\r\nThe main advantage of this approach is that `MPA` pages are isolated modules, with page-specific behavior just declared for that page.\r\n\r\nThis means improvement **maintenance**, **readability**, **separation of concerns**, and **deployability**.\r\n\r\n## Shared Components\r\n\r\nHaving separate app pages, in an `MPA` app, doesn't mean that there's no shared code.\r\n\r\n### ECMAScript Imports\r\n\r\nWe rely on the standards-based, `ECMAScript` runtime import mechanism, that is similar to the way imports are done in bundling/building environments (webpack, vite, esbuild, etc.), but uses the `.js` extension:\r\n\r\n```js\r\nimport { CustomElement } from \"../../common.js\";\r\n```\r\n\r\n## Web Components\r\n\r\nPurePWA uses two kinds of Web Componenents:\r\n\r\n### 1. Self-contained controls (with Shadow DOM)\r\n\r\nThese components insert their own content into the `Shadow DOM` below their container element. Styling is then isolated and should be done using [adopted stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets).\r\n\r\n### 2. Progressive Enhancement wrappers (with Light DOM)\r\n\r\nThese components enhance existing, contained HTML structures, using the `Light DOM`, the content nested within the Web Component's tags. Styling doesn't differ from standard styling in the rest of the document.\r\n\r\n### The CustomElement class\r\n\r\nThis is a generic Web Component class that offers a few benefits over the `HTMLElement` base class:\r\n\r\n#### 1. `state` property (managed by a `Proxy` object)\r\n\r\n- [More on the `Proxy` class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)\r\n- [Make your own state management with Proxies and Event emitters](https://dev.to/fly/make-your-own-state-management-for-react-with-proxies-1n0m)\r\n\r\n#### 2. `render()` method that mimics framework best practices, but uses `ES String Literals`\r\n\r\n```js\r\nrender() {\r\n  const builder = new HTMLBuilder(/*html*/ `\u003col\u003e{html}\u003c/ol\u003e`);\r\n\r\n  for (let item of this.listViewItems) {\r\n    builder.add(this.getItem(item));\r\n  }\r\n  return builder.toHTML();\r\n}\r\n```\r\n\r\n### The RouterElement class\r\n\r\nInherits from `CustomElement`. Provides simple 'SPA'-like routing, but within the `MPA` page.\r\n\r\nInstead of the `render()` method of the `CustomElement` class, you use the `routes` property to implement your rendering logic:\r\n\r\nExample:\r\n\r\n```js\r\n  get routes() {\r\n    return {\r\n      \"/movies/\": this.getPopularMovies.bind(this),\r\n      \"/movie/\": this.getMovie.bind(this)\r\n    };\r\n  }\r\n\r\n  async getPopularMovies() {\r\n    hookEscapeKey(() =\u003e history.back());\r\n\r\n    const movies = await this.tmdb.getPopularMovies();\r\n\r\n    return /*html*/ `\u003csection class=\"cards\" \u003e${this.getCards(\r\n      movies.results\r\n    )}\u003c/section\u003e`;\r\n  }\r\n```\r\n\r\n# Native Look \u0026 Feel\r\n\r\nOne of the goals of the `PurePWA` experiment is getting a **near-native** experience. But to get that, we first need to get a grasp of what that actually means.\r\n\r\n- Glossy appearance (scalable vector graphics, gradients, shadows)\r\n- Download/launch/interaction speed\r\n- Optimal device real estate usage\r\n- Responsive, mobile-first design\r\n- Offline capability\r\n- No HTML reflows during app usage\r\n- Micro-animations: subtle effects to give immediate feedback, more user satisfaction and engagemen, create fluid state transitions, and even prevent errors (because of immediate feedback).\r\n- No browser chrome (PWAs run in the browser, but you want to hide all browser UI)\r\n\r\nThis project tries to address all of these areas.\r\n\r\n## SVG Sprites\r\n\r\nFor PurePWA, I've chosen the very much undervalued `SVG Sprites` technique for icons and other graphics.\r\n\r\nCheck out this [free store for more icons](https://materialdesignicons.com/).\r\n\r\n```xml\r\n\u003csvg xmlns=\"http://www.w3.org/2000/svg\"\u003e\r\n  \u003cdefs\u003e\r\n    \u003csymbol id=\"menu\"\r\n      xmlns=\"http://www.w3.org/2000/svg\"\u003e\r\n      \u003cpath d=\"M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z\" /\u003e\r\n    \u003c/symbol\u003e\r\n\r\n    \u003csymbol id=\"blog\"\r\n      xmlns=\"http://www.w3.org/2000/svg\"\u003e\r\n      \u003cpath d=\"M4,5H20V7H4V5M4,9H20V11H4V9M4,13H20V15H4V13M4,17H14V19H4V17Z\" /\u003e\r\n    \u003c/symbol\u003e\r\n  \u003c/defs\u003e\r\n\r\n  \u003c!-- more --\u003e\r\n\u003c/svg\u003e\r\n```\r\n\r\nThe `svg-icon` Web Component is used to include graphics within the HTML structure.\r\n\r\n```html\r\n\u003ca href=\"/\" aria-label=\"Home\"\u003e\r\n  \u003csvg-icon icon=\"home\" color\u003e\u003c/svg-icon\u003e\r\n  \u003cspan\u003eHome\u003c/span\u003e\r\n\u003c/a\u003e\r\n```\r\n\r\nThe `svg-icon` component sits in the `/public/assets/js/shared` folder.\r\n\r\n```js\r\nimport { CustomElement } from \"../../common.js\";\r\n\r\ncustomElements.define(\r\n  \"svg-icon\",\r\n  /**\r\n   * Renders SVG icon using SVG sprites defined in /assets/img/icons.svg\r\n   */\r\n  class SVGIcon extends CustomElement {\r\n    static get observedAttributes() {\r\n      return [\"icon\"];\r\n    }\r\n\r\n    attributeChangedCallback(name, oldValue, newValue) {\r\n      if (name === \"icon\" \u0026\u0026 newValue) {\r\n        this.querySelector(\"use\")?.setAttribute(\r\n          \"href\",\r\n          \"/assets/img/icons.svg#\" + newValue\r\n        );\r\n      }\r\n    }\r\n\r\n    render() {\r\n      return /*html*/ `\u003csvg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\"\u003e\r\n        \u003cuse href=\"/assets/img/icons.svg#${this.getAttribute(\r\n          \"icon\"\r\n        )}\" /\u003e\r\n      \u003c/svg\u003e`;\r\n    }\r\n  }\r\n);\r\n```\r\n\r\n# Showcased Technologies\r\n\r\n## General\r\n\r\n- Auto adapting app menu (hamburger, bottom menu on mobile)\r\n- CSS grid auto flow\r\n- SVG sprite icons\r\n- Micro animations (and consideration of user preferences)\r\n- Light \u0026 Dark modes (defaults to system settings)\r\n- Automatic caching of MPA pages (to minimize delays/page loads)\r\n- Skeleton Empty States\r\n\r\n## MPA Demo Pages\r\n\r\nOn each of the demo pages, there's a lot of stuff going on, but all of it is done with pure HTML, CSS and JS, and without any dependencies.\r\n\r\nHere are some highlights:\r\n\r\n### Power\r\n\r\n- Peer-to-peer video calls using WebRTC API\r\n- Web Share API\r\n\r\n### Purity\r\n\r\n- Accordion using details/summary semantic HTML tags\r\n- Progressive enhancement (accordion-details Web Component)\r\n\r\n### Action\r\n\r\n- Use of `fetch()` API\r\n- In-page router using `Navigator.navigate` interception\r\n- View Transition API: morphing between master \u0026 detail\r\n- Custom Skeleton Empty State\r\n\r\n### Flow\r\n\r\n- Semantically correct, accessible web form\r\n- Enclosing label inputs to avoid unique ids and 'for' attributes\r\n- x-form progressive enhancement Web Component\r\n\r\n### Entry\r\n\r\n- Great accessibility\r\n- Semantic HTML (fieldset, legend, label, aria-attributes)\r\n- Progressive enhancement dropdown-list (using input + dynamic datalist)\r\n- CSS-based required field indication\r\n- Powerful HTML5 Form Validation API\r\n\r\n### Beauty\r\n\r\n- Scrollbar Snap Behavior (scroll-snap-\\*)\r\n- View Transitions\r\n- SPA sub-routes\r\n\r\n# Development Experience\r\n\r\nDid you notice?\r\n\r\n- No `node_modules/`\r\n- No `package.json`\r\n- No build time\r\n\r\n# Localization\r\n\r\nPurePWA supports localization (introduced in version `0.1.0`).\r\n\r\nThe `/public/assets/js/app-settings.js` file's `APP_SETTINGS.localization` hosts localized strings\r\n\r\n```js\r\nlocalization: {\r\n    defaultLanguage: \"en\",\r\n    strings: {\r\n      \"Power \u0026 Purity\": {\r\n        nl: \"Kracht \u0026 Puurheid\"\r\n      },\r\n      Power: {\r\n        nl: \"Kracht\"\r\n      },\r\n      Purity: {\r\n        nl: \"Puurheid\"\r\n      },\r\n      [...]\r\n```\r\n\r\nTo accomodate for larger, localized sections (which are handy for localized HTML blocks), there's a `\u003clocalize-section\u003e\u003c/localize-section\u003e` Web Component :\r\n\r\n```html\r\n\u003clocalize-section url=\"about.html\"\u003e\r\n  \u003cp\u003e\r\n    \u003cem\u003ePurePWA\u003c/em\u003e is an experiment by\r\n    \u003ca\r\n      rel=\"noopener\"\r\n      target=\"_blank\"\r\n      href=\"https://www.linkedin.com/in/mvneerven/\"\r\n      \u003eMarc van Neerven\u003c/a\r\n    \u003e, a Fractional CTO with a passion for the \u003cem\u003eModern Web\u003c/em\u003e\r\n  \u003c/p\u003e\r\n\r\n  \u003cp\u003e\r\n    The aim was to \u003cem\u003esimplify boilerplate for PWAs\u003c/em\u003e, while just using the\r\n    \u003cem\u003eModern Web\u003c/em\u003e, and nothing else.\r\n  \u003c/p\u003e\r\n\r\n  \u003cp\u003e\r\n    The Modern Web is a beautiful place, but many Web Developers seem to missed\r\n    that, with the 'framework path' being what everybody gets taught.\r\n  \u003c/p\u003e\r\n\u003c/localize-section\u003e\r\n```\r\n\r\nThe `url` attribute refers to a file under `/public/assets/locale/\u003c2-letter-language-code\u003e/`. You can see a few examples of these HTML block files under `/public/assets/locale/nl/`\r\n\r\n## Localization Under the Hood\r\n\r\nSince a lot of PurePWA uses Progressive Enhancement, we had to come up with an easy way to localize-in-place. This is quite different from a Framework app that generates UI from code, like React, Angular of Vue.\r\n\r\nWe use the `Document.createNodeIterator()` method together with a custom Node Filtering to make very efficient `#text` node changes based on our localization strings.\r\n\r\n```js\r\nconst textFilter = (node) =\u003e {\r\n  return node.nodeType === Node.TEXT_NODE\r\n    ? NodeFilter.FILTER_ACCEPT\r\n    : NodeFilter.FILTER_SKIP;\r\n};\r\n\r\nconst iterator = document.createNodeIterator(\r\n  document.body,\r\n  NodeFilter.SHOW_TEXT,\r\n  textFilter\r\n);\r\nlet node = iterator.nextNode();\r\n\r\nwhile (node) {\r\n  // translate\r\n  node = iterator.nextNode();\r\n}\r\n```\r\n\r\n# Releasing a PWA version\r\n\r\n## App name, version and file List (ServiceWorker)\r\n\r\nSince the PWA has a ServiceWorker that caches the app files, the array of application files needs to be in `service-worker.js`:\r\n\r\n```js\r\nconst app = {\r\n  name: \"pure-pwa\",\r\n  version: \"0.1.0\",\r\n  files: [\r\n    \"/about/index.html\",\r\n    \"/action/domain/tmdb.js\",\r\n    \"/action/index.html\",\r\n    \"/action/movies-api.js\",\r\n    \"/assets/css/app.css\",\r\n    \"/assets/css/app.css.map\",\r\n    [...]\r\n```\r\n\r\nTo update this array (if needed), you can run the PowerShell file in the project root:\r\n\r\n```powershell\r\n.\\update_cache_files.ps1\r\n```\r\n\r\nSince the project doesn't have bundling and NPM, this was an easy way to get the file list updated.\r\n\r\nThe .ps1 file updates `/public/assets/js/services/app-files.json`, and before deploying, I manually copy the array contents to the `service-worker.js` `app.files` array.\r\n\r\nBefore deploying to your cloud location, don't forget to update app.name (first deploy) \u0026 app.version (every deploy). These are used in the ServiceWorker code to refresh the cache.\r\n\r\n\r\n# Stripping Down to Boilerplate\r\n\r\n`/public/assets/js/app.js` contains a few lines that add the demo pages and some other demo-related stuff to the project:\r\n\r\n```js\r\nimport { mergeDeep, PurePWA } from \"./common.js\";\r\nimport { APP_SETTINGS } from \"./app-settings.js\";\r\nimport { DEMO_SETTINGS } from \"./demo-settings.js\";\r\n\r\n/**\r\n * TO REMOVE THE DEMO, CHANGE THE NEXT 2 LINES INTO THIS:\r\n * let appSettings = APP_SETTINGS;\r\n */\r\nconst appSettings = {}; // APP_SETTINGS\r\nmergeDeep(appSettings, APP_SETTINGS, DEMO_SETTINGS);\r\n\r\nwindow.purePWA = new PurePWA(appSettings); // Launch main PWA controlling component\r\n\r\nif (typeof navigator.serviceWorker !== \"undefined\")\r\n  navigator.serviceWorker.register(\"/assets/js/services/sw.js\");\r\n```\r\n\r\nCreating pure boilerplate thus comes down to changing the code:\r\n\r\n```js\r\nimport { mergeDeep, PurePWA } from \"./common.js\";\r\nimport { APP_SETTINGS } from \"./app-settings.js\";\r\n\r\nwindow.purePWA = new PurePWA(APP_SETTINGS); // Launch main PWA controlling component\r\n\r\nif (typeof navigator.serviceWorker !== \"undefined\")\r\n  navigator.serviceWorker.register(\"/assets/js/services/sw.js\");\r\n```\r\n\r\nOf course, you'll then remove the `about`, `purity`, `flow`, `entry`, `action` and `power` folders and adapt the `routes` property in `/public/assets/js.app-settings`.\r\n\r\n# New In\r\n\r\n## 0.1.6\r\n\r\n### `CustomElement.on` changes\r\n\r\n`on(eventNames, funcOrObject)`\r\n\r\n- `eventNames`: Attaches one or more event listeners. Space-separated.\r\n- `funcOrObject`: function, or object with selectors as keys and functions as values.\r\n\r\n\r\nExample:\r\n```js\r\nrendered() {\r\n  this.on(\"mousedown touchstart\", {\r\n    \".next\": (e) =\u003e {\r\n      // do something on button.next click\r\n    },\r\n    \".prev\": (e) =\u003e {\r\n      // do something on button.prev click\r\n    }\r\n  });\r\n}\r\n```\r\n\r\n## 0.1.0\r\n\r\n- Added a custom Splash screen (all other pages, including `/home/`, are now in their own folder)\r\n- Added support for Localization (see Localization). \r\n- Fixed `\u003cheader\u003e` HTML semantics\r\n\r\n# More Info\r\n\r\nIf you want to follow further developments of the `PurePWA` project, follow this repo, and these other sources:\r\n\r\n## Blogs\r\n\r\n- [PurePWA: Power \u0026amp; Purity](https://medium.com/javascript-in-plain-english/embracing-the-beauty-of-the-standards-in-2023-web-development-29dbeece2966)\r\n\r\n## LinkedIn\r\n\r\nFollow the [#purepwa](https://www.linkedin.com/feed/hashtag/?keywords=purepwa) hashtag.\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmvneerven%2Fpure-pwa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmvneerven%2Fpure-pwa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmvneerven%2Fpure-pwa/lists"}