Showing preview only (2,774K chars total). Download the full file or copy to clipboard to get everything.
Repository: jsebrech/plainvanilla
Branch: main
Commit: 25e704acad1e
Files: 352
Total size: 2.6 MB
Directory structure:
gitextract_5pqw1n3f/
├── .github/
│ └── workflows/
│ └── static.yml
├── .gitignore
├── .hintrc
├── LICENSE.md
├── README.md
├── eslint.config.cjs
└── public/
├── blog/
│ ├── archive.html
│ ├── articles/
│ │ ├── 2024-08-17-lets-build-a-blog/
│ │ │ ├── card.html
│ │ │ ├── example.html
│ │ │ ├── generator.js
│ │ │ └── index.html
│ │ ├── 2024-08-25-vanilla-entity-encoding/
│ │ │ ├── example1.js
│ │ │ ├── example2.js
│ │ │ ├── example3.js
│ │ │ ├── html.js
│ │ │ └── index.html
│ │ ├── 2024-08-30-poor-mans-signals/
│ │ │ ├── adder.html
│ │ │ ├── adder.js
│ │ │ ├── index.html
│ │ │ ├── preact-example.js
│ │ │ ├── signals.js
│ │ │ ├── signals1-use.js
│ │ │ ├── signals1.js
│ │ │ ├── signals2-use.js
│ │ │ ├── signals2.js
│ │ │ ├── signals3-use.js
│ │ │ └── signals3.js
│ │ ├── 2024-09-03-unix-philosophy/
│ │ │ ├── adder.svelte
│ │ │ ├── bind.js
│ │ │ ├── bind1.js
│ │ │ ├── bind2-partial.js
│ │ │ ├── bind3-partial.js
│ │ │ ├── bind4-partial.js
│ │ │ ├── example-bind3/
│ │ │ │ ├── bind.js
│ │ │ │ ├── example.html
│ │ │ │ ├── example.js
│ │ │ │ └── signals.js
│ │ │ ├── example-combined/
│ │ │ │ ├── adder.js
│ │ │ │ ├── bind.js
│ │ │ │ ├── example.html
│ │ │ │ ├── html.js
│ │ │ │ └── signals.js
│ │ │ └── index.html
│ │ ├── 2024-09-06-how-fast-are-web-components/
│ │ │ └── index.html
│ │ ├── 2024-09-09-sweet-suspense/
│ │ │ ├── error-boundary-partial.html
│ │ │ ├── error-boundary.js
│ │ │ ├── example/
│ │ │ │ ├── components/
│ │ │ │ │ ├── error-boundary.js
│ │ │ │ │ ├── error-message.js
│ │ │ │ │ ├── hello-world/
│ │ │ │ │ │ ├── hello-world.js
│ │ │ │ │ │ └── later.js
│ │ │ │ │ ├── lazy.js
│ │ │ │ │ └── suspense.js
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── index.html
│ │ │ ├── lazy1.js
│ │ │ ├── lazy2-partial.js
│ │ │ ├── lazy3-partial.js
│ │ │ ├── suspense1-partial.html
│ │ │ ├── suspense1.js
│ │ │ └── suspense2-partial.js
│ │ ├── 2024-09-16-life-and-times-of-a-custom-element/
│ │ │ ├── defined/
│ │ │ │ └── example.html
│ │ │ ├── defined2/
│ │ │ │ └── example.html
│ │ │ ├── index.html
│ │ │ ├── observer/
│ │ │ │ └── example.html
│ │ │ ├── shadowed/
│ │ │ │ └── example.html
│ │ │ └── undefined/
│ │ │ ├── example.css
│ │ │ └── example.html
│ │ ├── 2024-09-28-unreasonable-effectiveness-of-vanilla-js/
│ │ │ ├── complete/
│ │ │ │ ├── AddTask.js
│ │ │ │ ├── App.js
│ │ │ │ ├── TaskList.js
│ │ │ │ ├── TasksContext.js
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── styles.css
│ │ │ ├── index.html
│ │ │ └── react/
│ │ │ ├── package.json
│ │ │ ├── public/
│ │ │ │ └── index.html
│ │ │ └── src/
│ │ │ ├── AddTask.js
│ │ │ ├── App.js
│ │ │ ├── TaskList.js
│ │ │ ├── TasksContext.js
│ │ │ ├── index.js
│ │ │ └── styles.css
│ │ ├── 2024-09-30-lived-experience/
│ │ │ └── index.html
│ │ ├── 2024-10-07-needs-more-context/
│ │ │ ├── combined/
│ │ │ │ ├── context-provider.js
│ │ │ │ ├── context-request.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── theme-context.js
│ │ │ │ └── theme-provider.js
│ │ │ ├── context-provider.js
│ │ │ ├── context-request-1.js
│ │ │ ├── context-request-2.js
│ │ │ ├── context-request-3.js
│ │ │ ├── context-request-4.js
│ │ │ ├── index.html
│ │ │ ├── theme-context-fragment.html
│ │ │ ├── theme-context.js
│ │ │ └── theme-provider.js
│ │ ├── 2024-10-20-editing-plain-vanilla/
│ │ │ ├── .hintrc
│ │ │ ├── eslint.config.cjs
│ │ │ └── index.html
│ │ ├── 2024-12-16-caching-vanilla-sites/
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ ├── 2025-01-01-new-years-resolve/
│ │ │ ├── example-index.js
│ │ │ ├── index.html
│ │ │ ├── layout.js
│ │ │ └── layout.tsx
│ │ ├── 2025-04-21-attribute-property-duality/
│ │ │ ├── demo1.html
│ │ │ ├── demo1.js
│ │ │ ├── demo2.html
│ │ │ ├── demo2.js
│ │ │ ├── demo3.html
│ │ │ ├── demo3.js
│ │ │ ├── demo4.html
│ │ │ ├── demo4.js
│ │ │ ├── demo5-before.js
│ │ │ ├── demo5.html
│ │ │ ├── demo5.js
│ │ │ └── index.html
│ │ ├── 2025-05-09-form-control/
│ │ │ ├── demo1/
│ │ │ │ ├── index-partial.txt
│ │ │ │ ├── index.html
│ │ │ │ └── input-inline.js
│ │ │ ├── demo2/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo3/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo4/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo5/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline.css
│ │ │ │ └── input-inline.js
│ │ │ ├── demo6/
│ │ │ │ ├── index-partial.txt
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ ├── input-inline.css
│ │ │ │ └── input-inline.js
│ │ │ └── index.html
│ │ ├── 2025-06-12-view-transitions/
│ │ │ ├── example1/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── transitions.css
│ │ │ ├── example2/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── transitions.css
│ │ │ ├── example3/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ └── view-transition.js
│ │ │ ├── example4/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ └── view-transition.js
│ │ │ ├── example5/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ ├── view-transition-part.js
│ │ │ │ └── view-transition.js
│ │ │ ├── example6/
│ │ │ │ ├── index.html
│ │ │ │ ├── lib/
│ │ │ │ │ ├── html.js
│ │ │ │ │ ├── view-route.js
│ │ │ │ │ └── view-transition.js
│ │ │ │ └── src/
│ │ │ │ ├── App.js
│ │ │ │ ├── Details.js
│ │ │ │ ├── Home.js
│ │ │ │ ├── Icons.js
│ │ │ │ ├── Layout.js
│ │ │ │ ├── LikeButton.js
│ │ │ │ ├── Videos.js
│ │ │ │ ├── animations.css
│ │ │ │ ├── data.js
│ │ │ │ ├── index.js
│ │ │ │ └── styles.css
│ │ │ └── index.html
│ │ ├── 2025-06-25-routing/
│ │ │ ├── example1/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ └── view-route.js
│ │ │ ├── example2/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ ├── view-route-partial.js
│ │ │ │ ├── view-route-partial2.js
│ │ │ │ └── view-route.js
│ │ │ ├── example3/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ ├── view-route-partial.js
│ │ │ │ └── view-route.js
│ │ │ ├── example4/
│ │ │ │ ├── 404.html
│ │ │ │ └── index-partial.html
│ │ │ └── index.html
│ │ ├── 2025-07-13-history-architecture/
│ │ │ └── index.html
│ │ ├── 2025-07-16-local-first-architecture/
│ │ │ ├── example1.html
│ │ │ └── index.html
│ │ ├── 2026-03-01-redesigning-plain-vanilla/
│ │ │ ├── index.html
│ │ │ └── nav-menu.html
│ │ ├── 2026-03-09-details-matters/
│ │ │ ├── example1/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example2/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example3/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example4/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example5/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ └── index.html
│ │ └── index.json
│ ├── components/
│ │ ├── blog-archive.js
│ │ ├── blog-footer.js
│ │ ├── blog-header.js
│ │ └── blog-latest-posts.js
│ ├── example-base.css
│ ├── feed.xml
│ ├── generator.html
│ ├── generator.js
│ ├── index.css
│ ├── index.html
│ └── index.js
├── components/
│ ├── analytics/
│ │ └── analytics.js
│ ├── code-viewer/
│ │ ├── code-viewer.css
│ │ └── code-viewer.js
│ └── tab-panel/
│ ├── tab-panel.css
│ └── tab-panel.js
├── index.css
├── index.html
├── index.js
├── lib/
│ ├── html.js
│ └── speed-highlight/
│ ├── LICENSE
│ ├── common.js
│ ├── index.js
│ ├── languages/
│ │ ├── css.js
│ │ ├── html.js
│ │ ├── js.js
│ │ ├── js_template_literals.js
│ │ ├── jsdoc.js
│ │ ├── json.js
│ │ ├── log.js
│ │ ├── plain.js
│ │ ├── regex.js
│ │ ├── todo.js
│ │ ├── ts.js
│ │ ├── uri.js
│ │ └── xml.js
│ └── themes/
│ ├── default.css
│ ├── github-dark.css
│ └── github-light.css
├── manifest.json
├── pages/
│ ├── applications.html
│ ├── components.html
│ ├── examples/
│ │ ├── applications/
│ │ │ ├── counter/
│ │ │ │ ├── components/
│ │ │ │ │ └── counter.js
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── lifting-state-up/
│ │ │ │ ├── components/
│ │ │ │ │ ├── accordion.js
│ │ │ │ │ └── panel.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── react/
│ │ │ │ └── App.js
│ │ │ ├── passing-data-deeply/
│ │ │ │ ├── components/
│ │ │ │ │ ├── button.js
│ │ │ │ │ ├── panel.js
│ │ │ │ │ └── theme-context.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ └── tiny-context.js
│ │ │ └── single-page/
│ │ │ ├── app/
│ │ │ │ └── App.js
│ │ │ ├── components/
│ │ │ │ └── route/
│ │ │ │ └── route.js
│ │ │ ├── index.css
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ ├── components/
│ │ │ ├── adding-children/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ ├── avatar.js
│ │ │ │ │ ├── badge.css
│ │ │ │ │ └── badge.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── advanced/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ └── avatar.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── simple.html
│ │ │ ├── data/
│ │ │ │ ├── components/
│ │ │ │ │ ├── app.js
│ │ │ │ │ ├── form.js
│ │ │ │ │ ├── list-safe.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ └── summary.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── shadow-dom/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ ├── avatar.js
│ │ │ │ │ ├── badge.css
│ │ │ │ │ ├── badge.js
│ │ │ │ │ ├── header.css
│ │ │ │ │ └── header.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── reset.css
│ │ │ └── simple/
│ │ │ ├── hello-world.js
│ │ │ └── index.html
│ │ ├── sites/
│ │ │ ├── importmap/
│ │ │ │ ├── components/
│ │ │ │ │ └── metrics.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ ├── dayjs/
│ │ │ │ │ ├── module.js
│ │ │ │ │ └── relativeTime.js
│ │ │ │ └── web-vitals.js
│ │ │ ├── imports/
│ │ │ │ ├── components/
│ │ │ │ │ └── metrics.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ ├── dayjs/
│ │ │ │ │ └── relativeTime.js
│ │ │ │ ├── imports.js
│ │ │ │ └── web-vitals.js
│ │ │ └── page/
│ │ │ ├── example.html
│ │ │ ├── example2.html
│ │ │ └── index.js
│ │ └── styling/
│ │ ├── replacing-css-modules/
│ │ │ ├── nextjs/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── styles.module.css
│ │ │ └── vanilla/
│ │ │ ├── layout.js
│ │ │ └── styles.css
│ │ ├── scoping-prefixed/
│ │ │ ├── components/
│ │ │ │ └── example/
│ │ │ │ ├── example.css
│ │ │ │ ├── example.js
│ │ │ │ └── example_nested.css
│ │ │ ├── index.css
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ └── scoping-shadowed/
│ │ ├── components/
│ │ │ └── example/
│ │ │ ├── example.css
│ │ │ └── example.js
│ │ ├── index.html
│ │ └── index.js
│ ├── sites.html
│ └── styling.html
├── robots.txt
├── sitemap.txt
├── styles/
│ ├── global.css
│ ├── reset.css
│ └── variables.css
└── tests/
├── imports-test.js
├── index.html
├── index.js
├── lib/
│ ├── @testing-library/
│ │ └── dom.umd.js
│ └── mocha/
│ ├── chai.js
│ ├── mocha.css
│ └── mocha.js
└── tabpanel.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/static.yml
================================================
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Create analytics file
run: echo "${{ vars.CLOUDFLARE_ANALYTICS }}" > public/analytics.template
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Deploy public folder only
path: './public'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .gitignore
================================================
.DS_Store
*.pem
public/analytics.template
================================================
FILE: .hintrc
================================================
{
"extends": [
"development"
],
"hints": {
"compat-api/html": [
"default",
{
"ignore": [
"iframe[loading]",
"nav[popover]",
"button[popovertarget]",
"button[popovertargetaction]"
]
}
],
"no-inline-styles": "off",
"compat-api/css": [
"default",
{
"ignore": [
"view-transition-name",
"text-size-adjust",
"text-size-adjust: none",
"overscroll-behavior",
"overscroll-behavior: none"
]
}
]
}
}
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2024 Joeri Sebrechts
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Plain Vanilla
A website demonstrating how to do web development using only vanilla techniques: no tools, no frameworks, just the browser and vanilla web code. The site itself is also built in this way.
## Running
Run the `public/` folder as a static website:
- node: `npx http-server public -c-1`
- php: `php -S localhost:8000 -t public`
- python: `python3 -m http.server 8000 --directory public`
Or use the VS Code Live Preview extension to show `public/index.html`.
## Contributing
Issues or PR's welcome!
## Other resources
These are some other resources demonstrating #notools web development techniques.
- [MDN Learn Web Development](https://developer.mozilla.org/en-US/docs/Learn): a vanilla web development learning path
- [Odin Project Foundations](https://www.theodinproject.com/paths/foundations/courses/foundations): a vanilla web development course
- [create-react-app-zero](https://github.com/jsebrech/create-react-app-zero): another project of mine, a no-tools version of create-react-app, to be able to use React without frills
- [HEX: a No Framework Approach to Building Modern Web Apps](https://medium.com/@metapgmr/hex-a-no-framework-approach-to-building-modern-web-apps-e43f74190b9c): a React-like approach based on vanilla web development techniques
- [plainJS](https://plainjs.com/): a collection of vanilla javascript functions and plugins to replace the use of jQuery
- [Web Dev Toolkit](https://gomakethings.com/toolkit/): a collection of vanilla helper functions, boilerplates and libraries
- [The Modern JavaScipt Tutorial](https://javascript.info/): a step-by-step tutorial to learn vanilla JavaScript
## Attribution
Soft ice icon by [Twemoji](https://github.com/jdecked/twemoji) is licensed under [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/).
================================================
FILE: eslint.config.cjs
================================================
/* eslint-disable no-undef */
const globals = require("globals");
const js = require("@eslint/js");
module.exports = [
js.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.mocha
},
ecmaVersion: 2022,
sourceType: "module",
}
},
{
ignores: [
"public/blog/articles/",
"**/lib/",
"**/react/",
]
}
];
================================================
FILE: public/blog/archive.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Plain Vanilla Blog</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="description" content="A blog about vanilla web development.">
<link rel="icon" href="../favicon.ico">
<link rel="apple-touch-icon" href="../apple-touch-icon.png">
<link rel="stylesheet" href="./index.css">
</head>
<body>
<noscript><strong>Please enable JavaScript to view this page correctly.</strong></noscript>
<header>
<h1>Plain Vanilla Blog</h1>
<nav aria-label="breadcrumb">
<ol>
<li><a href="../index.html">Plain Vanilla</a></li>
<li><a href="./index.html">Blog</a></li>
<li><a href="#" aria-current="page">Archive</a></li>
</ol>
</nav>
</header>
<main>
<h2>Archive</h2>
<blog-archive><noscript>Please enable scripting to see the archives.</noscript></blog-archive>
</main>
<blog-footer></blog-footer>
<script type="module" src="./index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-08-17-lets-build-a-blog/card.html
================================================
<ul class="cards">
<li class="card">
<img src="./articles/2024-08-17-lets-build-a-blog/image.webp" aria-hidden="true" loading="lazy" />
<h3><a href="./articles/2024-08-17-lets-build-a-blog/">Let's build a blog, vanilla-style!</a></h3>
<p>Explaining how this vanilla web development blog was built, using nothing but vanilla web techniques.</p>
<small>
<time datetime="2024-08-17">August 17, 2024</time>
</small>
</li>
</ul>
================================================
FILE: public/blog/articles/2024-08-17-lets-build-a-blog/example.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>A spiffy title!</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="This is a spiffy article. You should read it.">
<link rel="stylesheet" href="index.css">
</head>
<body>
<blog-header published="2024-08-17">
<img src="image.jpeg" alt="Another AI image" loading="lazy" />
<h2>A spiffy title!</h2>
<p class="byline" aria-label="author">Malkovich</p>
</blog-header>
<main>
Article text goes here ...
</main>
<blog-footer mastodon-url="https://example.com/@jmalkovich/12345"></blog-footer>
<script type="module" src="index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-08-17-lets-build-a-blog/generator.js
================================================
customElements.define('blog-generator', class BlogGenerator extends HTMLElement {
// ...
async processArticle(article, path) {
const file = await article.getFile();
const html = await file.text();
const dom = (new DOMParser()).parseFromString(html, 'text/html');
// mandatory
const title = dom.querySelector('title').textContent;
const summary = dom.querySelector('meta[name="description"]').getAttribute('content');
const published = dom.querySelector('blog-header').getAttribute('published');
const content = await this.processArticleContent(dom.querySelector('main'), path);
const slug = path.name;
// optional
const img = dom.querySelector('blog-header img');
const image = img && { src: img.getAttribute('src'), alt: img.getAttribute('alt') };
const updated = dom.querySelector('blog-header').getAttribute('updated') || undefined;
this.#articles.push({
slug, title, summary, content, published, updated, image
});
}
async processArticleContent(main, path) {
// inline code examples
await Promise.all([...main.querySelectorAll('x-code-viewer')].map(async (elem) => {
const text = await this.downloadFile(elem.getAttribute('src'), path);
const pre = document.createElement('pre');
pre.innerHTML = html`<code>${text}</code>`;
elem.replaceWith(pre);
}));
// convert img src to absolute url
[...main.querySelectorAll('img')].map((elem) => {
const src = elem.getAttribute('src');
if (src.indexOf('http') !== 0) {
elem.setAttribute('src', new URL(`articles/${path.name}/${src}`, BLOG_BASE_URL));
}
});
// replace iframes by links
[...main.querySelectorAll('iframe')].map((elem) => {
const src = elem.getAttribute('src');
const title = elem.getAttribute('title') || src;
const a = document.createElement('a');
a.textContent = title;
const p = document.createElement('p');
p.appendChild(a);
elem.replaceWith(p);
if (src.indexOf('http') !== 0) {
a.href = new URL(`articles/${path.name}/${src}`, BLOG_BASE_URL);
} else {
a.href = src;
}
});
return main.innerHTML;
}
// ...
});
================================================
FILE: public/blog/articles/2024-08-17-lets-build-a-blog/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Let's build a blog, vanilla-style!</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="Explaining how this vanilla web development blog was built, using nothing but vanilla web techniques.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-08-17" updated="2024-08-26">
<img src="image.webp" alt="Bricks being laid by hand" loading="lazy" />
<h2>Let's build a blog, vanilla-style!</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
As I write this paragraph it is my birthday, and it seemed like as good an opportunity as any to start a blog about vanilla web development.
This blog post will be a bit unusual, as I will be writing it while I'm making the blog's inner workings.
But before I get around to figuring out and then explaining how it was made, let me start with why.
</p>
<section>
<h3>Origin story</h3>
<p>
I have been building web sites since the late 90's, and over the years there were always two constants:
(1) browsers were terrible developer platforms, (2) new tools and frameworks built ever taller on top of them.
The tools were necessary, but their growing complexity frustrated me, and in that frustration lies the origin of this blog.
</p>
<p>
A few years ago something unexpected happened: Microsoft moved away from their (underfeatured) Trident browser engine.
Suddenly there was a new baseline of browsers, a capable baseline. Browsers got good!
I explored what modern browsers could do as a developer platform, and grew excited with the possibilities
to treat the browser itself as the framework, without a middleman.
That eventually led into making the <a href="https://plainvanillaweb.com">Plain Vanilla website</a>, a framework tutorial for the web standards platform.
</p>
<p>
In building this website editorial choices had to be made.
Trying to explain too much would only confuse people, so the tutorial was trimmed of its fat.
There is however so much more to explore, and that is where this blog enters the picture.
Here I will talk about some of the things that didn't find a home in the Plain Vanilla tutorial,
and document the new things that do.
</p>
</section>
<section>
<h3>What is a blog anyway?</h3>
<p>
Of course, a blog about vanilla web development has to be built vanilla-style.
That means no build steps, no server-side logic, no frameworks or libraries.
Bottom line that means throwing up a bunch of HTML pages with an index page linking them together,
but that by itself isn't enough. The idea is to make a full-fat modern feeling blog,
something that has what people expect a blog to have. So off I went to look at popular blogs and build a list of features.
</p>
<p>A <em>modern</em> blog will have ...</p>
<ul>
<li>One page per article, easy for sharing as a link and discovery by Google.</li>
<li>A welcome page, with one or more hero cards leading to articles and a list of cards for recent articles.</li>
<li>An archive page, with the full history of articles linking to the article pages.</li>
<li>An RSS feed, for the 20 most recent articles, containing the full text.</li>
<li>Comments on every article. This is a big one.</li>
<li>Some colors and imagery, to spruce things up and please the readers.</li>
<li>Easy to author articles. This is also a big one.</li>
</ul>
<p>The challenge was: how to do all of that within the vanilla constraints that I set myself?</p>
</section>
<section>
<h3>Article-first design</h3>
<p>
The core of the blog experience is the article, so getting that right is key and that makes it the best place to start.
Lacking any kind of generator or server-side routing, each article has to be written as a discrete html page
in order to be discoverable by Google. Authoring those html pages should be straightforward,
with minimal boilerplate.
</p>
<p>After careful consideration we present to you, an article page blueprint...</p>
<x-code-viewer src="example.html"></x-code-viewer>
<p>
This does several things for me. It keeps the <head> section as minimal as possible.
It also moves the navigation at the top and bottom into dedicated web components.
The header component accepts the article's image and title as child elements,
neatly leaving the main element containing just the article's content and nothing else,
making it easy to extract (but more on that later).
</p>
<p>
When users have scripting disabled they won't get the header navigation,
but thanks to this CSS they do get a warning:<br>
<code>@media (scripting: none) { blog-header::before { content: ' ... ' } }</code><br>
This approach frees me from thinking about noscript warnings while writing an article.
</p>
<p>
Finally, for comments I considered Disqus, but didn't want to include their embed.
So instead the footer accepts the URL to a mastodon toot about the article,
and will automatically generate a link that goes there. Given that the blog has a technical audience
I'm pretty sure they can figure out how to reply over there. This approach can be extended
to <a href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">show replies inline on the page</a>
by calling the Mastodon API, but I didn't tackle that yet. It's somewhat cumbersome to first post an article,
then toot about it, and then update the article with the toot's URL, but I'll survive.
Incidentally, I still giggle like a schoolgirl inside my head every time I type the word <em>toot</em>.
</p>
</section>
<section>
<h3>Organizing files</h3>
<p>
Next comes the question how to organize the article files into a coherent structure.
After thinking it over, this is what I landed on:
</p>
<ul>
<li><code>articles/</code>
<ul>
<li><code><em>YYYY-MM-DD</em>-some-blog-title/</code>
<ul>
<li><code>index.html</code></li>
<li><code>image.jpeg</code></li>
<li>other files used in the article ...</li>
</ul>
</li>
</ul>
</li>
<li><code>components/</code>: the blog's shared web components</li>
<li><code>index.html</code>: the main landing page</li>
<li><code>archive.html</code>: the archives page</li>
<li><code>index.js/css</code>: shared resources</li>
<li><code>feed.xml</code>: the RSS feed</li>
</ul>
<p>
By wrapping every article and all its resources into a folder, each article can get as messy and complicated as it wants.
The shared index.js and index.css is separate from that of the main site
to keep the blog's resources out of the Plain Vanilla site's pages, and vice versa.
</p>
</section>
<section>
<h3>Building indexes</h3>
<p>
You wouldn't think a blog has a need for many indexes, but in fact this modest blog will have three:
</p>
<ol>
<li>The recent posts section on the main landing page</li>
<li>The recent posts in the RSS feed</li>
<li>The full list of articles in the archive page</li>
</ol>
<p>
Visually showing an index is not so difficult, as a web component built around a simple <code><li></code>-based
card design can be used to show both the recent posts and the archive page, and was straighforward to style with CSS.
</p>
<x-code-viewer src="card.html"></x-code-viewer>
<p>
Getting that data in a convenient form however is another matter.
The RSS feed contains full text contents, so needs a separate step to build from the articles' HTML.
The recent posts section on the index page thankfully can be built by reading the RSS feed,
so was trivial to solve with a <code><blog-latest-posts></code> web component once the feed was set up.
The full list of articles however cannot, as the RSS feed would grow too large if it contained all posts.
So the archive page needs another separate step to build the full list of links from the folder of articles.
</p>
<p>
For these build steps I considered various options:
</p>
<dl>
<dt>❌ <del>Manually keeping the files in sync</del><dt>
<dd>
It sounded like a lot of work, and error-prone, so a hard no on that one.
</dd>
<dt>❌ <del>A generator script, and a package.json</del></dt>
<dd>
This is what I would normally go for, relying on a bunch of npm dependencies and a bunch of scripting
to process the articles into the index files that are needed.
It felt like cheating to bring in node and its ecosystem, so again this was a no.
</dd>
<dt>✅ A separate generator webpage</dt>
<dd>
I've wanted to play around with the
<a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API">File System API</a>
for a while, and this seemed a good opportunity.
Turning the generator into a webpage also leaves options for actually running the web components
and extracting their dynamically rendered content.
</dd>
</dl>
<p>
For the generator page I built a dedicated web component that allows opening or dropping the
local <code>blog/</code> folder with the newly written or updated articles,
and then will process those into a <code>feed.xml</code> and <code>index.json</code>.
The JSON file is used to load the contents of the archive page.
The RSS feed is particularly tricky, because there is a limited set of tags that it should contain.
By loading the article's HTML into a DOM parser, and replacing all unsupported tags (like the code viewer web component shown below)
the HTML can be transformed into something that can be used as RSS feed entry contents.
</p>
<p>The core logic of the generator extracts the article's metadata and transforms the HTML:</p>
<x-code-viewer src="generator.js"></x-code-viewer>
<p>To give you an idea of what <a href="../../generator.html">generator.html</a> looks like in use:</p>
<img src="generator.webp" alt="generator page screenshot" />
<p>
The generator JS ended up around 250 lines of code, so not too cumbersome to build or maintain.
If you're curious about the generator, check out the
<a href="https://github.com/jsebrech/plainvanilla/tree/main/public/blog/">blog's code on Github</a>.
It can be found in <code>generator.html</code> and <code>generator.js</code>.
</p>
<p>The user experience of writing a blog post then boils down to this:</p>
<ol>
<li>Create an article folder and write the article as HTML</li>
<li>Open the generator page</a></li>
<li>Drop the blog folder on the generator, it will automatically process the articles</li>
<li>Copy the <code>feed.xml</code> and <code>index.json</code> text to their respective files</li>
<li>Commit and push the changes</li>
<li>Optionally: toot on mastodon, add the toot URL in the page, commit and push</li>
</ol>
<p>Not too shabby...</p>
</section>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113030045608088158"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example1.js
================================================
class MyComponent extends HTMLElement {
connectedCallback() {
const btn = `<button>${this.getAttribute('foo')}</button>`;
this.innerHTML = `
<header><h1>${this.getAttribute('bar')}</h1></header>
<article>
<p class="${this.getAttribute('baz')}">${this.getAttribute('xyzzy')}</p>
${btn}
</article>
`;
}
}
customElements.define('my-component', MyComponent);
================================================
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example2.js
================================================
function htmlEncode(s) {
return s.replace(/[&<>'"]/g,
tag => ({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag]))
}
class MyComponent extends HTMLElement {
connectedCallback() {
const btn = `<button>${htmlEncode(this.getAttribute('foo'))}</button>`;
this.innerHTML = `
<header><h1>${htmlEncode(this.getAttribute('bar'))}</h1></header>
<article>
<p class="${htmlEncode(this.getAttribute('baz'))}">${htmlEncode(this.getAttribute('xyzzy'))}</p>
${btn}
</article>
`;
}
}
customElements.define('my-component', MyComponent);
================================================
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example3.js
================================================
import { html } from './html.js';
class MyComponent extends HTMLElement {
connectedCallback() {
const btn = html`<button>${this.getAttribute('foo')}</button>`;
this.innerHTML = html`
<header><h1>${this.getAttribute('bar')}</h1></header>
<article>
<p class="${this.getAttribute('baz')}">${this.getAttribute('xyzzy')}</p>
${btn}
</article>
`;
}
}
customElements.define('my-component', MyComponent);
================================================
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/html.js
================================================
class Html extends String { }
/**
* tag a string as html not to be encoded
* @param {string} str
* @returns {string}
*/
export const htmlRaw = str => new Html(str);
/**
* entity encode a string as html
* @param {*} value The value to encode
* @returns {string}
*/
export const htmlEncode = (value) => {
// avoid double-encoding the same string
if (value instanceof Html) {
return value;
} else {
// https://stackoverflow.com/a/57448862/20980
return htmlRaw(
String(value).replace(/[&<>'"]/g,
tag => ({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag]))
);
}
}
/**
* html tagged template literal, auto-encodes entities
*/
export const html = (strings, ...values) =>
htmlRaw(String.raw({ raw: strings }, ...values.map(htmlEncode)));
================================================
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Vanilla entity encoding</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="The first version of this site didn't use entity encoding in the examples. Now it does.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-08-25">
<img src="image.webp" alt="A man working a printing press printing HTML code" loading="lazy" />
<h2>Vanilla entity encoding</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<h3>Good enough</h3>
<p>
When I made the first version of the Plain Vanilla website, there were things that I would have liked
to spend more time on, but that I felt didn't belong in a Good Enough™ version of the site.
One of those things was defending against <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html">Cross-Site Scripting</a> (XSS).
</p>
<p>
XSS is still in the <a href="https://owasp.org/www-project-top-ten/">OWASP Top Ten</a> of security issues,
but it's no longer as prevalent as it used to be. Frameworks have built in a lot of defenses,
and when using their templating systems you have to go out of your way to inject code into the generated HTML.
When eschewing frameworks we're reduced to standard templating in our web components, and those offer no defense against XSS.
</p>
<p>
Because of this, in the original site the <a href="https://plainvanillaweb.com/pages/components.html#passing-data">Passing Data example</a>
on the <em>Components</em> page had an undocumented XSS bug.
The <em>name</em> field could have scripts injected into it. I felt ambivalent about leaving that bug in.
On the one hand, the code was very compact and neat by leaving it in.
On the other hand it made that code a bad example that shouldn't be copied.
I ended up choosing to leave it as-is because an example doesn't have to be production-grade
and generating properly encoded HTML was not the point of that specific example.
It's time however to circle back to that XSS bug and figure out how it would have been solved in a clean and readable way,
if Santa really did want to bring his List application to production-level quality.
</p>
<h3>The problem</h3>
<p>
The basic problem we need to solve is that vanilla web components end up having a lot of code that looks like this:
</p>
<x-code-viewer src="example1.js"></x-code-viewer>
<p>
If any of <code>foo</code>, <code>bar</code>, <code>baz</code> or <code>xyzzy</code> contain one of the dangerous HTML entities,
we risk seeing our component break, and worst-case risk seeing an attacker inject a malicious payload into the page.
Just as a reminder, those dangerous HTML entities are <, >, &, ' and ".
</p>
<h3>The fix, take one</h3>
<p>
A naive fix is creating a html-encoding function and using it consistently:
</p>
<x-code-viewer src="example2.js"></x-code-viewer>
<p>
While this does work to defend against XSS, it is verbose and ugly, not pleasant to type and not pleasant to read.
What really kills it though, is that it assumes attention to detail from us messy humans. We can never forget,
never ever, to put a <code>htmlEncode()</code> around each and every variable.
In the real world, that is somewhat unlikely.
</p>
<p>
What is needed is a solution that allows us to forget about entity encoding, by doing it automatically
when we're templating. I drew inspiration from templating libraries that work in-browser and are based on tagged templates,
like <a href="https://lit.dev/docs/api/templates/#html">lit-html</a>
and <a href="https://github.com/developit/htm">htm</a>. The quest was on to build the most minimalistic
html templating function that encoded entities automatically.
</p>
<h3>The fix, take two</h3>
<p>
Ideally, the fixed example should look more like this:
</p>
<x-code-viewer src="example3.js"></x-code-viewer>
<p>
The <code>html``</code> <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates">tagged template function</a>
would automatically encode entities, in a way that we don't even have to think about it.
Even when we nest generated HTML inside of another template, like with <code>${btn}</code>, it should just magically work.
It would be so minimal as to disappear in the background, barely impacting readability, maybe even improving it.
You may be thinking that doing that correctly would involve an impressive amount of code. I must disappoint.
</p>
<x-code-viewer src="html.js"></x-code-viewer>
<p>
Those couple dozen lines of code are all that is needed. Let's go through it from top to bottom.
</p>
<dl>
<dt><code>class Html extends String { }</code></dt>
<dd>The Html class is used to mark strings as encoded, so that they won't be encoded again.</dd>
<dt><code>export const htmlRaw = str => new Html(str);</code></dt>
<dd>Case in point, the htmlRaw function does the marking.</dd>
<dt><code>export const htmlEncode = ...</code></dt>
<dd>The earlier htmlEncode function is still doing useful work, only this time it will mark the resulting string as HTML, and it won't double-encode.</dd>
<dt><code>export const html = ...</code></dt>
<dd>The tagged template function that binds it together.</dd>
</dl>
<p>
A nice upside of the html template function is that the <em>html-in-template-string</em> Visual Studio Code extension
can detect it automatically and will syntax highlight the templated HTML. This is what example 3 looked like after I made it:
</p>
<img src="syntax-highlighting.webp" alt="example 3 with syntax highlighting" />
<p>
Granted, there's still a bunch of boilerplate here, and that <code>getAttribute</code> gets unwieldy.
But with this syntax highlighting enabled sometimes when I'm working on vanilla web components I forget it's not React and JSX, but just HTML and JS.
It's surprising how nice of a development experience web standards can be if you embrace them.
</p>
<p>
I decided to leave the XSS bug in the <em>Passing Data</em> example, but now the <em>Applications</em> page
has an explanation about entity encoding documenting this html template function.
I can only hope people that work their way through the tutorial make it that far.
For your convenience I also put the HTML templating function in its own separate
<a href="https://github.com/jsebrech/html-literal">html-literal repo on Github</a>.
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113030056958516573"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/adder.html
================================================
<!doctype html>
<head>
<title>Adder example</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
</head>
<body>
<style>
:root { margin: 0.5em; font-family: system-ui, sans-serif; }
</style>
<script type="module" src="adder.js"></script>
<x-adder></x-adder>
</body>
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/adder.js
================================================
import { signal, computed } from './signals.js';
customElements.define('x-adder', class extends HTMLElement {
a = signal(1);
b = signal(2);
result = computed((a, b) => `${a} + ${b} = ${+a + +b}`, [this.a, this.b]);
connectedCallback() {
if (this.querySelector('input')) return;
this.innerHTML = `
<input type="number" name="a" value="${this.a}">
<input type="number" name="b" value="${this.b}">
<p></p>
`;
this.result.effect(
() => this.querySelector('p').textContent = this.result);
this.addEventListener('input',
e => this[e.target.name].value = e.target.value);
}
});
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Poor man's signals</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="Signals are all the rage over in frameworkland, so let's bring them to vanilla JS.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-08-30">
<img src="image.webp" alt="Train signals with mountains in the distance" loading="lazy" />
<h2>Poor man's signals</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
Signals are all the rage right now. Everyone's doing them.
<a href="https://angular.dev/guide/signals">Angular</a>,
and <a href="https://docs.solidjs.com/concepts/signals">Solid</a>,
and <a href="https://preactjs.com/guide/v10/signals/">Preact</a>,
and there are third party packages for just about every framework that doesn't already have them.
There's even a <a href="https://github.com/tc39/proposal-signals">proposal</a>
to add them to the language, and if that passes it's just a
<a href="https://thenewstack.io/did-signals-just-land-in-react/">matter of time</a> before all frameworks
have them built in.
</p>
<h3>Living under a rock</h3>
<p>
In case you've been living under a rock, here's the example from Preact's documentation
that neatly summarizes what signals do:
</p>
<x-code-viewer src="preact-example.js" name=""></x-code-viewer>
<p>
Simply put, signals wrap values and computations
in a way that allows us to easily respond to every change to those values and results in a targeted way,
without having to rerender the entire application in the way that we would do in React.
In short, signals are an efficient and targeted way to respond to changes without having to do state comparison and DOM-diffing.
</p>
<p>
OK, so, if signals are so great, why am I trying to sell you on them on a vanilla web development blog?
Don't worry! Vanilla web developers can have signals too.
</p>
<h3>Just a wrapper</h3>
<p>
Signals are at heart nothing more than a wrapper for a value that sends events when the value changes.
That's nothing that a little trickery with the not well known but very handy <code>EventTarget</code> base class can't fix for us.
</p>
<x-code-viewer src="signals1.js" name=""></x-code-viewer>
<p>
This gets us a very barebones signals experience:
</p>
<x-code-viewer src="signals1-use.js" name=""></x-code-viewer>
<p>
But that's kind of ugly. The <code>new</code> keyword went out of fashion a decade ago,
and that <code>addEventListener</code> sure is unwieldy.
So let's add a little syntactic sugar.
</p>
<x-code-viewer src="signals2.js" name=""></x-code-viewer>
<p>
Now our barebones example is a lot nicer to use:
</p>
<x-code-viewer src="signals2-use.js" name=""></x-code-viewer>
<p>
The <code>effect(fn)</code> method will call the specified function,
and also subscribe it to changes in the signal's value.
</p>
<p>
It also returns a dispose function that can be used to unregister the effect.
However, a nice side effect of using <code>EventTarget</code> and browser built-in events as the reactivity primitive
is that it makes the browser smart enough to garbage collect the signal and its effect when the signal goes out of scope.
This means less chance for memory leaks even if we never call the dispose function.
</p>
<p>
Finally, the <code>toString</code> and <code>valueOf</code> magic methods allow for dropping <code>.value</code> in most places
that the signal's value gets used. (But not in this example, because the console is far too clever for that.)
</p>
<h3>Does not compute</h3>
<p>
This signals implementation is already capable, but at some point it might be handy to have an effect based on more than one signal.
That means supporting computed values. Where the base signals are a wrapper around a value,
computed signals are a wrapper around a function.
</p>
<x-code-viewer src="signals3.js" name=""></x-code-viewer>
<p>
The computed signal calculates its value from a function.
It also depends on other signals, and when they change it will recompute its value.
It's a bit obnoxious to have to pass the signals that it depends on
as an additional parameter, but hey, I didn't title this article <em>Rich man's signals</em>.
</p>
<p>
This enables porting Preact's signals example to vanilla JS.
</p>
<x-code-viewer src="signals3-use.js" name=""></x-code-viewer>
<h3>Can you use it in a sentence?</h3>
<p>
You may be thinking, all these <code>console.log</code> examples are fine and dandy,
but how do you use this stuff in actual web development?
This simple adder demonstrates how signals can be combined with web components:
</p>
<x-code-viewer src="adder.js"></x-code-viewer>
<p>
And here's a live demo:
</p>
<iframe src="adder.html" name="Adder example"></iframe>
<p>
In case you were wondering, the <code>if</code> is there to prevent adding the effect twice
if connectedCallback is called when the component is already rendered.
</p>
<p>
The full poor man's signals code in all its 36 line glory can be found in the <a href="https://github.com/jsebrech/tiny-signals/">tiny-signals repo</a> on Github.
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113051808087212046"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/preact-example.js
================================================
import { signal, computed, effect } from "@preact/signals";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => `${name.value} ${surname.value}`);
// Logs name every time it changes:
effect(() => console.log(fullName.value));
// Logs: "Jane Doe"
// Updating `name` updates `fullName`, which triggers the effect again:
name.value = "John";
// Logs: "John Doe"
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals.js
================================================
export class Signal extends EventTarget {
#value;
get value () { return this.#value; }
set value (value) {
if (this.#value === value) return;
this.#value = value;
this.dispatchEvent(new CustomEvent('change'));
}
constructor (value) {
super();
this.#value = value;
}
effect(fn) {
fn();
this.addEventListener('change', fn);
return () => this.removeEventListener('change', fn);
}
valueOf () { return this.#value; }
toString () { return String(this.#value); }
}
export class Computed extends Signal {
constructor (fn, deps) {
super(fn(...deps));
for (const dep of deps) {
if (dep instanceof Signal)
dep.addEventListener('change', () => this.value = fn(...deps));
}
}
}
export const signal = _ => new Signal(_);
export const computed = (fn, deps) => new Computed(fn, deps);
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals1-use.js
================================================
const name = new Signal('Jane');
name.addEventListener('change', () => console.log(name.value));
name.value = 'John';
// Logs: John
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals1.js
================================================
class Signal extends EventTarget {
#value;
get value () { return this.#value; }
set value (value) {
if (this.#value === value) return;
this.#value = value;
this.dispatchEvent(new CustomEvent('change'));
}
constructor (value) {
super();
this.#value = value;
}
}
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals2-use.js
================================================
const name = signal('Jane');
name.effect(() => console.log(name.value));
// Logs: Jane
name.value = 'John';
// Logs: John
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals2.js
================================================
class Signal extends EventTarget {
#value;
get value () { return this.#value; }
set value (value) {
if (this.#value === value) return;
this.#value = value;
this.dispatchEvent(new CustomEvent('change'));
}
constructor (value) {
super();
this.#value = value;
}
effect(fn) {
fn();
this.addEventListener('change', fn);
return () => this.removeEventListener('change', fn);
}
valueOf () { return this.#value; }
toString () { return String(this.#value); }
}
const signal = _ => new Signal(_);
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals3-use.js
================================================
const name = signal('Jane');
const surname = signal('Doe');
const fullName = computed(() => `${name} ${surname}`, [name, surname]);
// Logs name every time it changes:
fullName.effect(() => console.log(fullName.value));
// -> Jane Doe
// Updating `name` updates `fullName`, which triggers the effect again:
name.value = 'John';
// -> John Doe
================================================
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals3.js
================================================
class Computed extends Signal {
constructor (fn, deps) {
super(fn(...deps));
for (const dep of deps) {
if (dep instanceof Signal)
dep.addEventListener('change', () => this.value = fn(...deps));
}
}
}
const computed = (fn, deps) => new Computed(fn, deps);
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/adder.svelte
================================================
<script>
export let a;
export let b;
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {a + b}</p>
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind.js
================================================
/**
* Render a template (element or string) to an html fragment
* while connecting it up to data using Vue-style shorthand binding syntax.
*
* - `@click="handleClick"` -> `click` event and `target.handleClick` method
* - `@change="mySignal"` -> `change` event will set `target.mySignal.value` to e.target.value
* - `@change="myProp"` -> `change` event will set `target.myProp` to e.target.value
* - `:textContent="text"` -> elem `textContent` property is set to `this.text` property once
* (also supports special shorthand `:text` for textContent and `:html` for innerHTML)
* - `:value="mySignal"` -> elem `value` attribute is bound to the signal `target.mySignal`'s value
*
* @param {*} template The template to be bound to an object
* @param {*} target The object being targeted for binding
* @returns fragment
*/
export const bind = (template, target) => {
if (!template.content) {
const text = template;
template = document.createElement('template');
template.innerHTML = text;
}
const fragment = template.content.cloneNode(true);
// iterate over all nodes in the fragment and bind them
// based on https://hawkticehurst.com/2024/05/bring-your-own-base-class/
const iterator = document.createNodeIterator(
fragment,
NodeFilter.SHOW_ELEMENT,
{
// Reject any node that is not an HTML element
acceptNode: (node) => {
if (!(node instanceof HTMLElement))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
}
);
let node;
while (node = iterator.nextNode()) {
if (!node) return;
const elem = node;
// list of all properties
// const properties = getAllProperties(elem);
// copy NamedNodeMap because we're about to change it
for (const attr of Array(...node.attributes)) {
// check for custom event listener attributes
if (attr.name.startsWith('@')) {
const event = attr.name.slice(1);
const property = attr.value;
let listener;
// if we're binding the event to a function, call it directly
if (typeof target[property] === 'function') {
listener = target[property].bind(target);
// if we're binding to a signal, set the signal's value
} else if (typeof target[property] === 'object' &&
typeof target[property].value !== 'undefined') {
listener = e => target[property].value = e.target.value;
// fallback: assume we're binding to a property, set the property's value
} else {
listener = e => target[property] = e.target.value;
}
elem.addEventListener(event, listener);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
// check for custom property/attribute binding attributes
} else if (attr.name.startsWith(':')) {
// extract the name and value of the attribute/property
let name = attr.name.slice(1);
const property = getPropertyForAttribute(name, target); // properties.values().find(k => k.toLowerCase() === name.toLowerCase());
const setter = property ?
() => elem[property] = target[attr.value] :
() => elem.setAttribute(name, target[attr.value]);
setter();
// if we're binding to a signal, listen to updates
target[attr.value].effect?.(setter);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
}
}
}
return fragment;
}
function getPropertyForAttribute(name, obj) {
switch (name.toLowerCase()) {
case 'text': case 'textcontent':
return 'textContent';
case 'html': case 'innerhtml':
return 'innerHTML';
default:
for (let prop of Object.getOwnPropertyNames(obj)) {
if (prop.toLowerCase() === name.toLowerCase()) {
return prop;
}
}
}
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind1.js
================================================
export const bind = (template) => {
const fragment = template.content.cloneNode(true);
// iterate over all nodes in the fragment
const iterator = document.createNodeIterator(
fragment,
NodeFilter.SHOW_ELEMENT,
{
// reject any node that is not an HTML element
acceptNode: (node) => {
if (!(node instanceof HTMLElement))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
}
);
let node;
while (node = iterator.nextNode()) {
if (!node) return;
const elem = node;
for (const attr of Array(...node.attributes)) {
// check for event binding directive
if (attr.name.startsWith('@')) {
// TODO: bind event ...
elem.removeAttributeNode(attr);
// check for property/attribute binding directive
} else if (attr.name.startsWith(':')) {
// TODO: bind data ...
elem.removeAttributeNode(attr);
}
}
}
return fragment;
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind2-partial.js
================================================
export const bind = (template, target) => {
if (!template.content) {
const text = template;
template = document.createElement('template');
template.innerHTML = text;
}
const fragment = template.content.cloneNode(true);
// ...
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind3-partial.js
================================================
// check for custom event listener attributes
if (attr.name.startsWith('@')) {
const event = attr.name.slice(1);
const property = attr.value;
let listener;
// if we're binding the event to a function, call it directly
if (typeof target[property] === 'function') {
listener = target[property].bind(target);
// if we're binding to a signal, set the signal's value
} else if (typeof target[property] === 'object' &&
typeof target[property].value !== 'undefined') {
listener = e => target[property].value = e.target.value;
// fallback: assume we're binding to a property, set the property's value
} else {
listener = e => target[property] = e.target.value;
}
elem.addEventListener(event, listener);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind4-partial.js
================================================
// ...
if (attr.name.startsWith(':')) {
// extract the name and value of the attribute/property
let name = attr.name.slice(1);
const property = getPropertyForAttribute(name, target);
const setter = property ?
() => elem[property] = target[attr.value] :
() => elem.setAttribute(name, target[attr.value]);
setter();
// if we're binding to a signal, listen to updates
if (target[attr.value]?.effect) {
target[attr.value].effect(setter);
// if we're binding to a property, listen to the target's updates
} else if (target.addEventListener) {
target.addEventListener('change', setter);
}
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
}
// ...
function getPropertyForAttribute(name, obj) {
switch (name.toLowerCase()) {
case 'text': case 'textcontent':
return 'textContent';
case 'html': case 'innerhtml':
return 'innerHTML';
default:
for (let prop of Object.getOwnPropertyNames(obj)) {
if (prop.toLowerCase() === name.toLowerCase()) {
return prop;
}
}
}
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/bind.js
================================================
export const bind = (template, target) => {
if (!template.content) {
const text = template;
template = document.createElement('template');
template.innerHTML = text;
}
const fragment = template.content.cloneNode(true);
// iterate over all nodes in the fragment
const iterator = document.createNodeIterator(
fragment,
NodeFilter.SHOW_ELEMENT,
{
// reject any node that is not an HTML element
acceptNode: (node) => {
if (!(node instanceof HTMLElement))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
}
);
let node;
while (node = iterator.nextNode()) {
if (!node) return;
const elem = node;
for (const attr of Array(...node.attributes)) {
// check for event binding directive
if (attr.name.startsWith('@')) {
// check for custom event listener attributes
if (attr.name.startsWith('@')) {
const event = attr.name.slice(1);
const property = attr.value;
let listener;
// if we're binding the event to a function, call it directly
if (typeof target[property] === 'function') {
listener = target[property].bind(target);
// if we're binding to a signal, set the signal's value
} else if (typeof target[property] === 'object' &&
typeof target[property].value !== 'undefined') {
listener = e => target[property].value = e.target.value;
// fallback: assume we're binding to a property, set the property's value
} else {
listener = e => target[property] = e.target.value;
}
elem.addEventListener(event, listener);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
}
// check for property/attribute binding directive
} else if (attr.name.startsWith(':')) {
// TODO: bind data ...
elem.removeAttributeNode(attr);
}
}
}
return fragment;
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/example.html
================================================
<!doctype html>
<head>
<title>Binding example</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
</head>
<body>
<style>
:root { margin: 0.5em; font-family: system-ui, sans-serif; }
div { margin: 0.5em 0; }
</style>
<script type="module" src="example.js"></script>
<x-example></x-example>
</body>
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/example.js
================================================
import { bind } from './bind.js';
import { signal } from './signals.js';
customElements.define('x-example', class Example extends HTMLElement {
set a(value) {
this.setAttribute('a', value);
this.querySelector('label[for=a] span').textContent = value;
}
set b(value) {
this.setAttribute('b', value);
this.querySelector('label[for=b] span').textContent = value;
}
c = signal('');
connectedCallback() {
this.append(bind(`
<div>
<input id="a" type="number" @input="onInputA">
<label for="a">A = <span></span></label>
</div>
<div>
<input id="b" type="number" @input="b">
<label for="b">B = <span></span></label>
</div>
<div>
<input id="c" type="number" @input="c">
<label for="c">C = <span></span></label>
</div>
<button @click="onClick">Add</button>
<div>Result: <span id="result"></span></div>
`, this));
this.c.effect(() =>
this.querySelector('label[for=c] span').textContent = this.c);
}
onInputA (e) {
this.a = e.target.value;
}
onClick() {
this.querySelector('#result').textContent =
+this.getAttribute('a') + +this.getAttribute('b') + +this.c;
}
});
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/signals.js
================================================
export class Signal extends EventTarget {
#value;
get value () { return this.#value; }
set value (value) {
if (this.#value === value) return;
this.#value = value;
this.dispatchEvent(new CustomEvent('change'));
}
constructor (value) {
super();
this.#value = value;
}
effect(fn) {
fn();
this.addEventListener('change', fn);
return () => this.removeEventListener('change', fn);
}
valueOf () { return this.#value; }
toString () { return String(this.#value); }
}
export class Computed extends Signal {
constructor (fn, deps) {
super(fn(...deps));
for (const dep of deps) {
if (dep instanceof Signal)
dep.addEventListener('change', () => this.value = fn(...deps));
}
}
}
export const signal = _ => new Signal(_);
export const computed = (fn, deps) => new Computed(fn, deps);
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/adder.js
================================================
import { bind } from './bind.js';
import { signal, computed } from './signals.js';
import { html } from './html.js';
customElements.define('x-adder', class Adder extends HTMLElement {
a = signal();
b = signal();
result = computed(() =>
html`${+this.a} + ${+this.b} = ${+this.a + +this.b}`, [this.a, this.b]);
connectedCallback() {
this.a.value ??= this.getAttribute('a') || 0;
this.b.value ??= this.getAttribute('b') || 0;
this.append(bind(html`
<input type="number" :value="a" @input="a" />
<input type="number" :value="b" @input="b" />
<p :html="result"></p>
`, this));
}
});
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/bind.js
================================================
/**
* Render a template (element or string) to an html fragment
* while connecting it up to data using Vue-style shorthand binding syntax.
*
* - `@click="handleClick"` -> `click` event and `target.handleClick` method
* - `@change="mySignal"` -> `change` event will set `target.mySignal.value` to e.target.value
* - `@change="myProp"` -> `change` event will set `target.myProp` to e.target.value
* - `:textContent="text"` -> elem `textContent` property is set to `this.text` property once
* (also supports special shorthand `:text` for textContent and `:html` for innerHTML)
* - `:value="mySignal"` -> elem `value` attribute is bound to the signal `target.mySignal`'s value
*
* @param {*} template The template to be bound to an object
* @param {*} target The object being targeted for binding
* @returns fragment
* @license Unlicense
*/
export const bind = (template, target) => {
if (!template.content) {
const text = template;
template = document.createElement('template');
template.innerHTML = text;
}
const fragment = template.content.cloneNode(true);
// iterate over all nodes in the fragment and bind them
// based on https://hawkticehurst.com/2024/05/bring-your-own-base-class/
const iterator = document.createNodeIterator(
fragment,
NodeFilter.SHOW_ELEMENT,
{
// Reject any node that is not an HTML element
acceptNode: (node) => {
if (!(node instanceof HTMLElement))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
}
);
let node;
while (node = iterator.nextNode()) {
if (!node) return;
const elem = node;
for (const attr of Array(...node.attributes)) {
// check for custom event listener attributes
if (attr.name.startsWith('@')) {
const event = attr.name.slice(1);
const property = attr.value;
let listener;
// if we're binding the event to a function, call it directly
if (typeof target[property] === 'function') {
listener = target[property].bind(target);
// if we're binding to a signal, set the signal's value
} else if (typeof target[property] === 'object' &&
typeof target[property].value !== 'undefined') {
listener = e => target[property].value = e.target.value;
// fallback: assume we're binding to a property, set the property's value
} else {
listener = e => target[property] = e.target.value;
}
elem.addEventListener(event, listener);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
// check for custom property/attribute binding attributes
} else if (attr.name.startsWith(':')) {
// extract the name and value of the attribute/property
let name = attr.name.slice(1);
const property = getPropertyForAttribute(name, target);
const setter = property ?
() => elem[property] = target[attr.value] :
() => elem.setAttribute(name, target[attr.value]);
setter();
// if we're binding to a signal, listen to updates
target[attr.value].effect?.(setter);
// remove (non-standard) attribute from element
elem.removeAttributeNode(attr);
}
}
}
return fragment;
}
function getPropertyForAttribute(name, obj) {
switch (name.toLowerCase()) {
case 'text': case 'textcontent':
return 'textContent';
case 'html': case 'innerhtml':
return 'innerHTML';
default:
for (let prop of Object.getOwnPropertyNames(obj)) {
if (prop.toLowerCase() === name.toLowerCase()) {
return prop;
}
}
}
}
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/example.html
================================================
<!doctype html>
<head>
<title>Binding example</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
</head>
<body>
<style>
:root { margin: 0.5em; font-family: system-ui, sans-serif; }
div { margin: 0.5em 0; }
</style>
<script type="module" src="adder.js"></script>
<x-adder a="1" b="2"></x-adder>
</body>
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/html.js
================================================
class Html extends String { }
/**
* tag a string as html not to be encoded
* @param {string} str
* @returns {string}
*/
export const htmlRaw = str => new Html(str);
/**
* entity encode a string as html
* @param {*} value The value to encode
* @returns {string}
*/
export const htmlEncode = (value) => {
// avoid double-encoding the same string
if (value instanceof Html) {
return value;
} else {
// https://stackoverflow.com/a/57448862/20980
return htmlRaw(
String(value).replace(/[&<>'"]/g,
tag => ({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag]))
);
}
}
/**
* html tagged template literal, auto-encodes entities
*/
export const html = (strings, ...values) =>
htmlRaw(String.raw({ raw: strings }, ...values.map(htmlEncode)));
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/signals.js
================================================
export class Signal extends EventTarget {
#value;
get value () { return this.#value; }
set value (value) {
if (this.#value === value) return;
this.#value = value;
this.dispatchEvent(new CustomEvent('change'));
}
constructor (value) {
super();
this.#value = value;
}
effect(fn) {
fn();
this.addEventListener('change', fn);
return () => this.removeEventListener('change', fn);
}
valueOf () { return this.#value; }
toString () { return String(this.#value); }
}
export class Computed extends Signal {
constructor (fn, deps) {
super(fn(...deps));
for (const dep of deps) {
if (dep instanceof Signal)
dep.addEventListener('change', () => this.value = fn(...deps));
}
}
}
export const signal = _ => new Signal(_);
export const computed = (fn, deps) => new Computed(fn, deps);
================================================
FILE: public/blog/articles/2024-09-03-unix-philosophy/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>A unix philosophy for web development</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="Maybe all web components need to be a light-weight framework is the right set of helper functions.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-09-03">
<img src="image.webp" alt="A pattern of connected spheres" loading="lazy" />
<h2>A unix philosophy for web development</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
Web components have their malcontents. While frameworks have done their best to provide
a place for web components to fit into their architecture, the suit never fits quite right,
and framework authors have not been shy about expressing their disappointment.
Here's Ryan Carniato of SolidJS explaining what's wrong with web components:
</p>
<blockquote>
The collection of standards (Custom Elements, HTML Templates, Shadow DOM, and formerly HTML Imports)
put together to form Web Components on the surface seem like they could be used to replace
your favourite library or framework. But they are not an advanced templating solution.
They don't improve your ability to render or update the DOM.
They don't manage higher-level concerns for you like state management.
</blockquote>
<cite><a href="https://dev.to/ryansolid/maybe-web-components-are-not-the-future-hfh">Ryan Carniato</a></cite>
<p>
While this criticism is true, perhaps it's besides the point.
Maybe web components were never meant to solve those problems anyway.
Maybe there are ways to solve those problems in a way that dovetails with web components as they exist.
In the main <a href="../../../pages/components.html">components tutorial</a> I've already explained what they <em>can</em> do,
now let's see what can be done about the things that they <em>can't</em> do.
</p>
<h3>The Unix Philosophy</h3>
<p>
The Unix operating system carries with it a culture and philosophy of system design,
which carries over to the command lines of today's Unix-like systems like Linux and MacOS.
This philosophy can be summarized as follows:
</p>
<ul>
<li>Write programs that do one thing and do it well.</li>
<li>Write programs to work together.</li>
<li>Write programs to handle text streams, because that is a universal interface.</li>
</ul>
<p>
What if we look at the various technologies that comprise web components as just programs,
part of a Unix-like system of web development that we collectively call the browser platform?
In that system we can do better than text and use the DOM as the universal interface between programs,
and we can extend the system with a set of single purpose independent "programs" (functions)
that fully embrace the DOM by augmenting it instead of replacing it.
</p>
<p>
In a sense this is the most old-school way of building web projects, the one people who "don't know any better"
automatically gravitate to. What us old-school web developers did before Vue and Solid and Svelte, before Angular and React,
before Knockout and Ember and Backbone, before even jQuery, was have a bunch of functions in <code>utilities.js</code>
that we copied along from project to project.
But, you know, sometimes old things can become new again.
</p>
<p>
In previous posts I've already covered a <code>html()</code> function for <a href="../2024-08-25-vanilla-entity-encoding/">vanilla entity encoding</a>,
and a <code>signal()</code> function that provides a <a href="../2024-08-30-poor-mans-signals/">tiny signals</a> implementation
that can serve as a lightweight system for state management.
That still leaves a missing link between the state managed by the signals and the DOM that is rendered from safely entity-encoded HTML.
What we need is a <code>bind()</code> function that can bind data to DOM elements and bind DOM events back to data.
</p>
<h3>Finding inspiration</h3>
<p>
In order to bind a template to data, we need a way of describing that behavior in the HTML markup.
Well-trodden paths are often the best starting place to look for inspiration. I like <a href="https://vuejs.org/guide/essentials/template-syntax.html">Vue's template syntax</a>,
because it is valid HTML but just augmented, and because it is proven. Vue's templates only pretend to be HTML
because they're actually compiled to JavaScript behind the scenes, but let's start there as an API.
This is what it looks like:
</p>
<dl>
<dt><code><img :src="imageSrc" /></code></dt>
<dd>Bind <em>src</em> to track the value of the <em>imageSrc</em> property of the current component.
Vue is smart enough to set a property if one exists, and falls back to setting an attribute otherwise.
(If that confuses you, read about <a href="https://javascript.info/dom-attributes-and-properties">attributes and properties</a> first.)</dd>
<dt><code><button @click="doThis"></button></code></dt>
<dd>Bind the <em>click</em> event to the <em>doThis</em> method of the current component.</dd>
</dl>
<p>
By chance I came across this article about <a href="https://hawkticehurst.com/2024/05/bring-your-own-base-class/">making a web component base class</a>.
In the section <em>Declarative interactivity</em> the author shows a way to do the Vue-like event binding syntax
on a vanilla web component. This is what inspired me to develop the concept into a generic binding function and write this article.
</p>
<h3>Just an iterator</h3>
<p>
The heart of the binding function is an HTML fragment iterator.
After all, before we can bind attributes we need to first find the ones that have binding directives.
</p>
<x-code-viewer src="bind1.js" name=""></x-code-viewer>
<p>
This code will take an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">HTML template element</a>,
clone it to a <a href="https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment">document fragment</a>,
and then iterate over all the nodes in the fragment, discovering their attributes.
Then for each attribute a check is made to see if it's a binding directive (@ or :).
The node is then bound to data according to the directive attribute (shown here as TODO's),
and the attribute is removed from the node. At the end the bound fragment is returned for inserting into the DOM.
</p>
<p>
The benefit of using a fragment is that it is disconnected from the main DOM, while still offering all of the DOM API's.
That means we can easily create a node iterator to walk over it and discover all the attributes
with binding directives, modify those nodes and attributes in-place, and still be sure we're not causing
DOM updates in the main page until the fragment is inserted there. This makes the bind function very fast.
</p>
<p>
If you're thinking "woah dude, that's a lot of code and a lot of technobabble, I ain't reading all that,"
then please, I implore you to read through the code line by line, and you'll see it will all make sense.
</p>
<p>
Of course, we also need to have something to bind <em>to</em>, so we need to add a second parameter.
At the same time, it would be nice to just be able to pass in a string and have it auto-converted into a template.
The beginning of our bind function then ends up looking like this:
</p>
<x-code-viewer src="bind2-partial.js" name=""></x-code-viewer>
<p>That just leaves us the TODO's. We can make those as simple or complicated as we want. I'll pick a middle ground.</p>
<h3>Binding to events</h3>
<p>This 20 line handler binds events to methods, signals or properties:</p>
<x-code-viewer src="bind3-partial.js" name=""></x-code-viewer>
<p>That probably doesn't explain much, so let me give an example of what this enables:</p>
<iframe src="example-bind3/example.html" title="Binding to events example" height="200"></iframe>
<x-code-viewer src="./example-bind3/example.js" name=""></x-code-viewer>
<ul>
<li><code>input#a</code>'s input event is handled by calling the <code>onClickA()</code> method.</li>
<li><code>input#b</code>'s input event is handled by assigning <code>e.target.value</code> to the <code>b</code> property.</li>
<li><code>input#c</code>'s input event is handled by setting the value of the <code>c</code> signal.</li>
</ul>
<p>
If you're not familiar with the <code>signal()</code> function, check out the <a href="../2024-08-30-poor-mans-signals/">tiny signals</a>
implementation in the previous post. For now you can also just roll with it.
</p>
<p>Not a bad result for 20 lines of code.</p>
<h3>Binding to data</h3>
<p>
Having established the pattern for events that automatically update properties,
we now reverse the polarity to make data values automatically set element properties or attributes.
</p>
<x-code-viewer src="bind4-partial.js" name=""></x-code-viewer>
<p>
The <code>getPropertyForAttribute</code> function is necessary because the attributes that contain the directives
will have names that are case-insensitive, and these must be mapped to property names that are case-sensitive.
Also, the <code>:text</code> and <code>:html</code> shorthand notations replace the role of <code>v-text</code>
and <code>v-html</code> in Vue's template syntax.
</p>
<p>
When the value of the target's observed property changes, we need to update the bound element's property or attribute.
This means a triggering <code>'change'</code> event is needed that is then subscribed to.
A framework's templating system will compare state across time, and detect the changed values automatically.
Lacking such a system we need a light-weight alternative.
</p>
<p>
When the property being bound to is a signal, this code registers an effect on the signal.
When the property is just a value, it registers an event listener on the target object,
making it the responsibility of that target object to dispatch the <code>'change'</code> event when values change.
This approach isn't going to get many points for style, but it does work.
</p>
<p>
Check out the <a href="example-combined/bind.js">completed bind.js</a> code.
</p>
<h3>Bringing the band together</h3>
<p>
In the article <a href="https://dev.to/richharris/why-i-don-t-use-web-components-2cia">Why I don't use web components</a>
Svelte's Rich Harris lays out the case against web components. He demonstrates how this simple 9 line Svelte component
<code><Adder a={1} b={2}/></code> becomes an incredible verbose 59 line monstrosity when ported to a vanilla web component.
</p>
<x-code-viewer src="adder.svelte"></x-code-viewer>
<p>
Now that we have assembled our three helper functions <code>html()</code>, <code>signal()</code> and <code>bind()</code>
on top of the web components baseline, at a total budget of around 150 lines of code, how close can we get for a web component <code><x-adder a="1" b="2"></x-adder></code>?
</p>
<x-code-viewer src="example-combined/adder.js"></x-code-viewer>
<iframe src="example-combined/example.html" title="combined example"></iframe>
<p>
To be fair, that's still twice the lines of code, but it describes clearly what it does, and really that is all you need.
And I'm just shooting in the wind here, trying stuff out.
Somewhere out there could be a minimal set of functions that transforms web components into something resembling a framework,
and the idea excites me! Who knows, maybe in a few years the web community will return to writing projects in
vanilla web code, dragging along the modern equivalent of <code>utilities.js</code> from project to project...
</p>
<br>
<p><em>What do you think?</em></p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113075285264597399"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-06-how-fast-are-web-components/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>How fast are web components?</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="Benchmarking the relative performance of different web component techniques.">
<link rel="stylesheet" href="../../index.css">
<style>
table th:nth-of-type(1) {
text-align: left;
padding-right: 1em;
}
table td:nth-of-type(2) {
text-align: right;
}
</style>
</head>
<body>
<blog-header published="2024-09-06" updated="2024-09-15">
<img src="image.webp" alt="A snail and a hare in a race" loading="lazy" />
<h2>How fast are web components?</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<aside>
<h3>Author's note</h3>
<p>
This article initially had somewhat different results and conclusions,
but deficiencies in the original benchmark were pointed out and addressed.
Where the conclusions were changed from the original article, this is pointed out in the text.
</p>
</aside>
<p>
It is often said that web components are slow.
This was also my experience when I first tried building web components a few years ago.
At the time I was using the Stencil framework, because I didn't feel confident that they could be built well without a framework.
Performance when putting hundreds of them on a page was so bad that I ended up going back to React.
</p>
<p>
But as I've gotten deeper into vanilla web development I started to realize that maybe I was just using it wrong.
Perhaps web components can be fast, if built in a light-weight way.
This article is an attempt to settle the question "How fast are web components?".
</p>
<h3>The lay of the land</h3>
<p>
What kinds of questions did I want answered?
</p>
<ul>
<li>How many web components can you render on the page in a millisecond?</li>
<li>Does the technique used to built the web component matter, which technique is fastest?</li>
<li>How do web component frameworks compare? I used Lit as the framework of choice as it is well-respected.</li>
<li>How does React compare?</li>
<li>What happens when you combine React with web components?</li>
</ul>
<p>
To figure out the answer I made a benchmark as a vanilla web page (of course),
that renders thousands of very simple components containing only <code><span>.</span></code>
and measured the elapsed time. This benchmark was then run on multiple devices and multiple browsers
to figure out performance characteristics. The ultimate goal of this test is to figure out the absolute best performance
that can be extracted from the most minimal web component.
</p>
<p>To get a performance range I used two devices for testing:</p>
<ul>
<li>A <em>Macbook Air M1</em> running MacOS, to stand in as the "fast" device, comparable to a new high end iPhone.</li>
<li>An <em>Asus Chi T300 Core M</em> from 2015 running Linux Mint Cinnamon, to stand in as the "slow" device, comparable to an older low end Android.</li>
</ul>
<p>Between these devices is a 7x CPU performance gap.</p>
<h3>The test</h3>
<p>
The test is simple: render thousands of components using a specific technique,
call <code>requestAnimationFrame()</code> repeatedly until they actually render,
then measure elapsed time. This produces a <em>components per millisecond</em> number.
</p>
<p>The techniques being compared:</p>
<ul>
<li><strong>innerHTML:</strong> each web component renders its content by assigning to <code>this.innerHTML</code></li>
<li><strong>append:</strong> each web component creates the span using <code>document.createElement</code> and then appends it to itself</li>
<li><strong>append (buffered):</strong> same as the append method, except all web components are first buffered to a document fragment which is then appended to the DOM</li>
<li><strong>shadow + innerHTML:</strong> the same as innerHTML, except each component has a shadow DOM</li>
<li><strong>shadow + append:</strong> the same as append, except each component has a shadow DOM</li>
<li><strong>template + append:</strong> each web component renders its content by cloning a template and appending it</li>
<li><strong>textcontent:</strong> each web component directly sets its textContent property, instead of adding a span (making the component itself be the span)
<li><strong>direct:</strong> appends spans instead of custom elements, to be able to measure custom element overhead</li>
<li><strong>lit:</strong> each web component is rendered using the lit framework, in the way that its documentation recommends</li>
<li><strong>react pure:</strong> rendering in React as a standard React component, to have a baseline for comparison to mainstream web development</li>
<li><strong>react + wc:</strong> each React component wraps the append-style web component</li>
<li><strong>(norender):</strong> same as other strategies, except the component is only created but not added to the DOM, to separate out component construction cost</li>
</ul>
<p>
This test was run on M1 in Brave, Chrome, Edge, Firefox and Safari. And on Chi in Chrome and Firefox.
It was run for 10 iterations and a geometric mean was taken of the results.
</p>
<h3>The results</h3>
<p>First, let's compare techniques. The number here is components per millisecond, so <strong>higher is better</strong>.</p>
<p><strong>Author's note:</strong> the numbers from the previous version of this article are <del>crossed out</del>.</p>
<table>
<thead>
<tr><th colspan="2">Chrome on M1</th></tr>
<tr><th>technique</th><th>components/ms</th></tr>
</thead>
<tbody>
<tr><td>innerHTML</td><td><del>143</del> 135</td></tr>
<tr><td>append</td><td><del>233</del> 239</td></tr>
<tr><td>append (buffered)</td><td><del>228</del> 239</td></tr>
<tr><td>shadow + innerHTML</td><td><del>132</del> 127</td></tr>
<tr><td>shadow + append</td><td><del>183</del> 203</td></tr>
<tr><td>template + append</td><td><del>181</del> 198</td></tr>
<tr><td>textcontent</td><td>345</td></tr>
<tr><td>direct</td><td>461</td></tr>
<tr><td>lit</td><td><del>133</del> 137</td></tr>
<tr><td>react pure</td><td><del>275</del> 338</td></tr>
<tr><td>react + wc</td><td><del>172</del> 212</td></tr>
<tr><td>append (norender)</td><td>1393</td></tr>
<tr><td>shadow (norender)</td><td>814</td></tr>
<tr><td>direct (norender)</td><td>4277</td></tr>
<tr><td>lit (norender)</td><td>880</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th colspan="2">Chrome on Chi, best of three</th></tr>
<tr><th>technique</th><th>components/ms</th></tr>
</thead>
<tbody>
<tr><td>innerHTML</td><td><del>25</del> 29</td></tr>
<tr><td>append</td><td><del>55</del> 55</td></tr>
<tr><td>append (buffered)</td><td><del>56</del> 59</td></tr>
<tr><td>shadow + innerHTML</td><td><del>24</del> 26</td></tr>
<tr><td>shadow + append</td><td><del>36</del> 47</td></tr>
<tr><td>template + append</td><td><del>45</del> 46</td></tr>
<tr><td>textcontent</td><td>81</td></tr>
<tr><td>direct</td><td>116</td></tr>
<tr><td>lit</td><td><del>30</del> 33</td></tr>
<tr><td>react pure</td><td><del>77</del> 87</td></tr>
<tr><td>react + wc</td><td><del>45</del> 52</td></tr>
<tr><td>append (norender)</td><td>434</td></tr>
<tr><td>shadow (norender)</td><td>231</td></tr>
<tr><td>direct (norender)</td><td>1290</td></tr>
<tr><td>lit (norender)</td><td>239</td></tr>
</tbody>
</table>
<p>
One relief right off the bat is that even the slowest implementation on the slow device renders 100.000 components in 4 seconds.
React is roughly in the same performance class as well-written web components.
That means for a typical web app performance is not a reason to avoid web components.
</p>
<aside>
<h3>Author's note</h3>
<p>
The previous version of this article said React was faster than web components,
but this only the case if we make the web components render a span. Unlike a React component a web component
is itself part of the DOM, and so is itself the equivalent of a span. The <em>textcontent</em> strategy exploits this advantage
to functionally do the same as the React code, and it matches its performance.
</p>
</aside>
<p>
As far as web component technique goes, the performance delta between the fastest and the slowest technique is around 2x,
so again for a typical web app that difference will not matter. Things that slow down web components
are shadow DOM and innerHTML. Appending directly created elements or cloned templates and avoiding shadow DOM is the right strategy
for a well-performing web component that needs to end up on the page thousands of times.
</p>
<p>
On the slow device the Lit framework is a weak performer, probably due to its use of shadow DOM and JS-heavy approaches.
Meanwhile, pure React is the best performer, because while it does more work in creating the virtual DOM and diffing it to the real DOM,
it benefits from not having to initialize the web component class instances.
Consequently, when wrapping web components inside React components we see React's performance advantage disappear, and that it adds a performance tax.
In the grand scheme of things however, the differences between React and optimized web components remains small.
</p>
<p>
The fast device is up to 5x faster than the slow device in Chrome, depending on the technique used,
so it is really worth testing applications on slow devices to get an idea of the range of performance.
</p>
<p>
Next, let's compare browsers:
</p>
<table>
<thead>
<tr><th colspan="2">M1, append, best of three</th></tr>
<tr><th>browser</th><th>components/ms</th></tr>
</thead>
<tbody>
<tr><td>Brave</td><td><del>146</del> 145</td></tr>
<tr><td>Chrome</td><td><del>233</del> 239</td></tr>
<tr><td>Edge</td><td><del>224</del> 237</td></tr>
<tr><td>Firefox</td><td><del>232</del> 299</td></tr>
<tr><td>Safari</td><td><del>260</del> 239</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th colspan="2">Chi, append, best of three</th></tr>
<tr><th>browser</th><th>components/ms</th></tr>
</thead>
<tbody>
<tr><td>Chrome</td><td><del>55</del> 55</td></tr>
<tr><td>Firefox</td><td><del>180</del> 77</td></tr>
</tbody>
</table>
<p>
Brave is really slow, probably because of its built-in ad blocking. Ad blocking extensions also slow down the other browsers by a lot.
Safari, Chrome and Edge end up in roughly the same performance bucket. Firefox is the best performer overall.
Using the "wrong" browser can halve the performance of a machine.
</p>
<p>
<strong>Author's note:</strong>
due to a measurement error in measuring elapsed time, the previous version of this article had Safari as fastest and Firefox as middle of the pack.
</p>
<p>
There is a large performance gap when you compare the slowest technique on the slowest browser on the slowest device,
with its fastest opposite combo. Specifically, there is a 16x performance gap:
</p>
<ul>
<li>textContent, Firefox on M1: 430 components/ms</li>
<li>Shadow DOM + innerHTML, Chrome on Chi: 26 components/ms</li>
</ul>
<p>
That means it becomes worthwhile to carefully consider technique when having to support a wide range of browsers and devices,
because a bad combination may lead to a meaningfully degraded user experience.
And of course, you should always test your web app on a slow device to make sure it still works ok.
</p>
<h3>Bottom line</h3>
<p>
I feel confident now that web components can be fast enough for almost all use cases where someone might consider React instead.
</p>
<p>
However, it does matter how they are built. Shadow DOM should not be used for smaller often used web components,
and the contents of those smaller components should be built using append operations instead of innerHTML.
The use of web component frameworks might impact their performance significantly,
and given how easy it is to write vanilla web components I personally don't see the point behind Lit or Stencil. YMMV.
</p>
<p>
The full benchmark code and results can be <a href="https://github.com/jsebrech/vanilla-benchmarks">found on Github</a>.
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113092382700063677"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/error-boundary-partial.html
================================================
<x-error-boundary>
<p slot="error">Something went wrong</p>
<x-suspense>
<p slot="fallback">Loading...</p>
<x-lazy><x-hello-world></x-hello-world></x-lazy>
</x-suspense>
</x-error-boundary>
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/error-boundary.js
================================================
export class ErrorBoundary extends HTMLElement {
static showError(sender, error) {
if (!error) throw new Error('ErrorBoundary.showError: expected two arguments but got one');
const boundary = sender.closest('x-error-boundary');
if (boundary) {
boundary.error = error;
} else {
console.error('unable to find x-error-boundary to show error');
console.error(error);
}
}
#error;
#errorSlot;
#contentSlot;
get error() {
return this.#error;
}
set error(error) {
if (!this.#errorSlot) return;
this.#error = error;
this.#errorSlot.style.display = error ? 'contents' : 'none';
this.#contentSlot.style.display = !error ? 'contents' : 'none';
if (error) {
this.#errorSlot.assignedElements().forEach(element => {
if (Object.hasOwn(element, 'error')) {
element.error = error;
} else {
element.setAttribute('error', error?.message || error);
}
});
this.dispatchEvent(new CustomEvent('error', { detail: error }));
}
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#errorSlot = document.createElement('slot');
this.#errorSlot.style.display = 'none';
this.#errorSlot.name = 'error';
// default error message
this.#errorSlot.textContent = 'Something went wrong.';
this.#contentSlot = document.createElement('slot');
this.shadowRoot.append(this.#errorSlot, this.#contentSlot);
}
reset() {
this.error = null;
}
connectedCallback() {
this.style.display = 'contents';
}
}
customElements.define('x-error-boundary', ErrorBoundary);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/error-boundary.js
================================================
/**
* A vanilla version of react-error-boundary ( https://github.com/bvaughn/react-error-boundary )
*
* Usage:
*
* <x-error-boundary>
* <p slot="error">Something went wrong</p>
* <x-suspense>
* Shows the error if loading fails:
* <x-lazy><x-load-on-demand></x-load-on-demand></x-lazy>
* </x-suspense>
* </x-error-boundary>
*
* @license MIT
*/
export class ErrorBoundary extends HTMLElement {
/**
* Find the nearest error boundary to the sender element and make it show an error
* @param {*} sender The element that sends the error
* @param {*} error Error or string, the error to show
*/
static showError(sender, error) {
if (!error) throw new Error('ErrorBoundary.showError: expected two arguments but got one');
const boundary = sender.closest('x-error-boundary');
if (boundary) {
boundary.error = error;
} else {
console.error('unable to find x-error-boundary to show error');
console.error(error);
}
}
#error;
#errorSlot;
#contentSlot;
get error() {
return this.#error;
}
set error(error) {
if (!this.#errorSlot) return;
this.#error = error;
this.#errorSlot.style.display = error ? 'contents' : 'none';
this.#contentSlot.style.display = !error ? 'contents' : 'none';
if (error) {
this.#errorSlot.assignedElements().forEach(element => {
if (Object.hasOwn(element, 'error')) {
element.error = error;
} else {
element.setAttribute('error', error?.message || error);
}
});
this.dispatchEvent(new CustomEvent('error', { detail: error }));
}
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#errorSlot = document.createElement('slot');
this.#errorSlot.style.display = 'none';
this.#errorSlot.name = 'error';
// default error message
this.#errorSlot.textContent = 'Something went wrong.';
this.#contentSlot = document.createElement('slot');
this.shadowRoot.append(this.#errorSlot, this.#contentSlot);
}
reset() {
this.error = null;
}
connectedCallback() {
this.style.display = 'contents';
}
}
export const registerErrorBoundary = () => customElements.define('x-error-boundary', ErrorBoundary);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/error-message.js
================================================
class ErrorMessage extends HTMLElement {
connectedCallback() {
this.update();
}
static get observedAttributes() {
return ['error'];
}
attributeChangedCallback() {
this.update();
}
update() {
const errorMsg = this.getAttribute('error') || '';
this.textContent = errorMsg;
}
}
export const registerErrorMessage = () => customElements.define('x-error-message', ErrorMessage);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/hello-world.js
================================================
import { Suspense } from '../suspense.js';
import { later } from './later.js';
class HelloWorldComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<p>Hello world!</p>
<button id="load">Load</button>
<button id="error">Load with error</button>
`;
const btnLoad = this.querySelector('button#load');
btnLoad.onclick = () => {
// simulate loading of data
Suspense.waitFor(this, later(1000));
};
const btnError = this.querySelector('button#error');
btnError.onclick = () => {
// simulate loading of data that ends in an error
Suspense.waitFor(this, later(1000).then(() => { throw new Error('An error, as expected.'); }));
}
}
}
export default function register() {
customElements.define('x-hello-world', HelloWorldComponent);
}
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/later.js
================================================
export function later(delay) {
return new Promise(function(resolve) {
setTimeout(resolve, delay);
});
}
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/lazy.js
================================================
import { Suspense } from './suspense.js';
/**
* A vanilla version of React's lazy() function
* inspired by https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/
*
* Usage: <x-lazy><x-load-on-demand></x-load-on-demand></x-lazy>
*
* Will load default function from ./components/<load-on-demand>/<load-on-demand>.js and execute it.
* Only direct children are lazy-loaded, and only on initial DOM insert.
*
* Pass the root attribute to modify the path to load relative to the current document.
* <x-lazy root="./submodule/components/"><x-load-on-demand></x-load-on-demand></x-lazy>
*
* Put the lazy-path attribute on a custom element to specify the path to the JS file to load.
*
* @license MIT
*/
class Lazy extends HTMLElement {
connectedCallback() {
this.style.display = 'contents';
this.#loadLazy();
}
/**
* Find direct child custom elements that need loading, then load them
*/
#loadLazy() {
const elements =
[...this.children].filter(_ => _.localName.includes('-'));
const unregistered =
elements.filter(_ => !customElements.get(_.localName));
if (unregistered.length) {
Suspense.waitFor(this,
...unregistered.map(_ => this.#loadElement(_))
);
}
}
/**
* Load a custom element
* @param {*} element
* @returns {Promise} a promise that settles when loading completes or fails
*/
#loadElement(element) {
// does the element advertise its own path?
let url = element.getAttribute('lazy-path');
if (!url) {
// strip leading x- off the name
const cleanName = element.localName.replace(/^x-/, '').toLowerCase();
// root directory to load from, relative to current document
const rootDir = this.getAttribute('root') || './components/';
// assume component is in its own folder
url = `${rootDir}${cleanName}/${cleanName}.js`;
}
// dynamically import, then register if not yet registered
return import(new URL(url, document.location)).then(module =>
!customElements.get(element.localName) && module && module.default());
}
}
export const registerLazy = () => customElements.define('x-lazy', Lazy);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/suspense.js
================================================
import { ErrorBoundary } from './error-boundary.js';
/**
* A vanilla version of React's Suspense
*
* Usage:
*
* <x-suspense>
* <p slot="fallback">Loading...</p>
* <p>
* While it loads it shows the fallback:
* <x-lazy><x-load-on-demand></x-load-on-demand></x-lazy>
* </p>
* </x-suspense>
*
* @license MIT
*/
export class Suspense extends HTMLElement {
/**
* Find the nearest suspense to the sender element and make it wait for the promises to complete.
* @param {*} sender The element that sends the promise
* @param {...Promise} promises
*/
static waitFor(sender, ...promises) {
const suspense = sender.closest('x-suspense');
if (suspense) suspense.addPromises(...promises);
}
#fallbackSlot;
#contentSlot;
#waitingForPromise;
set #loading(isLoading) {
if (!this.#fallbackSlot) return;
this.#fallbackSlot.style.display = isLoading ? 'contents' : 'none';
this.#contentSlot.style.display = !isLoading ? 'contents' : 'none';
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#fallbackSlot = document.createElement('slot');
this.#fallbackSlot.style.display = 'none';
this.#fallbackSlot.name = 'fallback';
this.#contentSlot = document.createElement('slot');
this.shadowRoot.append(this.#fallbackSlot, this.#contentSlot);
}
connectedCallback() {
this.style.display = 'contents';
}
/**
* Wait for one or more promises to settle, showing fallback content
* @param {...Promise} promises
*/
addPromises(...promises) {
if (!promises.length) return;
this.#loading = true;
// combine into previous promises if there are any
const newPromise = this.#waitingForPromise =
Promise.allSettled([...promises, this.#waitingForPromise]);
// wait for all promises to complete
newPromise.then(settled => {
// if no more promises were added, we're done
if (newPromise === this.#waitingForPromise) {
this.#loading = false;
// if a promise failed, show an error
const failed = settled.find(_ => _.status === 'rejected');
if (failed) ErrorBoundary.showError(this, failed.reason);
}
});
}
}
export const registerSuspense = () => customElements.define('x-suspense', Suspense);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Lazy, Suspense and Error Boundary example</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<style>
body { font-family: system-ui, sans-serif; }
div { min-height: 2em; border: 1px dotted black; margin: 1em 0; padding: 1em; }
button { font-size: 90%; }
</style>
</head>
<body>
<x-demo></x-demo>
<script type="module" src="index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/index.js
================================================
import { registerLazy } from './components/lazy.js';
import { registerSuspense } from './components/suspense.js';
import { registerErrorBoundary } from './components/error-boundary.js';
import { registerErrorMessage } from './components/error-message.js';
customElements.define('x-demo', class extends HTMLElement {
constructor() {
super();
registerLazy();
registerSuspense();
registerErrorBoundary();
registerErrorMessage();
}
connectedCallback() {
this.innerHTML = `
<p>Lazy loading demo</p>
<button id="lazy-load">Load lazy</button>
<button id="error-reset" disabled>Reset error</button>
<div id="lazy-load-div">
<p>Click to load..</p>
</div>
`;
const resetBtn = this.querySelector('button#error-reset')
resetBtn.onclick = () => {
this.querySelector('x-error-boundary').reset();
resetBtn.setAttribute('disabled', true);
};
const loadBtn = this.querySelector('button#lazy-load');
loadBtn.onclick = () => {
this.querySelector('div#lazy-load-div').innerHTML = `
<x-error-boundary>
<x-error-message slot="error"></x-error-message>
<x-suspense>
<p slot="fallback">Loading...</p>
<p><x-lazy><x-hello-world></x-hello-world></x-lazy></p>
</x-suspense>
</x-error-boundary>
`
this.querySelector('x-error-boundary').addEventListener('error', _ => {
resetBtn.removeAttribute('disabled');
});
loadBtn.setAttribute('disabled', true);
};
}
});
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Sweet Suspense</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="React-style lazy loading of web components.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-09-09">
<img src="image.webp" alt="A shadowed figure in a city square, waiting on a woman" loading="lazy" />
<h2>Sweet Suspense</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
I was reading Addy Osmani and Hassan Djirdeh's book <a href="https://largeapps.dev/">Building Large Scale Web Apps</a>.
(Which, by the way, I can definitely recommend.) In it they cover all the ways to make a React app sing at scale.
The chapter on <em>Modularity</em> was especially interesting to me, because JavaScript modules
are a common approach to modularity in both React and vanilla web code.
</p>
<p>
In that chapter on <em>Modularity</em> there was one particular topic that caught my eye,
and it was the use of <code>lazy()</code> and <code>Suspense</code>, paired with an <code>ErrorBoundary</code>.
These are the primitives that React gives us to asynchronously load UI components and their data on-demand while showing a fallback UI,
and replace the UI with an error message when something goes wrong.
If you're not familiar, here's a good <a href="https://refine.dev/blog/react-lazy-loading/#catching-loading-errors">overview page</a>.
</p>
<p>
It was at that time that I was visited by the imp of the perverse, which posed to me a simple challenge:
<em>can you bring React's lazy loading primitives to vanilla web components?</em>
To be clear, there are <a href="https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/">many</a>
<a href="https://github.com/codewithkyle/lazy-loader">ways</a> to
<a href="https://www.webcomponents.org/element/st-lazy">load</a> web components
<a href="https://lamplightdev.com/blog/2020/03/20/lazy-loading-web-components-with-intersection-observer/">lazily</a>.
This is well-trodden territory. What wasn't out there was a straight port of lazy, suspense and error boundary.
The idea would not let me go. So here goes nothing.
</p>
<h3>Lazy</h3>
<p>
The idea and execution of React's lazy is simple. Whenever you want to use a component in your code,
but you don't want to actually fetch its code yet until it needs to be rendered, wrap it using the <code>lazy()</code> function:<br>
<code>const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));</code><br>
</p>
<p>
React will automatically "suspend" rendering when it first bumps into this lazy component until the component has loaded, and then continue automatically.
</p>
<p>
This works in React because the markup of a component only looks like HTML,
but is actually JavaScript in disguise, better known as <em>JSX</em>.
With web components however, the markup that the component is used in is actually HTML,
where there is no <code>import()</code> and no calling of functions.
That means our vanilla <em>lazy</em> cannot be a JavaScript function, but instead it must be an HTML custom element:<br>
<code><x-lazy><x-hello-world></x-hello-world></x-lazy></code>
</p>
<p>
The basic setup is simple, when the lazy component is added to the DOM,
we'll scan for children that have a '-' in the name and therefore are custom elements,
see if they're not yet defined, and load and define them if so.
By using <code>display: contents</code> we can avoid having the <code><x-lazy></code> impact layout.
</p>
<x-code-viewer src="./lazy1.js" name="lazy.js"></x-code-viewer>
<p>
To actually load the element, we'll have to first find the JS file to import, and then run its register function.
By having the function that calls <code>customElements.define</code> as the default export by convention the problem is reduced to finding the path to the JS file.
The following code uses a heuristic that assumes components are in a <code>./components/</code> subfolder of the current document
and follow a consistent file naming scheme:
</p>
<x-code-viewer src="./lazy2-partial.js" name="lazy.js (continued)"></x-code-viewer>
<p>
One could get a lot more creative however, and for example use an
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap">import map</a>
to map module names to files. This I leave as an exercise for the reader.
</p>
<h3>Suspense</h3>
<p>
While the lazy component is loading, we can't show it yet. This is true for custom elements just as much as for React.
That means we need a wrapper component that will show a fallback UI as long as any components in its subtree are loading,
the <code><x-suspense></code> component. This starts out as a tale of two slots. When the suspense element is loading it shows the fallback, otherwise the content.
</p>
<x-code-viewer src="./suspense1-partial.html" name="example.html"></x-code-viewer>
<x-code-viewer src="./suspense1.js" name="suspense.js"></x-code-viewer>
<p>
The trick now is, how to we get <code>loading = true</code> to happen?
In Plain Vanilla's applications page I showed how a React context can be simulated using the <code>element.closest()</code> API.
We can use the same mechanism to create a generic API that will let our suspense wait on a promise to complete.
</p>
<x-code-viewer src="./suspense2-partial.js" name="suspense.js (continued)"></x-code-viewer>
<p>
<code>Suspense.waitFor</code> will call the nearest ancestor <code><x-suspense></code>
to a given element, and give it a set of promises that it should wait on.
This API can then be called from our <code><x-lazy></code> component.
Note that <code>#loadElement</code> returns a promise that completes when the custom element is loaded or fails to load.
</p>
<x-code-viewer src="./lazy3-partial.js" name="lazy.js (continued)"></x-code-viewer>
<p>
The nice thing about the promise-based approach is that we can give it any promise, just like we would with React's suspense.
For example, when loading data in a custom element that is in the suspense's subtree, we can call the exact same API:<br>
<code>Suspense.waitFor(this, fetch(url).then(...))</code>
</p>
<h3>Error boundary</h3>
<p>
Up to this point, we've been assuming everything always works. This is <del>Sparta</del>software, it will never "always work".
What we need is a graceful way to intercept failed promises that are monitored by the suspense,
and show an error message instead. That is the role that React's error boundary plays.
</p>
<p>
The approach is similar to suspense:
</p>
<x-code-viewer src="./error-boundary-partial.html" name="example.html"></x-code-viewer>
<p>
And the code is also quite similar to suspense:
</p>
<x-code-viewer src="./error-boundary.js"></x-code-viewer>
<p>
Similar to suspense, this has an API <code>ErrorBoundary.showError()</code> that can be called
from anywhere inside the error boundary's subtree to show an error that occurs.
The suspense component is then modified to call this API when it bumps into a rejected promise.
To hide the error, the <code>reset()</code> method can be called on the error boundary element.
</p>
<p>
Finally, the <code>error</code> setter will set the error as a property or attribute
on all children in the error slot, which enables customizing the error message's behavior based on the error object's properties
by creating a custom <code><x-error-message></code> component.
</p>
<h3>Conclusion</h3>
<p>
Finally, we can bring all of this together in a single example,
that combines lazy, suspense, error boundary, a customized error message, and a lazy-loaded hello-world component.
</p>
<x-code-viewer src="./example/index.js" name="example/index.js"></x-code-viewer>
<iframe src="example/index.html" title="complete example" height="250"></iframe>
<p>
For the complete example's code, as well as the lazy, suspense and error-boundary components,
check out the <a href="https://github.com/jsebrech/sweet-suspense">sweet-suspense repo on Github</a>.
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113109291222822968"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/lazy1.js
================================================
customElements.define('x-lazy', class extends HTMLElement {
connectedCallback() {
this.style.display = 'contents';
this.#loadLazy();
}
#loadLazy() {
const elements =
[...this.children].filter(_ => _.localName.includes('-'));
const unregistered =
elements.filter(_ => !customElements.get(_.localName));
unregistered.forEach(_ => this.#loadElement(_));
}
#loadElement(element) {
// TODO: load the custom element
}
});
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/lazy2-partial.js
================================================
#loadElement(element) {
// strip leading x- off the name
const cleanName = element.localName.replace(/^x-/, '').toLowerCase();
// assume component is in its own folder
const url = `./components/${cleanName}/${cleanName}.js`;
// dynamically import, then register if not yet registered
return import(new URL(url, document.location)).then(module =>
!customElements.get(element.localName) && module && module.default());
}
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/lazy3-partial.js
================================================
#loadLazy() {
const elements =
[...this.children].filter(_ => _.localName.includes('-'));
const unregistered =
elements.filter(_ => !customElements.get(_.localName));
if (unregistered.length) {
Suspense.waitFor(this,
...unregistered.map(_ => this.#loadElement(_))
);
}
}
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/suspense1-partial.html
================================================
<x-suspense>
<p slot="fallback">Loading...</p>
<x-lazy><x-hello-world></x-hello-world></x-lazy>
</x-suspense>
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/suspense1.js
================================================
export class Suspense extends HTMLElement {
#fallbackSlot;
#contentSlot;
set loading(isLoading) {
if (!this.#fallbackSlot) return;
this.#fallbackSlot.style.display = isLoading ? 'contents' : 'none';
this.#contentSlot.style.display = !isLoading ? 'contents' : 'none';
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#fallbackSlot = document.createElement('slot');
this.#fallbackSlot.style.display = 'none';
this.#fallbackSlot.name = 'fallback';
this.#contentSlot = document.createElement('slot');
this.shadowRoot.append(this.#fallbackSlot, this.#contentSlot);
}
connectedCallback() {
this.style.display = 'contents';
}
}
customElements.define('x-suspense', Suspense);
================================================
FILE: public/blog/articles/2024-09-09-sweet-suspense/suspense2-partial.js
================================================
static waitFor(sender, ...promises) {
const suspense = sender.closest('x-suspense');
if (suspense) suspense.addPromises(...promises);
}
addPromises(...promises) {
if (!promises.length) return;
this.loading = true;
// combine into previous promises if there are any
const newPromise = this.#waitingForPromise =
Promise.allSettled([...promises, this.#waitingForPromise]);
// wait for all promises to complete
newPromise.then(_ => {
// if no newer promises were added, we're done
if (newPromise === this.#waitingForPromise) {
this.loading = false;
}
});
}
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/defined/example.html
================================================
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<title>defining the custom element</title>
<style>
body { font-family: system-ui, sans-serif; margin: 1em; }
x-example {
background-color: lavender;
}
x-example:not(:defined)::after {
content: '{defined: false}'
}
x-example:defined::after {
content: '{defined: true, status: ' attr(status) '}'
}
</style>
</head>
<body>
<p>Custom element: <x-example></x-example></p>
<button onclick="define()">Define</button>
<button onclick="location.reload()">Reload</button>
<script>
function define() {
customElements.define('x-example', class extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.setAttribute('status', 'ready');
}
});
}
</script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/defined2/example.html
================================================
<!doctype html>
<html>
<head>
<title>defined custom elements</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<style>
body { font-family: system-ui, sans-serif; margin: 1em; }
x-container { display: block; border: 1px dotted gray; padding: 1em; }
x-controls { display: block; margin-top: 1em; }
#placeholder:empty::after { content: '-' }
#log { border: 1px dotted gray; padding: 1em; font-size: 80%; }
x-lifecycle { background: #ddd; padding: 0.1em; }
x-lifecycle:not(:defined)::after {
content: '{defined: false}';
}
x-lifecycle:defined::after {
content:
'{defined: true, constructed: true'
', connected: ' attr(connected)
', source: ' attr(source) '}';
}
</style>
</head>
<body>
<x-container>
<p>Static element: <x-lifecycle source="static"></x-lifecycle></p>
<p>Dynamic element: <span id="placeholder"></span></p>
</x-container>
<x-controls state="initial">
<button id="define" onclick="define()">Define</button>
<button id="create" onclick="create()">Create</button>
<button id="upgrade" onclick="upgrade()" disabled>Upgrade</button>
<button id="connect" onclick="connect()" disabled>Connect</button>
<button id="disconnect" onclick="disconnect()" disabled>Disconnect</button>
<button id="delete" onclick="deleteElement()" disabled>Delete</button>
<button onclick="location.reload()">Reload</button>
</x-controls>
<div>
<h4>Dynamic element (in-memory)</h4>
<p><span id="element-status"></span></p>
</div>
<div id="log"><h4>Reactions:</h4></div>
<script>
let storage = { };
update();
function define() {
customElements.define('x-lifecycle', class extends HTMLElement {
get status() {
return 'defined: true, constructed: true' +
', connected: ' + (this.getAttribute('connected') || 'false') +
', source: ' + (this.getAttribute('source') || 'unknown');
}
constructor() {
super();
update();
appendLog('constructed');
}
connectedCallback() {
this.setAttribute('connected', true);
update();
appendLog('connected');
}
disconnectedCallback() {
this.setAttribute('connected', false);
update();
appendLog('disconnected');
}
static observedAttributes = ['source'];
attributeChangedCallback() {
appendLog('attributes changed');
}
});
update();
}
function create() {
storage.element = document.createElement('x-lifecycle');
storage.element.setAttribute('source', 'createElement');
update();
}
function upgrade() {
customElements.upgrade(storage.element);
update();
}
function connect() {
document.querySelector('#placeholder').append(storage.element);
update();
}
function disconnect() {
storage.element.remove();
update();
}
function deleteElement() {
delete storage.element;
update();
}
function update() {
// definition
const isDefined = !!customElements.get('x-lifecycle');
// dynamic element
const isCreated = !!storage.element;
const isConstructed = isCreated && !!storage.element.status;
const isConnected = !!document.querySelector('#placeholder x-lifecycle');
document.querySelector('#element-status').textContent =
storage.element ? '{' +
( storage.element.status ||
(isDefined ? 'defined: true, constructed: false' : 'defined: false')) +
'}' : 'null';
// controls
setButtonDisabled('define', isDefined);
setButtonDisabled('create', isCreated);
setButtonDisabled('connect', !isCreated || isConnected);
setButtonDisabled('disconnect', !storage.element || !isConnected);
setButtonDisabled('upgrade', !isDefined || !isCreated || isConstructed || isConnected);
setButtonDisabled('delete', !isCreated || isConnected);
}
function getButton(id) {
return document.querySelector('button#' + id);
}
function setButtonDisabled(id, disabled) {
const btn = getButton(id);
if (btn) {
disabled ? btn.setAttribute('disabled', true) : btn.removeAttribute('disabled');
}
}
function appendLog(text) {
const p = document.createElement('p');
p.textContent = text;
document.querySelector('#log').append(p);
}
</script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>The life and times of a web component</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="The entire lifecycle of a web component, from original creation to when a shadow crosses.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-09-16">
<img src="image.webp" alt="A custom element at a house party" loading="lazy" />
<h2>The life and times of a web component</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
When first taught about the wave-particle duality of light most people's brains does a double take.
How can light be two different categories of things at the same time, both a wave and a particle? That's just weird.
The same thing happens with web components, confusing people when they first try to learn them and run into their Document-JavaScript duality.
The component systems in frameworks are typically JavaScript-first, only using the DOM as an outlet for their visual appearance.
Web components however — or custom elements to be precise — can start out in either JavaScript or the document, and are married to neither.
</p>
<h3>Just the DOM please</h3>
<p>
Do you want to see the minimal JavaScript code needed to set up an <code><x-example></code> custom element?
Here it is:
</p>
<p> </p>
<p>
No, that's not a typo. Custom elements can be used just fine without any JavaScript.
Consider this example of an <code><x-tooltip></code> custom element that is HTML and CSS only:
</p>
<iframe src="undefined/example.html" name="undefined custom element" height="75"></iframe>
<x-code-viewer src="undefined/example.html" name="example.html"></x-code-viewer>
<p>
<small>For the curious, here is the <a href="undefined/example.css">example.css</a>, but it is not important here.</small>
</p>
<p>
Such elements are called <em>undefined custom elements</em>.
Before custom elements are defined in the window by calling <code>customElements.define()</code> they always start out in this state.
There is no need to actually define the custom element if it can be solved in a pure CSS way.
In fact, many "pure CSS" components found online can be solved by such custom elements,
by styling the element itself and its <code>::before</code> and <code>::after</code> pseudo-elements.
</p>
<h3>A question of definition</h3>
<p>
The CSS-only representation of the custom element can be progressively enhanced by connecting it up to a JavaScript counterpart,
a custom element class. This is a class that inherits from <code>HTMLElement</code> and allows the custom element
to implement its own logic.
</p>
<iframe src="defined/example.html" name="defining the custom element" height="125"></iframe>
<x-code-viewer src="defined/example.html" name="example.html"></x-code-viewer>
<p>
What happens to the elements already in the markup at the moment <code>customElements.define()</code>
is called is an <em>element upgrade</em>. The browser will take all custom elements already in the document,
and create an instance of the matching custom element class that it connects them to.
This class enables the element to control its own part of the DOM, but also allows it to react to what happens in the DOM.
</p>
<p>
Element upgrades occur for existing custom elements in the document when <code>customElements.define()</code> is called,
and for all new custom elements with that tag name created afterwards (e.g. using <code>document.createElement('x-example')</code>).
It does not occur automatically for detached custom elements (not part of the document) that were created before the element was defined.
Those can be upgraded retroactively by calling <code>customElements.upgrade()</code>.
</p>
<p>
So far, this is the part of the lifecycle we've seen:
</p>
<pre>
<undefined>
-> define() -> <defined>
-> automatic upgrade()
-> constructor()
-> <constructed>
</pre>
<p>
The constructor as shown in the example above is optional, but if it is specified then it has a <a href="https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance">number of gotcha's</a>:
</p>
<dl>
<dt>It must start with a call to <code>super()</code>.</dt>
<dt>It should not make DOM changes yet, as the element is not yet guaranteed to be connected to the DOM.</dt>
<dd>
This includes reading or modifying its own DOM properties, like its attributes.
The tricky part is that in the constructor the element might already be in the DOM,
so setting attributes might work. Or it might give an error. It's best to avoid DOM interaction altogether in the constructor.
</dd>
<dt>It should initialize its state, like class properties</dt>
<dd>
But work done in the constructor should be minimized and maximally postponed until <code>connectedCallback</code>.
</dd>
</dl>
<h3>Making connections</h3>
<p>
After being constructed, if the element was already in the document, its <code>connectedCallback()</code> handler is called.
This handler is normally called only when the element is inserted into the document, but for elements that are already in the document when they are defined it ends up being called as well.
In this handler DOM changes can be made, and in the example above the <code>status</code> attribute is set to demonstrate this.
</p>
<p>
The <em>connectedCallback()</em> handler is part of what is known in the HTML standard as <a href="https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions">custom element reactions</a>:
These reactions allow the element to respond to various changes to the DOM:
</p>
<ul>
<li><code>connectedCallback()</code> is called when the element is inserted into the document, even if it was only moved from a different place in the same document.</li>
<li><code>disconnectedCallback()</code> is called when the element is removed from the document.</li>
<li><code>adoptedCallback()</code> is called when the element is moved to a new document. (You are unlikely to need this in practice.)</li>
<li><code>attributeChangedCallback()</code> is called when an attribute is changed, but only for the attributes listed in its <code>observedAttributes</code> property.</li>
</ul>
<p>
There are also special reactions for <a href="https://dev.to/stuffbreaker/custom-forms-with-web-components-and-elementinternals-4jaj">form-associated custom elements</a>,
but those are a rabbit hole beyond the purview of this blog post.
</p>
<p>
There are more gotcha's to these reactions:
</p>
<dl>
<dt><code>connectedCallback()</code> and <code>disconnectedCallback()</code> can be called multiple times</dt>
<dd>
This can occur when the element is moved around in the document.
These handlers should be written in such a way that it is harmless to run them multiple times,
e.g. by doing an early exit when it is detected that <em>connectedCallback()</em> was already run.
</dd>
<dt><code>attributeChangedCallback()</code> can be called before <code>connectedCallback()</code></dt>
<dd>
For all attributes already set when the element in the document is upgraded,
the <em>attributeChangedCallback()</em> handler will be called first,
and only after this <em>connectedCallback()</em> is called.
The unpleasant consequence is that any <em>attributeChangedCallback</em> that tries to update DOM structures
created in <em>connectedCallback</em> can produce errors.
</dd>
<dt><code>attributeChangedCallback()</code> is only called for attribute changes, not property changes.</dt>
<dd>
Attribute changes can be done in Javascript by calling <code>element.setAttribute('name', 'value')</code>.
DOM attributes and class properties can have the same name, but are not automatically linked.
Generally for this reason it is better to avoid having attributes and properties with the same name.
</dd>
</dl>
<p>
The lifecycle covered up to this point for elements that start out in the initial document:
</p>
<pre>
<undefined>
-> define() -> <defined>
-> automatic upgrade()
-> [element].constructor()
-> [element].attributeChangedCallback()
-> [element].connectedCallback()
-> <connected>
</pre>
<h3>Flip the script</h3>
<p>
So far we've covered one half of the Document-JavaScript duality, for custom elements starting out in the document,
and only after that becoming defined and gaining a JavaScript counterpart.
It is however also possible to reverse the flow, and start out from JavaScript.
</p>
<p>
This is the minimal code to create a custom element in JavaScript: <code>document.createElement('x-example')</code>.
The element does not need to be defined in order to run this code, although it can be, and the resulting node can be inserted into the document
as if it was part of the original HTML markup.
</p>
<p>
If it is inserted, and after insertion the element becomes defined, then it will behave as described above.
Things are however different if the element remains detached:
</p>
<dl>
<dt>The detached element will not be automatically upgraded when it is defined.</dt>
<dd>
The constructor or reactions will not be called. It will be automatically upgraded when it is inserted into the document.
It can also be upgraded explicitly by calling <code>customElements.upgrade()</code>.
</dd>
<dt>If the detached element is already defined when it is created, it will be upgraded automatically.</dt>
<dd>The <em>constructor()</em> and <em>attributeChangedCallback()</em> will be called. Because it is not yet part of the document <em>connectedCallback()</em> won't be.</dd>
</dl>
<p>
By now no doubt you are a bit confused. Here's an interactive playground that lets you test
what happens to elements as they go through their lifecycle, both for those in the initial document and those created dynamically.
</p>
<iframe src="defined2/example.html" name="custom element lifecycle" height="600"></iframe>
<p>Here are some interesting things to try out:</p>
<ul>
<li><em>Create</em>, then <em>Define</em>, and you will see that the created element is not upgraded automatically because it is detached from the document.</li>
<li><em>Create</em>, then <em>Connect</em>, then <em>Define</em>, and you will see that the element is upgraded automatically because it is in the document.</li>
<li><em>Define</em>, then <em>Create</em>, and you will see that the element is upgraded as soon as it is created (<em>constructed</em> appears in the reactions).</li>
</ul>
<p>
I tried writing a flowchart of all possible paths through the lifecycle that can be seen in this example,
but it got so unwieldy that I think it's better to just play around with the example until a solid grasp develops.
</p>
<h3>In the shadows</h3>
<p>
Adding shadow DOM creates yet another wrinkle in the lifecycle.
At any point in the element's JavaScript half, including in its constructor, a shadow DOM can be attached to the element by calling <code>attachShadow()</code>.
Because the shadow DOM is immediately available for DOM operations, that makes it possible to do those DOM operations in the constructor.
</p>
<p>
In this next interactive example you can see what happens when the shadow DOM becomes attached.
The <em>x-shadowed</em> element will immediately attach a shadow DOM in its constructor,
which happens when the element is upgraded automatically after defining.
The <em>x-shadowed-later</em> element postpones adding a shadow DOM until a link is clicked,
so the element first starts out as a non-shadowed custom element, and adds a shadow DOM later.
</p>
<iframe src="shadowed/example.html" name="shadowed custom elements"></iframe>
<x-code-viewer src="shadowed/example.html" name="example.html"></x-code-viewer>
<p>
While adding a shadow DOM can be done at any point, it is a one-way operation.
Once added the shadow DOM will replace the element's original contents, and this cannot be undone.
</p>
<h3>Keeping an eye out</h3>
<p>
So far we've mostly been dealing with initial setup of the custom element,
but a major part of the lifecycle is responding to changes as they occur.
Here are some of the major ways that custom elements can respond to DOM changes:
</p>
<ul>
<li><em>connectedCallback</em> and <em>disconnectedCallback</em> to handle DOM insert and remove of the element itself.</li>
<li><em>attributeChangedCallback</em> to handle attribute changes of the element.</li>
<li>For shadowed custom elements, the <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event">slotchange</a> event can be used to detect when children are added and removed in a <code><slot></code>.</li>
<li>Saving the best for last, <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a> can be used to monitor DOM subtree changes, as well as attribute changes.</li>
</ul>
<p>
<em>MutationObserver</em> in particular is worth exploring, because it is a swiss army knife for monitoring the DOM.
Here's an example of a counter that automatically updates when new child elements are added:
</p>
<iframe src="observer/example.html" name="custom element with observer"></iframe>
<x-code-viewer src="observer/example.html" name="example.html"></x-code-viewer>
<p>
There is still more to tell, but already I can feel eyes glazing over and brains turning to mush,
so I will keep the rest for another day.
</p>
<hr />
<p>
Phew, that was a much longer story than I originally set out to write, but custom elements have surprising intricacy.
I hope you found it useful, and if not at least you got to see some code and click some buttons.
It's all about the clicking of the buttons.
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113147980761074467"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/observer/example.html
================================================
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<title>custom element with observer</title>
<style>
body { font-family: system-ui, sans-serif; margin: 1em; }
x-wall { display: block; margin-bottom: 1em; }
</style>
</head>
<body>
<x-wall><x-bottle></x-bottle></x-wall>
<button onclick="add()">Add one more</button>
<button onclick="location.reload()">Reload</button>
<script>
customElements.define('x-wall', class extends HTMLElement {
connectedCallback() {
if (this.line) return; // prevent double initialization
this.line = document.createElement('p');
this.insertBefore(this.line, this.firstChild);
new MutationObserver(() => this.update()).observe(this, { childList: true });
this.update();
}
update() {
const count = this.querySelectorAll('x-bottle').length;
this.line.textContent =
`${count} ${count === 1 ? 'bottle' : 'bottles'} of beer on the wall`;
}
});
customElements.define('x-bottle', class extends HTMLElement {
connectedCallback() { this.textContent = '🍺'; }
});
function add() {
document.querySelector('x-wall').append(
document.createElement('x-bottle'));
}
</script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/shadowed/example.html
================================================
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<title>shadowed custom element</title>
<style>
body { font-family: system-ui, sans-serif; margin: 1em; }
x-shadowed, x-shadowed-later { background-color: lightgray; }
</style>
</head>
<body>
<p><x-shadowed>: <x-shadowed>undefined, not shadowed</x-shadowed></p>
<p><x-shadowed-later>: <x-shadowed-later>undefined, not shadowed</x-shadowed-later></p>
<button id="define" onclick="define()">Define</button>
<button onclick="location.reload()">Reload</button>
<script>
function define() {
customElements.define('x-shadowed', class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<span style="background-color: lightgreen">
shadowed
</span>
`;
}
});
customElements.define('x-shadowed-later', class extends HTMLElement {
connectedCallback() {
this.innerHTML = 'constructed, <a href="#">click to shadow</a>';
this.querySelector('a').onclick = (e) => { e.preventDefault(); this.addShadow() };
}
addShadow() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<span style="background-color: lightgreen">
shadowed
</span>
`;
}
});
document.querySelector('button#define').setAttribute('disabled', true);
}
</script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/undefined/example.css
================================================
body { font-family: system-ui, sans-serif; margin: 1em; }
button { user-select: none; }
/* based on https://github.com/argyleink/gui-challenges/blob/main/tooltips/tool-tip.css */
x-tooltip {
--color: lightgray;
--bg: hsl(0 0% 20%);
pointer-events: none;
user-select: none;
/* animate in on hover or focus of parent element */
z-index: 1;
opacity: 0;
transition: opacity .2s ease;
transition-delay: 200ms;
:is(:hover, :focus-visible, :active) > & {
opacity: 1;
}
/* vertically center and move to the right */
position:absolute;
top:50%;
transform:translateY(-50%);
left: calc(100% + 15px);
padding: 0.5em;
inline-size: max-content;
max-inline-size: 25ch;
/* color, backdrop and shadow */
color: var(--color);
filter:
drop-shadow(0 3px 3px hsl(0 0% 0% / 50%))
drop-shadow(0 12px 12px hsl(0 0% 0% / 50%));
&::after {
content: "";
background: var(--bg);
position: absolute;
z-index: -1;
left: 0;
top: 50%;
transform:translateY(-50%);
width: 100%;
height: 100%;
border-radius: 5px;
}
/* fix drop shadow in safari */
will-change: filter;
}
button:has(> x-tooltip) {
position: relative;
}
================================================
FILE: public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/undefined/example.html
================================================
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<link rel="stylesheet" href="example.css">
<title>undefined custom element</title>
</head>
<body>
<button>
Hover me
<x-tooltip inert role="tooltip">Thanks for hovering!</x-tooltip>
</button>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/AddTask.js
================================================
customElements.define('task-add', class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<input type="text" placeholder="Add task" />
<button>Add</button>
`;
this.querySelector('button').onclick = () => {
const input = this.querySelector('input');
this.closest('tasks-context').dispatch({
type: 'added',
id: nextId++,
text: input.value
});
input.value = '';
};
}
})
let nextId = 3;
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/App.js
================================================
customElements.define('tasks-app', class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<tasks-context>
<h1>Day off in Kyoto</h1>
<task-add></task-add>
<task-list></task-list>
</tasks-context>
`;
}
});
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TaskList.js
================================================
customElements.define('task-list', class extends HTMLElement {
get context() { return this.closest('tasks-context'); }
connectedCallback() {
this.context.addEventListener('change', () => this.update());
this.append(document.createElement('ul'));
this.update();
}
update() {
const ul = this.querySelector('ul');
let before = ul.firstChild;
this.context.tasks.forEach(task => {
let li = ul.querySelector(`:scope > [data-key="${task.id}"]`);
if (!li) {
li = document.createElement('li');
li.dataset.key = task.id;
li.append(document.createElement('task-item'));
}
li.firstChild.task = task;
// move to the right position in the list if not there yet
if (li !== before) ul.insertBefore(li, before);
before = li.nextSibling;
});
// remove unknown nodes
while (before) {
const remove = before;
before = before.nextSibling;
ul.removeChild(remove);
}
}
});
customElements.define('task-item', class extends HTMLElement {
#isEditing = false;
#task;
set task(task) { this.#task = task; this.update(); }
get context() { return this.closest('tasks-context'); }
connectedCallback() {
if (this.querySelector('label')) return;
this.innerHTML = `
<label>
<input type="checkbox" />
<input type="text" />
<span></span>
<button id="edit">Edit</button>
<button id="save">Save</button>
<button id="delete">Delete</button>
</label>
`;
this.querySelector('input[type=checkbox]').onchange = e => {
this.context.dispatch({
type: 'changed',
task: {
...this.#task,
done: e.target.checked
}
});
};
this.querySelector('input[type=text]').onchange = e => {
this.context.dispatch({
type: 'changed',
task: {
...this.#task,
text: e.target.value
}
});
};
this.querySelector('button#edit').onclick = () => {
this.#isEditing = true;
this.update();
};
this.querySelector('button#save').onclick = () => {
this.#isEditing = false;
this.update();
};
this.querySelector('button#delete').onclick = () => {
this.context.dispatch({
type: 'deleted',
id: this.#task.id
});
};
this.context.addEventListener('change', () => this.update());
this.update();
}
update() {
if (this.isConnected && this.#task) {
this.querySelector('input[type=checkbox]').checked = this.#task.done;
const inputEdit = this.querySelector('input[type=text]');
inputEdit.style.display = this.#isEditing ? 'inline' : 'none';
inputEdit.value = this.#task.text;
const span = this.querySelector('span');
span.style.display = this.#isEditing ? 'none' : 'inline';
span.textContent = this.#task.text;
this.querySelector('button#edit').style.display = this.#isEditing ? 'none' : 'inline';
this.querySelector('button#save').style.display = this.#isEditing ? 'inline' : 'none';
}
}
});
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TasksContext.js
================================================
customElements.define('tasks-context', class extends HTMLElement {
#tasks = structuredClone(initialTasks);
get tasks() { return this.#tasks; }
set tasks(tasks) {
this.#tasks = tasks;
this.dispatchEvent(new Event('change'));
}
dispatch(action) {
this.tasks = tasksReducer(this.tasks, action);
}
connectedCallback() {
this.style.display = 'contents';
}
});
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/index.js
================================================
import './App.js';
import './AddTask.js';
import './TaskList.js';
import './TasksContext.js';
const render = () => {
const root = document.getElementById('root');
root.append(document.createElement('tasks-app'));
}
document.addEventListener('DOMContentLoaded', render);
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/styles.css
================================================
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
h1 {
margin-top: 0;
font-size: 22px;
}
h2 {
margin-top: 0;
font-size: 20px;
}
h3 {
margin-top: 0;
font-size: 18px;
}
h4 {
margin-top: 0;
font-size: 16px;
}
h5 {
margin-top: 0;
font-size: 14px;
}
h6 {
margin-top: 0;
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-inline-start: 20px;
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>The unreasonable effectiveness of vanilla JS</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="A case study in porting intricate React code to vanilla.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-09-28">
<img src="image.webp" alt="A female comic book hero bearing a vanilla sigil on her chest" loading="lazy" />
<h2>The unreasonable effectiveness of vanilla JS</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
I have a confession to make. At the end of the Plain Vanilla tutorial's <a href="../../../pages/applications.html">Applications page</a>
a challenge was posed to the reader: port <a href="https://react.dev">react.dev</a>'s final example
<a href="https://react.dev/learn/scaling-up-with-reducer-and-context">Scaling Up with Reducer and Context</a> to vanilla web code.
Here's the confession: until today I had never actually ported over that example myself.
</p>
<p>
That example demonstrates a cornucopia of React's featureset.
Richly interactive UI showing a tasks application, making use of a context to lift the task state up,
and a reducer that the UI's controls dispatch to. React's DOM-diffing algorithm gets a real workout
because each task in the list can be edited independently from and concurrently with the other tasks.
It is an intricate and impressive demonstration. Here it is in its interactive glory:
</p>
<iframe src="./complete/index.html" title="complete example" height="250"></iframe>
<p>
But I lied. That interactive example is actually the vanilla version and it is identical.
If you want to verify that it is in fact identical, check out the <a href="https://codesandbox.io/p/sandbox/react-dev-wy7lfd">original React example</a>.
And with that out of the way, let's break apart the vanilla code.
</p>
<h3>Project setup</h3>
<p>The React version has these code files that we will need to port:</p>
<ul>
<li><strong>public/index.html</strong></li>
<li><strong>src/styles.css</strong></li>
<li><strong>src/index.js</strong>: imports the styles, bootstraps React and renders the App component</li>
<li><strong>src/App.js</strong>: renders the context's TasksProvider containing the AddTask and TaskList components</li>
<li><strong>src/AddTask.js</strong>: renders the simple form at the top to add a new task</li>
<li><strong>src/TaskList.js</strong>: renders the list of tasks</li>
</ul>
<p>
To make things fun, I chose the same set of files with the same filenames for the vanilla version.
Here's <strong>index.html</strong>:
</p>
<x-code-viewer src="./complete/index.html" name="index.html"></x-code-viewer>
<p>
The only real difference is that it links to <strong>index.js</strong> and <strong>styles.css</strong>.
The stylesheet was copied verbatim, but for the curious here's a link to <a href="./complete/styles.css">styles.css</a>.
</p>
<h3>Get to the code</h3>
<p>
<strong>index.js</strong> is where it starts to get interesting.
Compare the React version to the vanilla version:
</p>
<x-code-viewer src="./react/src/index.js" name="index.js (React)"></x-code-viewer>
<x-code-viewer src="./complete/index.js" name="index.js (Vanilla)"></x-code-viewer>
<p>
Bootstrapping is different but also similar. All of the web components are imported first to load them,
and then the <code><tasks-app></code> component is rendered to the page.
</p>
<p>
The <strong>App.js</strong> code also bears more than a striking resemblance:
</p>
<x-code-viewer src="./react/src/App.js" name="App.js (React)"></x-code-viewer>
<x-code-viewer src="./complete/App.js" name="App.js (Vanilla)"></x-code-viewer>
<p>
What I like about the code so far is that it <em>feels</em> React-like. I generally find programming against React's API pleasing,
but I don't like the tooling, page weight and overall complexity baggage that it comes with.
</p>
<h3>Adding context</h3>
<p>
The broad outline of how to bring a React-like context to a vanilla web application is
already explained in the <a href="https://plainvanillaweb.com/pages/applications.html#managing-state">passing data deeply section</a>
of the main Plain Vanilla tutorial, so I won't cover that again here.
What adds spice in this specific case is that the React context uses a reducer,
a function that accepts the old tasks and an action to apply to them, and returns the new tasks to show throughout the application.
</p>
<p>
Thankfully, the React example's reducer function and initial state were already vanilla JS code,
so those come along for the ride unchanged and ultimately the vanilla context is a very straightforward custom element:
</p>
<x-code-viewer src="./complete/TasksContext.js" name="TasksContext.js (Vanilla)"></x-code-viewer>
<p>
The actual context component is very bare bones, as it only needs to store the tasks,
emit change events for the other components to subscribe to, and provide a dispatch method
for those components to call that will use the reducer function to update the tasks.
</p>
<h3>Adding tasks</h3>
<p>
The AddTask component ends up offering more of a challenge. It's a stateful component with event listeners that dispatches to the reducer:
</p>
<x-code-viewer src="./react/src/AddTask.js" name="AddTask.js (React)"></x-code-viewer>
<p>
The main wrinkle this adds for the vanilla web component is that the event listener on the button element
cannot be put inline with the markup. Luckily the handling of the input is much simplified
because we can rely on it keeping its state automatically, a convenience owed to not using a virtual DOM.
Thanks to the groundwork in the context component the actual dispatching of the action is easy:
</p>
<x-code-viewer src="./complete/AddTask.js" name="AddTask.js (Vanilla)"></x-code-viewer>
<p>
Fascinating to me is that <strong>index.js</strong>, <strong>App.js</strong>, <strong>TasksContext.js</strong> and <strong>AddTask.js</strong>
are all fewer lines of code in the vanilla version than their React counterpart while remaining functionally equivalent.
</p>
<h3>Hard mode</h3>
<p>
The TaskList component is where React starts really pulling its weight.
The React version is clean and straightforward and juggles a lot of state with a constantly updating task list UI.
</p>
<x-code-viewer src="./react/src/TaskList.js" name="TaskList.js (React)"></x-code-viewer>
<p>
This proved to be a real challenge to port. The vanilla version ended up being a lot more verbose
because it has to do all the same DOM-reconciliation in explicit logic managed by the <strong>update()</strong> methods
of <code><task-list></code> and <code><task-item></code>.
</p>
<x-code-viewer src="./complete/TaskList.js" name="TaskList.js (Vanilla)"></x-code-viewer>
<p>
Some interesting take-aways:
</p>
<ul>
<li>
The <code><task-list></code> component's <strong>update()</strong> method implements a poor man's version of React reconciliation,
merging the current state of the <strong>tasks</strong> array into the child nodes of the <code><ul></code>.
In order to do this, it has to store a key on each list item, just like React requires, and here it becomes obvious why that is.
Without the key we can't find the existing <code><li></code> nodes that match up to task items,
and so would have to recreate the entire list. By adding the key it becomes possible to update the list in-place,
modifying task items instead of recreating them so that they can keep their on-going edit state.
</li>
<li>
That reconciliation code is very generic however, and it is easy to imagine a fully generic <strong>repeat()</strong>
function that converts an array of data to markup on the page. In fact, the Lit framework <a href="https://lit.dev/docs/templates/lists/#the-repeat-directive">contains exactly that</a>.
For brevity's sake this code doesn't go quite that far.
</li>
<li>
The <code><task-item></code> component cannot do what the React code does: create different markup depending on the current state.
Instead it creates the union of the markup across the various states, and then in the <strong>update()</strong>
shows the right subset of elements based on the current state.
</li>
</ul>
<p>
That wraps up the entire code. You can find the <a href="https://github.com/jsebrech/vanilla-context-and-reducer">ported example on Github</a>.
</p>
<h3>Some thoughts</h3>
<p>
A peculiar result of this porting challenge is that the vanilla version ends up being roughly
the same number of lines of code as the React version. The React code is still overall less verbose (all those querySelectors, oy!),
but it has its own share of boilerplate that disappears in the vanilla version.
This isn't a diss against React, it's more of a compliment to how capable browsers have gotten that vanilla web components
can carry us so far.
</p>
<p>
If I could have waved a magic wand, what would have made the vanilla version simpler?
</p>
<ul>
<li>
All of those <strong>querySelector</strong> calls get annoying. The alternatives are building the markup easily with innerHTML
and then fishing out references to the created elements using querySelector, or building the elements one by one verbosely using createElement,
but then easily having a reference to them. Either of those ends up very verbose.
An alternative templating approach that makes it easy to create elements <em>and</em> get a reference to them would be very welcome.
</li>
<li>
As long as we're dreaming, I'm jealous of how easy it is to add the event listeners in JSX.
A real expression language in HTML templates that supports data and event binding and data-conditional markup would be very neat
and would take away most of the reason to still find a framework's templating language more convenient.
Web components are a perfectly fine alternative to React components, they just lack an easy built-in templating mechanism.
</li>
<li>
Browsers could get a little smarter about how they handle DOM updates during event handling.
In the logic that sorts the <code><li></code> to the right order in the list,
the <em>if</em> condition before insertBefore proved necessary because the browser
didn't notice that the element was already placed where it needed to be inserted,
and click events would get lost as a consequence.
I've even noticed that assigning a textContent to a button mid-click will make Safari
lose track of that button's click event. All of that can be worked around with clever reconciliation logic,
but that's code that belongs in the browser, not in JavaScript.
</li>
</ul>
<p>
All in all though, I'm really impressed with vanilla JS. I call it unreasonably effective because it is
jarring just how capable the built-in abilities of browsers are, and just how many web developers despite that
still default to web frameworks for every new project. Maybe one day...
</p>
</main>
<blog-footer mastodon-url="https://mstdn.social/@joeri_s/113215359038426580"></blog-footer>
<script type="module" src="../../index.js"></script>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/package.json
================================================
{
"name": "react.dev",
"version": "0.0.0",
"main": "/src/index.js",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "^5.0.0"
},
"devDependencies": {}
}
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/AddTask.js
================================================
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch();
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/App.js
================================================
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function TaskApp() {
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TaskList.js
================================================
import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useTasksDispatch();
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
text: e.target.value
}
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
done: e.target.checked
}
});
}}
/>
{taskContent}
<button onClick={() => {
dispatch({
type: 'deleted',
id: task.id
});
}}>
Delete
</button>
</label>
);
}
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TasksContext.js
================================================
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/index.js
================================================
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
================================================
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/styles.css
================================================
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
h1 {
margin-top: 0;
font-size: 22px;
}
h2 {
margin-top: 0;
font-size: 20px;
}
h3 {
margin-top: 0;
font-size: 18px;
}
h4 {
margin-top: 0;
font-size: 16px;
}
h5 {
margin-top: 0;
font-size: 14px;
}
h6 {
margin-top: 0;
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-inline-start: 20px;
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
================================================
FILE: public/blog/articles/2024-09-30-lived-experience/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Lived experience</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="description" content="Thoughts on the past and future of frameworks, web components and web development.">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<blog-header published="2024-09-30">
<img src="image.webp" alt="An old man sitting astride a tall pile of books" loading="lazy" />
<h2>Lived experience</h2>
<p class="byline" aria-label="author">Joeri Sebrechts</p>
</blog-header>
<main>
<p>
Ryan Carniato shared a hot take a few days ago, <a href="https://dev.to/ryansolid/web-components-are-not-the-future-48bh">Web Components Are Not the Future</a>.
As hot takes tend to do, it got some responses, like Nolan Lawson's piece <a href="https://nolanlawson.com/2024/09/28/web-components-are-okay/">Web components are okay</a>,
or Cory LaViska's <a href="https://www.abeautifulsite.net/posts/web-components-are-not-the-future-they-re-the-present/">Web Components Are Not the Future — They're the Present</a>.
They do an excellent job of directly engaging Ryan's arguments, so I'm not going to do that here.
Instead I want to talk about my lived experience of web development, and where I hope it is headed in the future.
Take it in the spirit it is intended, one of optimism and possibility.
</p>
<h3>A galaxy far, far away</h3>
<p>
So I've been making web sites since a long time ago, since before CSS and HTML4.
I say this not to humblebrag, but to explain that I was here for all of it.
Every time when the definition of <em>modern web development</em> changed I would update my priors, following along.
</p>
<p>
For the longest time making web pages do anything except show text and images was an exercise in frustration.
Browsers were severely lacking in features, were wildly incompatible with web standards and each other,
and the tools that web developers needed to bring them to order were missing or lacking.
I built my share of vanilla JS components back in the IE6 days, and it made me dream of a better way.
When frameworks first started coming on the scene with that better way, adding missing features, abstracting away incompatibility,
and providing better tooling, I was ready for them. I was all-in.
</p>
<p>
I bought into ExtJS, loved it for a time, and then got a hundred thousand line codebase stuck on ExtJS 3 because version 4 changed things
so much that porting was too costly. I then bought into Backbone, loved that too, but had to move on when its principal developer did.
I joined a team that bought into AngularJS and got stuck painted into a corner when the Angular team went in a totally different direction for v2.
I helped rewrite a bunch of frontends in Angular v2 and React,
and found myself sucked into constant forced maintenance when their architecture and ecosystems churned.
</p>
<p>
Did I make bad choices? Even in hindsight I would say I picked the right choices for the time.
Time just moved on.
</p>
<h3>The cost of change</h3>
<p>
This lived experience taught me a strong awareness of rates of change in dependencies, and the costs they impose.
I imagine a web page as a thin old man sitting astride a tall pile of dependencies, each changing at their own pace.
Some dependencies are stable for decades, like HTML's core set of elements, or CSS 2's set of layout primitives.
They're so stable that we don't even consider them dependencies, they're just <em>the web</em>.
</p>
<p>
Other dependencies change every few years, like module systems, or new transpiled languages,
or the preferred build and bundling tool of the day, or what framework is in vogue.
Then there are the dependencies that change yearly, like major framework and OS releases.
Finally there are the dependencies that change constantly, like the many packages that contribute
to a typical web application built with a popular framework.
</p>
<p>
As a web developer who loves their user, taking on those dependencies creates a Solomon's choice.
Either you keep up with the churn, and spend a not insignificant amount of your day working and reworking code
that already works, instead of working on the things your user cares about.
Or, you stick it out for as long as you can on old versions, applying ever more workarounds to get
old framework releases and their outdated build and CLI tools to work in new OS and ecosystem environments,
slowly boiling a frog that will at some point force a deep rewrite, again at the expense of the user.
</p>
<p>
Which is not to say the frameworks don't add value. They absolutely do, and they keep getting better.
Writing new code on a new framework is a steadily rising tide of developer experience.
But let us not pretend these benefits don't come at a cost.
Wherever there is a codebase too complicated to understand and maintain yourself, wherever there is a set of build tools
that must be kept compatible with changes in operating systems and ecosystems,
there is a shelf life. Sooner or later the makers of every framework and of every tool will move on,
even if it's just to a new long-term supported release, and the web developers that they served will have to move with them.
</p>
<p>
I hold this truth to be self-evident: the larger the abstraction layer a web developer uses on top of web standards,
the shorter the shelf life of their codebase becomes, and the more they will feel the churn.
</p>
<h3>The rising tide</h3>
<p>
Why do modern web projects built with modern frameworks depend on so much <em>stuff</em>?
At first there was no other option. Interacting with the DOM was painful,
and web frameworks rightly made choices to keep component systems outside the DOM, minimizing and abstracting away those interactions
in increasingly clever DOM reconciliation strategies. Supporting the brittle browsers and slow devices of the day required many workarounds and polyfills,
and web frameworks rightly added intricate tools to build, bundle and minify the user's code.
</p>
<p>
They needed a way to bring dependencies into those build systems, and sanely settled on the convention of node modules
and the NPM ecosystem. It got easy to add more dependencies, and just as water always finds the easy way down,
dependencies found the easy way in. As the abstraction layer grew the load time cost imposed by it grew right along,
and so we got server-side rendering, client-side hydration, lazy loading, and many other load time reduction strategies.
</p>
<p>
DOM-diffing, synthetic event systems, functional components, JSX, reactive data layers, server-side rendering and streaming, bundlers, tree shaking,
transpilers and compilers, and all the other complications that you won't find in web standards but you will find in every major web framework —
they are the things invented to make the modern web possible, but they are not the web. The web is what ships to the browser.
And all of those things are downstream from the decision to abstract away the browser,
a decision once made in good faith and for good reasons. A decision which now needs revisiting.
</p>
<p>
Browsers were not standing still. They saw what web developers were doing in userland to compensate
for the deficiencies in browser API's, and they kept improving and growing the platform, a rising tide slowly catching up to what frameworks did.
When Microsoft bid IE <a href="https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/">a well-deserved farewell</a> on June 15, 2022 a tipping point was reached.
For the first time the browser platform was so capable that it felt to me like it didn't need so much abstracting away anymore.
It wasn't a great platform, not as cleanly designed or complete as the API's of the popular frameworks,
but it was <em>Good Enough™</em> as a foundation, and that was all that mattered.
</p>
<h3>Holding my breath</h3>
<p>
I was very excited for what would happen in the framework ecosystem. There was a golden opportunity for frameworks
to tear down their abstraction layers, make something far simpler, far more closely aligned with the base web platform.
They could have a component system built on top of web components,
leveraging browser events and built-in DOM API's. All the frameworks could become cross-compatible,
easily plugging into each other's data layers and components while preserving what makes them unique.
The page weights would shrink by an order of magnitude with so much infrastructure code removed,
and that in combination with the move to HTTP/3 could make build tools optional.
It would do less, so inevitably be worse in some ways, but sometimes <a href="https://en.wikipedia.org/wiki/Worse_is_better">worse is better</a>.
</p>
<p>
I gave a talk about how good the browser's platform had gotten,
showing off <a href="https://github.com/jsebrech/create-react-app-zero">a version of Create React App</a> that didn't need any build tools
and was extremely light-weight, and the developer audience was just as excited as I was.
And I held my breath waiting on framework churn to for once go in the other direction, towards simplicity...
</p>
<p>
But nothing happened. In fact, the major frameworks kept building up their abstraction layers instead of building down.
We got React Server Components and React Compiler, exceedingly clever, utterly incomprehensible,
workarounds for self-imposed problems caused by overengineering.
Web developers don't seem to mind, but they struggle quietly with how to keep up with these tools and deliver good user experiences.
The bigger the abstraction layer gets, the more they feel the churn.
</p>
<p>
The irony is not lost on me that now the framework authors also feel the churn in their dependencies,
struggling to adapt to web components as foundational technology. React 19 is supposed to finally support web components
in a way that isn't incredibly painful, or so they say, we'll see. I confess to feeling some satisfaction
in their struggle. The shoe is on the other foot. Welcome to modern web development.
</p>
<h3>The road ahead</h3>
<p>
What the frameworks are doing, that's fine for them, and they can keep doing it.
But I'm done with all that unless someone is paying me to do it.
They're on a fundamentally different path from where I want web development to go, from how I want to make web pages.
The web is what ships to the browser. Reducing the distance between what the developer writes and what ships to the browser
is valuable and necessary. This blog and this site are my own stake in the ground for this idea,
showing just how much you can get done without any framework code or build tools at all.
But let's be honest: web components are not a framework, no matter how hard I tried to explain them as one.
</p>
<blockquote>
<p>Comparing web components to React is like comparing a good bicycle with a cybertruck.</p>
<p>They do very different things, and they're used b
gitextract_5pqw1n3f/
├── .github/
│ └── workflows/
│ └── static.yml
├── .gitignore
├── .hintrc
├── LICENSE.md
├── README.md
├── eslint.config.cjs
└── public/
├── blog/
│ ├── archive.html
│ ├── articles/
│ │ ├── 2024-08-17-lets-build-a-blog/
│ │ │ ├── card.html
│ │ │ ├── example.html
│ │ │ ├── generator.js
│ │ │ └── index.html
│ │ ├── 2024-08-25-vanilla-entity-encoding/
│ │ │ ├── example1.js
│ │ │ ├── example2.js
│ │ │ ├── example3.js
│ │ │ ├── html.js
│ │ │ └── index.html
│ │ ├── 2024-08-30-poor-mans-signals/
│ │ │ ├── adder.html
│ │ │ ├── adder.js
│ │ │ ├── index.html
│ │ │ ├── preact-example.js
│ │ │ ├── signals.js
│ │ │ ├── signals1-use.js
│ │ │ ├── signals1.js
│ │ │ ├── signals2-use.js
│ │ │ ├── signals2.js
│ │ │ ├── signals3-use.js
│ │ │ └── signals3.js
│ │ ├── 2024-09-03-unix-philosophy/
│ │ │ ├── adder.svelte
│ │ │ ├── bind.js
│ │ │ ├── bind1.js
│ │ │ ├── bind2-partial.js
│ │ │ ├── bind3-partial.js
│ │ │ ├── bind4-partial.js
│ │ │ ├── example-bind3/
│ │ │ │ ├── bind.js
│ │ │ │ ├── example.html
│ │ │ │ ├── example.js
│ │ │ │ └── signals.js
│ │ │ ├── example-combined/
│ │ │ │ ├── adder.js
│ │ │ │ ├── bind.js
│ │ │ │ ├── example.html
│ │ │ │ ├── html.js
│ │ │ │ └── signals.js
│ │ │ └── index.html
│ │ ├── 2024-09-06-how-fast-are-web-components/
│ │ │ └── index.html
│ │ ├── 2024-09-09-sweet-suspense/
│ │ │ ├── error-boundary-partial.html
│ │ │ ├── error-boundary.js
│ │ │ ├── example/
│ │ │ │ ├── components/
│ │ │ │ │ ├── error-boundary.js
│ │ │ │ │ ├── error-message.js
│ │ │ │ │ ├── hello-world/
│ │ │ │ │ │ ├── hello-world.js
│ │ │ │ │ │ └── later.js
│ │ │ │ │ ├── lazy.js
│ │ │ │ │ └── suspense.js
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── index.html
│ │ │ ├── lazy1.js
│ │ │ ├── lazy2-partial.js
│ │ │ ├── lazy3-partial.js
│ │ │ ├── suspense1-partial.html
│ │ │ ├── suspense1.js
│ │ │ └── suspense2-partial.js
│ │ ├── 2024-09-16-life-and-times-of-a-custom-element/
│ │ │ ├── defined/
│ │ │ │ └── example.html
│ │ │ ├── defined2/
│ │ │ │ └── example.html
│ │ │ ├── index.html
│ │ │ ├── observer/
│ │ │ │ └── example.html
│ │ │ ├── shadowed/
│ │ │ │ └── example.html
│ │ │ └── undefined/
│ │ │ ├── example.css
│ │ │ └── example.html
│ │ ├── 2024-09-28-unreasonable-effectiveness-of-vanilla-js/
│ │ │ ├── complete/
│ │ │ │ ├── AddTask.js
│ │ │ │ ├── App.js
│ │ │ │ ├── TaskList.js
│ │ │ │ ├── TasksContext.js
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── styles.css
│ │ │ ├── index.html
│ │ │ └── react/
│ │ │ ├── package.json
│ │ │ ├── public/
│ │ │ │ └── index.html
│ │ │ └── src/
│ │ │ ├── AddTask.js
│ │ │ ├── App.js
│ │ │ ├── TaskList.js
│ │ │ ├── TasksContext.js
│ │ │ ├── index.js
│ │ │ └── styles.css
│ │ ├── 2024-09-30-lived-experience/
│ │ │ └── index.html
│ │ ├── 2024-10-07-needs-more-context/
│ │ │ ├── combined/
│ │ │ │ ├── context-provider.js
│ │ │ │ ├── context-request.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── theme-context.js
│ │ │ │ └── theme-provider.js
│ │ │ ├── context-provider.js
│ │ │ ├── context-request-1.js
│ │ │ ├── context-request-2.js
│ │ │ ├── context-request-3.js
│ │ │ ├── context-request-4.js
│ │ │ ├── index.html
│ │ │ ├── theme-context-fragment.html
│ │ │ ├── theme-context.js
│ │ │ └── theme-provider.js
│ │ ├── 2024-10-20-editing-plain-vanilla/
│ │ │ ├── .hintrc
│ │ │ ├── eslint.config.cjs
│ │ │ └── index.html
│ │ ├── 2024-12-16-caching-vanilla-sites/
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ ├── 2025-01-01-new-years-resolve/
│ │ │ ├── example-index.js
│ │ │ ├── index.html
│ │ │ ├── layout.js
│ │ │ └── layout.tsx
│ │ ├── 2025-04-21-attribute-property-duality/
│ │ │ ├── demo1.html
│ │ │ ├── demo1.js
│ │ │ ├── demo2.html
│ │ │ ├── demo2.js
│ │ │ ├── demo3.html
│ │ │ ├── demo3.js
│ │ │ ├── demo4.html
│ │ │ ├── demo4.js
│ │ │ ├── demo5-before.js
│ │ │ ├── demo5.html
│ │ │ ├── demo5.js
│ │ │ └── index.html
│ │ ├── 2025-05-09-form-control/
│ │ │ ├── demo1/
│ │ │ │ ├── index-partial.txt
│ │ │ │ ├── index.html
│ │ │ │ └── input-inline.js
│ │ │ ├── demo2/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo3/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo4/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ └── input-inline.js
│ │ │ ├── demo5/
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline.css
│ │ │ │ └── input-inline.js
│ │ │ ├── demo6/
│ │ │ │ ├── index-partial.txt
│ │ │ │ ├── index.html
│ │ │ │ ├── input-inline-partial.js
│ │ │ │ ├── input-inline.css
│ │ │ │ └── input-inline.js
│ │ │ └── index.html
│ │ ├── 2025-06-12-view-transitions/
│ │ │ ├── example1/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── transitions.css
│ │ │ ├── example2/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── transitions.css
│ │ │ ├── example3/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ └── view-transition.js
│ │ │ ├── example4/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ └── view-transition.js
│ │ │ ├── example5/
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ ├── transitions.css
│ │ │ │ ├── view-transition-part.js
│ │ │ │ └── view-transition.js
│ │ │ ├── example6/
│ │ │ │ ├── index.html
│ │ │ │ ├── lib/
│ │ │ │ │ ├── html.js
│ │ │ │ │ ├── view-route.js
│ │ │ │ │ └── view-transition.js
│ │ │ │ └── src/
│ │ │ │ ├── App.js
│ │ │ │ ├── Details.js
│ │ │ │ ├── Home.js
│ │ │ │ ├── Icons.js
│ │ │ │ ├── Layout.js
│ │ │ │ ├── LikeButton.js
│ │ │ │ ├── Videos.js
│ │ │ │ ├── animations.css
│ │ │ │ ├── data.js
│ │ │ │ ├── index.js
│ │ │ │ └── styles.css
│ │ │ └── index.html
│ │ ├── 2025-06-25-routing/
│ │ │ ├── example1/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ └── view-route.js
│ │ │ ├── example2/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ ├── view-route-partial.js
│ │ │ │ ├── view-route-partial2.js
│ │ │ │ └── view-route.js
│ │ │ ├── example3/
│ │ │ │ ├── app.js
│ │ │ │ ├── index.html
│ │ │ │ ├── view-route-partial.js
│ │ │ │ └── view-route.js
│ │ │ ├── example4/
│ │ │ │ ├── 404.html
│ │ │ │ └── index-partial.html
│ │ │ └── index.html
│ │ ├── 2025-07-13-history-architecture/
│ │ │ └── index.html
│ │ ├── 2025-07-16-local-first-architecture/
│ │ │ ├── example1.html
│ │ │ └── index.html
│ │ ├── 2026-03-01-redesigning-plain-vanilla/
│ │ │ ├── index.html
│ │ │ └── nav-menu.html
│ │ ├── 2026-03-09-details-matters/
│ │ │ ├── example1/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example2/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example3/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example4/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ ├── example5/
│ │ │ │ ├── index-partial.html
│ │ │ │ └── index.html
│ │ │ └── index.html
│ │ └── index.json
│ ├── components/
│ │ ├── blog-archive.js
│ │ ├── blog-footer.js
│ │ ├── blog-header.js
│ │ └── blog-latest-posts.js
│ ├── example-base.css
│ ├── feed.xml
│ ├── generator.html
│ ├── generator.js
│ ├── index.css
│ ├── index.html
│ └── index.js
├── components/
│ ├── analytics/
│ │ └── analytics.js
│ ├── code-viewer/
│ │ ├── code-viewer.css
│ │ └── code-viewer.js
│ └── tab-panel/
│ ├── tab-panel.css
│ └── tab-panel.js
├── index.css
├── index.html
├── index.js
├── lib/
│ ├── html.js
│ └── speed-highlight/
│ ├── LICENSE
│ ├── common.js
│ ├── index.js
│ ├── languages/
│ │ ├── css.js
│ │ ├── html.js
│ │ ├── js.js
│ │ ├── js_template_literals.js
│ │ ├── jsdoc.js
│ │ ├── json.js
│ │ ├── log.js
│ │ ├── plain.js
│ │ ├── regex.js
│ │ ├── todo.js
│ │ ├── ts.js
│ │ ├── uri.js
│ │ └── xml.js
│ └── themes/
│ ├── default.css
│ ├── github-dark.css
│ └── github-light.css
├── manifest.json
├── pages/
│ ├── applications.html
│ ├── components.html
│ ├── examples/
│ │ ├── applications/
│ │ │ ├── counter/
│ │ │ │ ├── components/
│ │ │ │ │ └── counter.js
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── lifting-state-up/
│ │ │ │ ├── components/
│ │ │ │ │ ├── accordion.js
│ │ │ │ │ └── panel.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── react/
│ │ │ │ └── App.js
│ │ │ ├── passing-data-deeply/
│ │ │ │ ├── components/
│ │ │ │ │ ├── button.js
│ │ │ │ │ ├── panel.js
│ │ │ │ │ └── theme-context.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ └── tiny-context.js
│ │ │ └── single-page/
│ │ │ ├── app/
│ │ │ │ └── App.js
│ │ │ ├── components/
│ │ │ │ └── route/
│ │ │ │ └── route.js
│ │ │ ├── index.css
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ ├── components/
│ │ │ ├── adding-children/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ ├── avatar.js
│ │ │ │ │ ├── badge.css
│ │ │ │ │ └── badge.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── advanced/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ └── avatar.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── simple.html
│ │ │ ├── data/
│ │ │ │ ├── components/
│ │ │ │ │ ├── app.js
│ │ │ │ │ ├── form.js
│ │ │ │ │ ├── list-safe.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ └── summary.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ ├── shadow-dom/
│ │ │ │ ├── components/
│ │ │ │ │ ├── avatar.css
│ │ │ │ │ ├── avatar.js
│ │ │ │ │ ├── badge.css
│ │ │ │ │ ├── badge.js
│ │ │ │ │ ├── header.css
│ │ │ │ │ └── header.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── reset.css
│ │ │ └── simple/
│ │ │ ├── hello-world.js
│ │ │ └── index.html
│ │ ├── sites/
│ │ │ ├── importmap/
│ │ │ │ ├── components/
│ │ │ │ │ └── metrics.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ ├── dayjs/
│ │ │ │ │ ├── module.js
│ │ │ │ │ └── relativeTime.js
│ │ │ │ └── web-vitals.js
│ │ │ ├── imports/
│ │ │ │ ├── components/
│ │ │ │ │ └── metrics.js
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ ├── index.js
│ │ │ │ └── lib/
│ │ │ │ ├── dayjs/
│ │ │ │ │ └── relativeTime.js
│ │ │ │ ├── imports.js
│ │ │ │ └── web-vitals.js
│ │ │ └── page/
│ │ │ ├── example.html
│ │ │ ├── example2.html
│ │ │ └── index.js
│ │ └── styling/
│ │ ├── replacing-css-modules/
│ │ │ ├── nextjs/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── styles.module.css
│ │ │ └── vanilla/
│ │ │ ├── layout.js
│ │ │ └── styles.css
│ │ ├── scoping-prefixed/
│ │ │ ├── components/
│ │ │ │ └── example/
│ │ │ │ ├── example.css
│ │ │ │ ├── example.js
│ │ │ │ └── example_nested.css
│ │ │ ├── index.css
│ │ │ ├── index.html
│ │ │ └── index.js
│ │ └── scoping-shadowed/
│ │ ├── components/
│ │ │ └── example/
│ │ │ ├── example.css
│ │ │ └── example.js
│ │ ├── index.html
│ │ └── index.js
│ ├── sites.html
│ └── styling.html
├── robots.txt
├── sitemap.txt
├── styles/
│ ├── global.css
│ ├── reset.css
│ └── variables.css
└── tests/
├── imports-test.js
├── index.html
├── index.js
├── lib/
│ ├── @testing-library/
│ │ └── dom.umd.js
│ └── mocha/
│ ├── chai.js
│ ├── mocha.css
│ └── mocha.js
└── tabpanel.test.js
SYMBOL INDEX (1411 symbols across 132 files)
FILE: public/blog/articles/2024-08-17-lets-build-a-blog/generator.js
method processArticle (line 5) | async processArticle(article, path) {
method processArticleContent (line 25) | async processArticleContent(main, path) {
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example1.js
class MyComponent (line 1) | class MyComponent extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example2.js
function htmlEncode (line 1) | function htmlEncode(s) {
class MyComponent (line 12) | class MyComponent extends HTMLElement {
method connectedCallback (line 13) | connectedCallback() {
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/example3.js
class MyComponent (line 3) | class MyComponent extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
FILE: public/blog/articles/2024-08-25-vanilla-entity-encoding/html.js
class Html (line 1) | class Html extends String { }
FILE: public/blog/articles/2024-08-30-poor-mans-signals/adder.js
method connectedCallback (line 8) | connectedCallback() {
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals.js
class Signal (line 1) | class Signal extends EventTarget {
method value (line 3) | get value () { return this.#value; }
method value (line 4) | set value (value) {
method constructor (line 10) | constructor (value) {
method effect (line 15) | effect(fn) {
method valueOf (line 21) | valueOf () { return this.#value; }
method toString (line 22) | toString () { return String(this.#value); }
class Computed (line 25) | class Computed extends Signal {
method constructor (line 26) | constructor (fn, deps) {
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals1.js
class Signal (line 1) | class Signal extends EventTarget {
method value (line 3) | get value () { return this.#value; }
method value (line 4) | set value (value) {
method constructor (line 10) | constructor (value) {
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals2.js
class Signal (line 1) | class Signal extends EventTarget {
method value (line 3) | get value () { return this.#value; }
method value (line 4) | set value (value) {
method constructor (line 10) | constructor (value) {
method effect (line 15) | effect(fn) {
method valueOf (line 21) | valueOf () { return this.#value; }
method toString (line 22) | toString () { return String(this.#value); }
FILE: public/blog/articles/2024-08-30-poor-mans-signals/signals3.js
class Computed (line 1) | class Computed extends Signal {
method constructor (line 2) | constructor (fn, deps) {
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind.js
function getPropertyForAttribute (line 83) | function getPropertyForAttribute(name, obj) {
FILE: public/blog/articles/2024-09-03-unix-philosophy/bind4-partial.js
function getPropertyForAttribute (line 22) | function getPropertyForAttribute(name, obj) {
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/example.js
method a (line 6) | set a(value) {
method b (line 10) | set b(value) {
method connectedCallback (line 16) | connectedCallback() {
method onInputA (line 37) | onInputA (e) {
method onClick (line 41) | onClick() {
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-bind3/signals.js
class Signal (line 1) | class Signal extends EventTarget {
method value (line 3) | get value () { return this.#value; }
method value (line 4) | set value (value) {
method constructor (line 10) | constructor (value) {
method effect (line 15) | effect(fn) {
method valueOf (line 21) | valueOf () { return this.#value; }
method toString (line 22) | toString () { return String(this.#value); }
class Computed (line 25) | class Computed extends Signal {
method constructor (line 26) | constructor (fn, deps) {
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/adder.js
method connectedCallback (line 11) | connectedCallback() {
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/bind.js
function getPropertyForAttribute (line 81) | function getPropertyForAttribute(name, obj) {
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/html.js
class Html (line 1) | class Html extends String { }
FILE: public/blog/articles/2024-09-03-unix-philosophy/example-combined/signals.js
class Signal (line 1) | class Signal extends EventTarget {
method value (line 3) | get value () { return this.#value; }
method value (line 4) | set value (value) {
method constructor (line 10) | constructor (value) {
method effect (line 15) | effect(fn) {
method valueOf (line 21) | valueOf () { return this.#value; }
method toString (line 22) | toString () { return String(this.#value); }
class Computed (line 25) | class Computed extends Signal {
method constructor (line 26) | constructor (fn, deps) {
FILE: public/blog/articles/2024-09-09-sweet-suspense/error-boundary.js
class ErrorBoundary (line 1) | class ErrorBoundary extends HTMLElement {
method showError (line 3) | static showError(sender, error) {
method error (line 18) | get error() {
method error (line 22) | set error(error) {
method constructor (line 39) | constructor() {
method reset (line 52) | reset() {
method connectedCallback (line 56) | connectedCallback() {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/error-boundary.js
class ErrorBoundary (line 16) | class ErrorBoundary extends HTMLElement {
method showError (line 23) | static showError(sender, error) {
method error (line 38) | get error() {
method error (line 42) | set error(error) {
method constructor (line 59) | constructor() {
method reset (line 72) | reset() {
method connectedCallback (line 76) | connectedCallback() {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/error-message.js
class ErrorMessage (line 1) | class ErrorMessage extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
method observedAttributes (line 6) | static get observedAttributes() {
method attributeChangedCallback (line 10) | attributeChangedCallback() {
method update (line 14) | update() {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/hello-world.js
class HelloWorldComponent (line 4) | class HelloWorldComponent extends HTMLElement {
method connectedCallback (line 5) | connectedCallback() {
function register (line 24) | function register() {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/later.js
function later (line 1) | function later(delay) {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/lazy.js
class Lazy (line 19) | class Lazy extends HTMLElement {
method connectedCallback (line 20) | connectedCallback() {
method #loadLazy (line 28) | #loadLazy() {
method #loadElement (line 45) | #loadElement(element) {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/components/suspense.js
class Suspense (line 18) | class Suspense extends HTMLElement {
method waitFor (line 25) | static waitFor(sender, ...promises) {
method #loading (line 34) | set #loading(isLoading) {
method constructor (line 40) | constructor() {
method connectedCallback (line 51) | connectedCallback() {
method addPromises (line 59) | addPromises(...promises) {
FILE: public/blog/articles/2024-09-09-sweet-suspense/example/index.js
method constructor (line 8) | constructor() {
method connectedCallback (line 16) | connectedCallback() {
FILE: public/blog/articles/2024-09-09-sweet-suspense/lazy1.js
method connectedCallback (line 2) | connectedCallback() {
method #loadLazy (line 7) | #loadLazy() {
method #loadElement (line 15) | #loadElement(element) {
FILE: public/blog/articles/2024-09-09-sweet-suspense/suspense1.js
class Suspense (line 1) | class Suspense extends HTMLElement {
method loading (line 5) | set loading(isLoading) {
method constructor (line 11) | constructor() {
method connectedCallback (line 22) | connectedCallback() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/AddTask.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/App.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TaskList.js
method context (line 2) | get context() { return this.closest('tasks-context'); }
method connectedCallback (line 4) | connectedCallback() {
method update (line 10) | update() {
method task (line 37) | set task(task) { this.#task = task; this.update(); }
method context (line 38) | get context() { return this.closest('tasks-context'); }
method connectedCallback (line 40) | connectedCallback() {
method update (line 88) | update() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TasksContext.js
method tasks (line 3) | get tasks() { return this.#tasks; }
method tasks (line 4) | set tasks(tasks) {
method dispatch (line 9) | dispatch(action) {
method connectedCallback (line 13) | connectedCallback() {
function tasksReducer (line 18) | function tasksReducer(tasks, action) {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/AddTask.js
function AddTask (line 4) | function AddTask() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/App.js
function TaskApp (line 5) | function TaskApp() {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TaskList.js
function TaskList (line 4) | function TaskList() {
function Task (line 17) | function Task({ task }) {
FILE: public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TasksContext.js
function TasksProvider (line 7) | function TasksProvider({ children }) {
function useTasks (line 22) | function useTasks() {
function useTasksDispatch (line 26) | function useTasksDispatch() {
function tasksReducer (line 30) | function tasksReducer(tasks, action) {
FILE: public/blog/articles/2024-10-07-needs-more-context/combined/context-provider.js
class ContextProvider (line 1) | class ContextProvider extends EventTarget {
method value (line 3) | get value() { return this.#value }
method value (line 4) | set value(v) { this.#value = v; this.dispatchEvent(new Event('change')...
method context (line 7) | get context() { return this.#context }
method constructor (line 9) | constructor(target, context, initialValue = undefined) {
method attach (line 17) | attach(target) {
method detach (line 21) | detach(target) {
method handle (line 29) | handle(e) {
FILE: public/blog/articles/2024-10-07-needs-more-context/combined/context-request.js
class ContextRequestEvent (line 1) | class ContextRequestEvent extends Event {
method constructor (line 2) | constructor(context, callback, subscribe) {
FILE: public/blog/articles/2024-10-07-needs-more-context/combined/index.js
method connectedCallback (line 6) | connectedCallback() {
method connectedCallback (line 24) | connectedCallback() {
method disconnectedCallback (line 31) | disconnectedCallback() {
method connectedCallback (line 39) | connectedCallback() {
method disconnectedCallback (line 50) | disconnectedCallback() {
function reparent (line 55) | function reparent() {
FILE: public/blog/articles/2024-10-07-needs-more-context/combined/theme-context.js
method connectedCallback (line 8) | connectedCallback() {
FILE: public/blog/articles/2024-10-07-needs-more-context/context-provider.js
class ContextProvider (line 1) | class ContextProvider extends EventTarget {
method value (line 3) | get value() { return this.#value }
method value (line 4) | set value(v) { this.#value = v; this.dispatchEvent(new Event('change')...
method context (line 7) | get context() { return this.#context }
method constructor (line 9) | constructor(target, context, initialValue = undefined) {
method attach (line 17) | attach(target) {
method detach (line 21) | detach(target) {
method handle (line 29) | handle(e) {
FILE: public/blog/articles/2024-10-07-needs-more-context/context-request-1.js
class ContextRequestEvent (line 1) | class ContextRequestEvent extends Event {
method constructor (line 2) | constructor(context, callback, subscribe) {
method connectedCallback (line 14) | connectedCallback() {
FILE: public/blog/articles/2024-10-07-needs-more-context/context-request-2.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2024-10-07-needs-more-context/context-request-3.js
method connectedCallback (line 3) | connectedCallback() {
method disconnectedCallback (line 11) | disconnectedCallback() {
FILE: public/blog/articles/2024-10-07-needs-more-context/context-request-4.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2024-10-07-needs-more-context/theme-context.js
method connectedCallback (line 6) | connectedCallback() {
FILE: public/blog/articles/2025-01-01-new-years-resolve/layout.js
class Layout (line 1) | class Layout extends HTMLElement {
method constructor (line 2) | constructor() {
FILE: public/blog/articles/2025-01-01-new-years-resolve/layout.tsx
function Layout (line 3) | function Layout({
FILE: public/blog/articles/2025-04-21-attribute-property-duality/demo1.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2025-04-21-attribute-property-duality/demo2.js
method connectedCallback (line 2) | connectedCallback() {
FILE: public/blog/articles/2025-04-21-attribute-property-duality/demo3.js
method value (line 2) | get value() {
method value (line 5) | set value(v) {
method attributeChangedCallback (line 10) | attributeChangedCallback() {
FILE: public/blog/articles/2025-04-21-attribute-property-duality/demo4.js
method value (line 2) | get value() {
method value (line 5) | set value(v) {
method glam (line 9) | get glam() {
method glam (line 12) | set glam(v) {
method attributeChangedCallback (line 21) | attributeChangedCallback() {
FILE: public/blog/articles/2025-04-21-attribute-property-duality/demo5.js
method value (line 2) | get value() {
method value (line 5) | set value(v) {
method glam (line 9) | get glam() {
method glam (line 12) | set glam(v) {
method attributeChangedCallback (line 21) | attributeChangedCallback() {
method connectedCallback (line 27) | connectedCallback() {
method #upgradeProperty (line 32) | #upgradeProperty(prop) {
FILE: public/blog/articles/2025-05-09-form-control/demo1/input-inline.js
method value (line 3) | get value() {
method value (line 6) | set value(value) {
method name (line 10) | get name() {
method name (line 13) | set name(v) {
method connectedCallback (line 17) | connectedCallback() {
method attributeChangedCallback (line 22) | attributeChangedCallback() {
method #update (line 26) | #update() {
FILE: public/blog/articles/2025-05-09-form-control/demo2/input-inline-partial.js
method constructor (line 7) | constructor() {
method #update (line 15) | #update() {
FILE: public/blog/articles/2025-05-09-form-control/demo2/input-inline.js
method value (line 5) | get value() {
method value (line 8) | set value(value) {
method name (line 12) | get name() {
method name (line 15) | set name(v) {
method constructor (line 19) | constructor() {
method connectedCallback (line 25) | connectedCallback() {
method attributeChangedCallback (line 30) | attributeChangedCallback() {
method #update (line 34) | #update() {
FILE: public/blog/articles/2025-05-09-form-control/demo3/input-inline-partial.js
method constructor (line 7) | constructor() {
method handleEvent (line 15) | handleEvent(e) {
function cleanTextContent (line 59) | function cleanTextContent(text) {
FILE: public/blog/articles/2025-05-09-form-control/demo3/input-inline.js
method value (line 6) | get value() {
method value (line 9) | set value(value) {
method name (line 13) | get name() {
method name (line 16) | set name(v) {
method constructor (line 20) | constructor() {
method handleEvent (line 31) | handleEvent(e) {
method connectedCallback (line 72) | connectedCallback() {
method attributeChangedCallback (line 77) | attributeChangedCallback() {
method #update (line 81) | #update() {
function cleanTextContent (line 93) | function cleanTextContent(text) {
FILE: public/blog/articles/2025-05-09-form-control/demo4/input-inline-partial.js
method value (line 8) | set value(v) {
method value (line 14) | get value() {
method defaultValue (line 18) | get defaultValue() {
method defaultValue (line 21) | set defaultValue(value) {
method disabled (line 25) | set disabled(v) {
method disabled (line 32) | get disabled() {
method readOnly (line 36) | set readOnly(v) {
method readOnly (line 43) | get readOnly() {
method attributeChangedCallback (line 50) | attributeChangedCallback() {
method #update (line 54) | #update() {
method formResetCallback (line 68) | formResetCallback() {
method formDisabledCallback (line 73) | formDisabledCallback(disabled) {
method formStateRestoreCallback (line 78) | formStateRestoreCallback(state) {
FILE: public/blog/articles/2025-05-09-form-control/demo4/input-inline.js
method value (line 8) | set value(v) {
method value (line 14) | get value() {
method defaultValue (line 18) | get defaultValue() {
method defaultValue (line 21) | set defaultValue(value) {
method disabled (line 25) | set disabled(v) {
method disabled (line 32) | get disabled() {
method readOnly (line 36) | set readOnly(v) {
method readOnly (line 43) | get readOnly() {
method name (line 47) | get name() {
method name (line 50) | set name(v) {
method constructor (line 54) | constructor() {
method handleEvent (line 65) | handleEvent(e) {
method connectedCallback (line 106) | connectedCallback() {
method attributeChangedCallback (line 111) | attributeChangedCallback() {
method #update (line 115) | #update() {
method formResetCallback (line 131) | formResetCallback() {
method formDisabledCallback (line 136) | formDisabledCallback(disabled) {
method formStateRestoreCallback (line 141) | formStateRestoreCallback(state) {
function cleanTextContent (line 147) | function cleanTextContent(text) {
FILE: public/blog/articles/2025-05-09-form-control/demo5/input-inline.js
method value (line 8) | set value(v) {
method value (line 14) | get value() {
method defaultValue (line 18) | get defaultValue() {
method defaultValue (line 21) | set defaultValue(value) {
method disabled (line 25) | set disabled(v) {
method disabled (line 32) | get disabled() {
method readOnly (line 36) | set readOnly(v) {
method readOnly (line 43) | get readOnly() {
method name (line 47) | get name() {
method name (line 50) | set name(v) {
method constructor (line 54) | constructor() {
method handleEvent (line 65) | handleEvent(e) {
method connectedCallback (line 106) | connectedCallback() {
method attributeChangedCallback (line 111) | attributeChangedCallback() {
method #update (line 115) | #update() {
method formResetCallback (line 134) | formResetCallback() {
method formDisabledCallback (line 139) | formDisabledCallback(disabled) {
method formStateRestoreCallback (line 144) | formStateRestoreCallback(state) {
function cleanTextContent (line 150) | function cleanTextContent(text) {
FILE: public/blog/articles/2025-05-09-form-control/demo6/input-inline-partial.js
constant VALUE_MISSING_MESSAGE (line 1) | let VALUE_MISSING_MESSAGE = 'Please fill out this field.';
method required (line 19) | set required(v) {
method required (line 26) | get required() {
method attributeChangedCallback (line 33) | attributeChangedCallback() {
method #update (line 37) | #update() {
method #updateValidity (line 46) | #updateValidity() {
method checkValidity (line 79) | checkValidity() {
method reportValidity (line 84) | reportValidity() {
method setCustomValidity (line 89) | setCustomValidity(message) {
method validity (line 94) | get validity() {
method validationMessage (line 98) | get validationMessage() {
method willValidate (line 102) | get willValidate() {
FILE: public/blog/articles/2025-05-09-form-control/demo6/input-inline.js
constant VALUE_MISSING_MESSAGE (line 1) | let VALUE_MISSING_MESSAGE = 'Please fill out this field.';
method value (line 19) | set value(v) {
method value (line 25) | get value() {
method defaultValue (line 29) | get defaultValue() {
method defaultValue (line 32) | set defaultValue(value) {
method disabled (line 36) | set disabled(v) {
method disabled (line 43) | get disabled() {
method readOnly (line 47) | set readOnly(v) {
method readOnly (line 54) | get readOnly() {
method name (line 58) | get name() {
method name (line 61) | set name(v) {
method required (line 65) | set required(v) {
method required (line 72) | get required() {
method constructor (line 76) | constructor() {
method handleEvent (line 87) | handleEvent(e) {
method connectedCallback (line 128) | connectedCallback() {
method attributeChangedCallback (line 133) | attributeChangedCallback() {
method #update (line 137) | #update() {
method formResetCallback (line 159) | formResetCallback() {
method formDisabledCallback (line 164) | formDisabledCallback(disabled) {
method formStateRestoreCallback (line 169) | formStateRestoreCallback(state) {
method #updateValidity (line 174) | #updateValidity() {
method checkValidity (line 207) | checkValidity() {
method reportValidity (line 212) | reportValidity() {
method setCustomValidity (line 217) | setCustomValidity(message) {
method validity (line 222) | get validity() {
method validationMessage (line 226) | get validationMessage() {
method willValidate (line 230) | get willValidate() {
function cleanTextContent (line 235) | function cleanTextContent(text) {
FILE: public/blog/articles/2025-06-12-view-transitions/example1/index.js
function transition (line 1) | function transition() {
FILE: public/blog/articles/2025-06-12-view-transitions/example2/index.js
function transition (line 1) | function transition() {
FILE: public/blog/articles/2025-06-12-view-transitions/example3/index.js
function transition (line 3) | function transition() {
FILE: public/blog/articles/2025-06-12-view-transitions/example4/index.js
function navigate (line 5) | function navigate() {
function updateRoute1 (line 11) | function updateRoute1() {
function updateRoute2 (line 21) | function updateRoute2() {
function load (line 37) | function load() {
FILE: public/blog/articles/2025-06-12-view-transitions/example5/index.js
function navigate (line 5) | function navigate() {
function updateRoute1 (line 11) | function updateRoute1() {
function updateRoute2 (line 21) | function updateRoute2() {
function load (line 37) | function load() {
FILE: public/blog/articles/2025-06-12-view-transitions/example5/view-transition.js
class QueueingViewTransition (line 62) | class QueueingViewTransition {
method id (line 70) | get id() { return this.#id; }
method constructor (line 79) | constructor() {
method addCallback (line 86) | addCallback(updateCallback) {
method run (line 93) | run(skipTransition = false) {
method updateCallbackDone (line 125) | get updateCallbackDone() { return this.#updateCallbackDone.promise }
method ready (line 127) | get ready() { return this.#ready.promise }
method finished (line 129) | get finished() { return this.#finished.promise }
method skipTransition (line 131) | skipTransition() {
function promiseTry (line 141) | function promiseTry(fn) {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/lib/html.js
class Html (line 1) | class Html extends String { }
FILE: public/blog/articles/2025-06-12-view-transitions/example6/lib/view-route.js
method isActive (line 26) | get isActive() {
method matches (line 31) | get matches() {
method connectedCallback (line 35) | connectedCallback() {
method disconnectedCallback (line 41) | disconnectedCallback() {
method handleEvent (line 45) | handleEvent(e) {
method observedAttributes (line 49) | static get observedAttributes() {
method attributeChangedCallback (line 53) | attributeChangedCallback() {
method update (line 57) | update() {
method setMatches (line 68) | setMatches(matches) {
method matchesRoute (line 73) | matchesRoute(path, exact) {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/lib/view-transition.js
method name (line 6) | get name() { return this.getAttribute('name') }
method name (line 7) | set name(v) { this.setAttribute('name', v); }
method observedAttributes (line 9) | static get observedAttributes() { return ['name'] }
method attributeChangedCallback (line 10) | attributeChangedCallback() { this.update(); }
method connectedCallback (line 12) | connectedCallback() { this.update(); }
method disconnectedCallback (line 14) | disconnectedCallback() { this.updateShadowRule(false); }
method update (line 16) | update() {
method updateShadowRule (line 22) | updateShadowRule(insert = true) {
class QueueingViewTransition (line 129) | class QueueingViewTransition {
method id (line 138) | get id() { return this.#id; }
method constructor (line 147) | constructor(transitionType) {
method addCallback (line 162) | addCallback(updateCallback) {
method run (line 170) | run(skipTransition = false) {
method updateCallbackDone (line 210) | get updateCallbackDone() { return this.#updateCallbackDone.promise }
method ready (line 212) | get ready() { return this.#ready.promise }
method finished (line 214) | get finished() { return this.#finished.promise }
method skipTransition (line 216) | skipTransition() {
function promiseTry (line 226) | function promiseTry(fn) {
function log (line 233) | function log(...args) {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/App.js
method connectedCallback (line 6) | connectedCallback() {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/Details.js
method connectedCallback (line 8) | connectedCallback() {
method handleEvent (line 29) | handleEvent(e) {
method update (line 35) | update(id) {
method update (line 55) | async update(id) {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/Home.js
method connectedCallback (line 13) | connectedCallback() {
method handleEvent (line 33) | handleEvent(e) { this.update(); }
method update (line 35) | update() {
method text (line 43) | get text() { return this.#text }
method text (line 44) | set text(v) { if (this.#text !== v) { this.#text = v; this.update(); } }
method connectedCallback (line 46) | connectedCallback() {
method update (line 69) | update() {
method update (line 75) | update(videos, text) {
function filterVideos (line 99) | function filterVideos(videos, query) {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/Icons.js
function ChevronLeft (line 1) | function ChevronLeft() {
function PauseIcon (line 22) | function PauseIcon() {
function PlayIcon (line 42) | function PlayIcon() {
function Heart (line 61) | function Heart({liked, animate}) {
function IconSearch (line 100) | function IconSearch() {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/Layout.js
method constructor (line 4) | constructor() {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/LikeButton.js
method connectedCallback (line 9) | connectedCallback() {
method update (line 13) | update() {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/Videos.js
method update (line 6) | update(video) {
method connectedCallback (line 30) | connectedCallback() {
method handleEvent (line 39) | handleEvent(e) {
method update (line 45) | update() {
FILE: public/blog/articles/2025-06-12-view-transitions/example6/src/data.js
constant VIDEO_DELAY (line 43) | const VIDEO_DELAY = 1;
constant VIDEO_DETAILS_DELAY (line 44) | const VIDEO_DETAILS_DELAY = 1000;
function fetchVideos (line 45) | function fetchVideos() {
function fetchVideo (line 58) | function fetchVideo(id) {
function fetchVideoDetails (line 71) | function fetchVideoDetails(id) {
FILE: public/blog/articles/2025-06-25-routing/example1/app.js
method constructor (line 7) | constructor() {
method connectedCallback (line 16) | connectedCallback() {
method update (line 20) | update() {
FILE: public/blog/articles/2025-06-25-routing/example2/app.js
method constructor (line 7) | constructor() {
method connectedCallback (line 19) | connectedCallback() {
method update (line 23) | update() {
FILE: public/blog/articles/2025-06-25-routing/example3/app.js
method constructor (line 5) | constructor() {
method connectedCallback (line 10) | connectedCallback() {
FILE: public/blog/articles/2025-06-25-routing/example3/view-route-partial.js
method isActive (line 7) | get isActive() {
method matches (line 11) | get matches() {
method matches (line 15) | set matches(v) {
method connectedCallback (line 23) | connectedCallback() {
method disconnectedCallback (line 28) | disconnectedCallback() {
method handleEvent (line 32) | handleEvent(e) {
method observedAttributes (line 36) | static get observedAttributes() {
method attributeChangedCallback (line 40) | attributeChangedCallback() {
method update (line 44) | update() {
method matchesRoute (line 49) | matchesRoute(path) {
FILE: public/blog/articles/2025-06-25-routing/example3/view-route.js
method isActive (line 47) | get isActive() {
method matches (line 51) | get matches() {
method matches (line 55) | set matches(v) {
method connectedCallback (line 63) | connectedCallback() {
method disconnectedCallback (line 68) | disconnectedCallback() {
method handleEvent (line 72) | handleEvent(e) {
method observedAttributes (line 76) | static get observedAttributes() {
method attributeChangedCallback (line 80) | attributeChangedCallback() {
method update (line 84) | update() {
method matchesRoute (line 89) | matchesRoute(path) {
FILE: public/blog/components/blog-archive.js
class BlogArchive (line 3) | class BlogArchive extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
FILE: public/blog/components/blog-footer.js
class BlogFooter (line 3) | class BlogFooter extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
FILE: public/blog/components/blog-header.js
class BlogHeader (line 3) | class BlogHeader extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
FILE: public/blog/components/blog-latest-posts.js
class LatestPosts (line 5) | class LatestPosts extends HTMLElement {
method connectedCallback (line 6) | connectedCallback() {
FILE: public/blog/generator.js
constant BLOG_BASE_URL (line 3) | const BLOG_BASE_URL = 'https://plainvanillaweb.com/blog/';
constant ATOM_FEED_XML (line 5) | const ATOM_FEED_XML = `<?xml version="1.0" encoding="UTF-8"?>
constant ATOM_FEED_LENGTH (line 20) | const ATOM_FEED_LENGTH = 20;
method reset (line 27) | reset() {
method showError (line 33) | showError(text) {
method connectedCallback (line 38) | connectedCallback() {
method addClickListener (line 44) | addClickListener() {
method addDragListeners (line 63) | addDragListeners() {
method startProcessing (line 88) | async startProcessing(blogFolder) {
method processArticle (line 125) | async processArticle(article, path) {
method processArticleContent (line 146) | async processArticleContent(main, path) {
method downloadFile (line 187) | async downloadFile(file, path) {
method addMessage (line 194) | addMessage(text, className) {
method addFeedBlock (line 201) | addFeedBlock() {
method addIndexJsonBlock (line 234) | addIndexJsonBlock() {
method addSitemapBlock (line 252) | addSitemapBlock() {
function toISODate (line 265) | function toISODate(date) {
FILE: public/components/analytics/analytics.js
class AnalyticsComponent (line 1) | class AnalyticsComponent extends HTMLElement {
method constructor (line 5) | constructor() {
method connectedCallback (line 16) | connectedCallback() {
method update (line 20) | update() {
FILE: public/components/code-viewer/code-viewer.js
class CodeViewer (line 10) | class CodeViewer extends HTMLElement {
method connectedCallback (line 11) | connectedCallback() {
method observedAttributes (line 31) | static get observedAttributes() {
method attributeChangedCallback (line 35) | attributeChangedCallback() {
method update (line 39) | update() {
FILE: public/components/tab-panel/tab-panel.js
class TabPanel (line 16) | class TabPanel extends HTMLElement {
method tablist (line 21) | get tablist() { return this.#tablist; }
method tabpanels (line 22) | get tabpanels() { return this.querySelectorAll('x-tab'); }
method constructor (line 24) | constructor() {
method connectedCallback (line 30) | connectedCallback() {
method onMutation (line 36) | onMutation(m) {
method activatePanel (line 55) | activatePanel(id) {
method update (line 66) | update() {
class Tab (line 74) | class Tab extends HTMLElement {
method connectedCallback (line 78) | connectedCallback() {
FILE: public/lib/html.js
class Html (line 1) | class Html extends String { }
FILE: public/lib/speed-highlight/index.js
function tokenize (line 60) | async function tokenize(src, lang, token) {
function highlightText (line 127) | async function highlightText(src, lang, multiline = true, opt = {}) {
function highlightElement (line 146) | async function highlightElement(elm, lang = elm.className.match(/shj-lan...
FILE: public/lib/speed-highlight/languages/js_template_literals.js
method exec (line 4) | exec(str) {
FILE: public/pages/examples/applications/counter/components/counter.js
class Counter (line 1) | class Counter extends HTMLElement {
method increment (line 4) | increment() {
method connectedCallback (line 9) | connectedCallback() {
method update (line 13) | update() {
FILE: public/pages/examples/applications/lifting-state-up/components/accordion.js
class Accordion (line 1) | class Accordion extends HTMLElement {
method activeIndex (line 4) | get activeIndex () { return this.#activeIndex; }
method activeIndex (line 5) | set activeIndex(index) { this.#activeIndex = index; this.update(); }
method connectedCallback (line 7) | connectedCallback() {
method update (line 27) | update() {
FILE: public/pages/examples/applications/lifting-state-up/components/panel.js
class Panel (line 1) | class Panel extends HTMLElement {
method constructor (line 2) | constructor() {
method connectedCallback (line 7) | connectedCallback() {
method observedAttributes (line 20) | static get observedAttributes() { return ['title', 'active']; }
method attributeChangedCallback (line 22) | attributeChangedCallback() {
method update (line 26) | update() {
FILE: public/pages/examples/applications/lifting-state-up/react/App.js
function Accordion (line 3) | function Accordion() {
function Panel (line 26) | function Panel({
FILE: public/pages/examples/applications/passing-data-deeply/components/button.js
class ButtonComponent (line 3) | class ButtonComponent extends HTMLElement {
method constructor (line 4) | constructor() {
method connectedCallback (line 12) | connectedCallback() {
method disconnectedCallback (line 30) | disconnectedCallback() {
method update (line 34) | update() {
FILE: public/pages/examples/applications/passing-data-deeply/components/panel.js
class PanelComponent (line 3) | class PanelComponent extends HTMLElement {
method constructor (line 4) | constructor() {
method connectedCallback (line 12) | connectedCallback() {
method disconnectedCallback (line 28) | disconnectedCallback() {
method observedAttributes (line 32) | static get observedAttributes() {
method attributeChangedCallback (line 36) | attributeChangedCallback() {
method update (line 40) | update() {
FILE: public/pages/examples/applications/passing-data-deeply/components/theme-context.js
class ThemeContext (line 3) | class ThemeContext extends HTMLElement {
method connectedCallback (line 10) | connectedCallback() {
FILE: public/pages/examples/applications/passing-data-deeply/lib/tiny-context.js
class ContextRequestEvent (line 1) | class ContextRequestEvent extends Event {
method constructor (line 2) | constructor(context, callback, subscribe) {
class ContextProvider (line 13) | class ContextProvider extends EventTarget {
method value (line 15) | get value() { return this.#value }
method value (line 16) | set value(v) { this.#value = v; this.dispatchEvent(new Event('change')...
method context (line 19) | get context() { return this.#context }
method constructor (line 21) | constructor(target, context, initialValue = undefined) {
method attach (line 28) | attach(target) {
method detach (line 32) | detach(target) {
method handleEvent (line 40) | handleEvent(e) {
FILE: public/pages/examples/applications/single-page/app/App.js
class App (line 1) | class App extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
class AppLayout (line 35) | class AppLayout extends HTMLElement {
method connectedCallback (line 36) | connectedCallback() {
FILE: public/pages/examples/applications/single-page/components/route/route.js
class RouteComponent (line 10) | class RouteComponent extends HTMLElement {
method constructor (line 12) | constructor() {
method isActive (line 20) | get isActive() {
method connectedCallback (line 24) | connectedCallback() {
method disconnectedCallback (line 30) | disconnectedCallback() {
method observedAttributes (line 34) | static get observedAttributes() {
method attributeChangedCallback (line 38) | attributeChangedCallback() {
method update (line 42) | update() {
method setIsActive (line 52) | setIsActive(active) {
method routeChangedCallback (line 58) | routeChangedCallback(...matches) {}
method #matchesRoute (line 60) | #matchesRoute(path, exact) {
FILE: public/pages/examples/components/adding-children/components/avatar.js
class AvatarComponent (line 6) | class AvatarComponent extends HTMLElement {
method connectedCallback (line 7) | connectedCallback() {
method observedAttributes (line 14) | static get observedAttributes() {
method attributeChangedCallback (line 18) | attributeChangedCallback() {
method update (line 22) | update() {
FILE: public/pages/examples/components/adding-children/components/badge.js
class BadgeComponent (line 1) | class BadgeComponent extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
method update (line 13) | update() {
method observedAttributes (line 17) | static get observedAttributes() {
method attributeChangedCallback (line 21) | attributeChangedCallback() {
method content (line 25) | set content(value) {
method content (line 31) | get content() {
FILE: public/pages/examples/components/advanced/components/avatar.js
class AvatarComponent (line 6) | class AvatarComponent extends HTMLElement {
method connectedCallback (line 7) | connectedCallback() {
method observedAttributes (line 14) | static get observedAttributes() {
method attributeChangedCallback (line 18) | attributeChangedCallback() {
method update (line 22) | update() {
FILE: public/pages/examples/components/data/components/app.js
class SantasApp (line 1) | class SantasApp extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
method update (line 20) | update() {
FILE: public/pages/examples/components/data/components/form.js
class SantasForm (line 1) | class SantasForm extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
FILE: public/pages/examples/components/data/components/list-safe.js
class SantasList (line 3) | class SantasList extends HTMLElement {
method list (line 5) | set list(newList) {
method update (line 9) | update() {
FILE: public/pages/examples/components/data/components/list.js
class SantasList (line 1) | class SantasList extends HTMLElement {
method list (line 3) | set list(newList) {
method update (line 7) | update() {
FILE: public/pages/examples/components/data/components/summary.js
class SantasSummary (line 1) | class SantasSummary extends HTMLElement {
method update (line 2) | update(list) {
FILE: public/pages/examples/components/shadow-dom/components/avatar.js
class AvatarComponent (line 6) | class AvatarComponent extends HTMLElement {
method connectedCallback (line 7) | connectedCallback() {
method observedAttributes (line 14) | static get observedAttributes() {
method attributeChangedCallback (line 18) | attributeChangedCallback() {
method update (line 22) | update() {
FILE: public/pages/examples/components/shadow-dom/components/badge.js
class BadgeComponent (line 1) | class BadgeComponent extends HTMLElement {
method connectedCallback (line 4) | connectedCallback() {
method update (line 13) | update() {
method observedAttributes (line 17) | static get observedAttributes() {
method attributeChangedCallback (line 21) | attributeChangedCallback() {
method content (line 25) | set content(value) {
method content (line 31) | get content() {
FILE: public/pages/examples/components/shadow-dom/components/header.js
class HeaderComponent (line 10) | class HeaderComponent extends HTMLElement {
method constructor (line 11) | constructor() {
method update (line 20) | update() {
method observedAttributes (line 24) | static get observedAttributes() {
method attributeChangedCallback (line 28) | attributeChangedCallback() {
FILE: public/pages/examples/components/simple/hello-world.js
class HelloWorldComponent (line 1) | class HelloWorldComponent extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
FILE: public/pages/examples/sites/importmap/components/metrics.js
class MetricsComponent (line 4) | class MetricsComponent extends HTMLElement {
method connectedCallback (line 9) | connectedCallback() {
method disconnectedCallback (line 14) | disconnectedCallback() {
method update (line 19) | update() {
FILE: public/pages/examples/sites/importmap/lib/dayjs/relativeTime.js
function i (line 1) | function i(r,e,t,o){return n.fromToBase(r,e,t,o)}
FILE: public/pages/examples/sites/importmap/lib/web-vitals.js
method firstHiddenTime (line 1) | get firstHiddenTime(){return m}
FILE: public/pages/examples/sites/imports/components/metrics.js
class MetricsComponent (line 3) | class MetricsComponent extends HTMLElement {
method connectedCallback (line 8) | connectedCallback() {
method disconnectedCallback (line 13) | disconnectedCallback() {
method update (line 18) | update() {
FILE: public/pages/examples/sites/imports/lib/dayjs/relativeTime.js
function i (line 1) | function i(r,e,t,o){return n.fromToBase(r,e,t,o)}
FILE: public/pages/examples/sites/imports/lib/web-vitals.js
method firstHiddenTime (line 1) | get firstHiddenTime(){return m}
FILE: public/pages/examples/styling/replacing-css-modules/nextjs/layout.tsx
function DashboardLayout (line 3) | function DashboardLayout({
FILE: public/pages/examples/styling/replacing-css-modules/vanilla/layout.js
class Layout (line 1) | class Layout extends HTMLElement {
method constructor (line 2) | constructor() {
FILE: public/pages/examples/styling/scoping-prefixed/components/example/example.js
class ExampleComponent (line 1) | class ExampleComponent extends HTMLElement {
method connectedCallback (line 2) | connectedCallback() {
FILE: public/pages/examples/styling/scoping-shadowed/components/example/example.js
class ExampleComponent (line 1) | class ExampleComponent extends HTMLElement {
method constructor (line 2) | constructor() {
FILE: public/tests/imports-test.js
function render (line 26) | function render(el) {
FILE: public/tests/lib/@testing-library/dom.umd.js
function _mergeNamespaces (line 7) | function _mergeNamespaces(n, m) {
function assembleStyles (line 46) | function assembleStyles() {
function printIteratorEntries (line 227) | function printIteratorEntries(iterator, config, indentation, depth, refs...
function printIteratorValues (line 267) | function printIteratorValues(iterator, config, indentation, depth, refs,...
function printListItems (line 298) | function printListItems(list, config, indentation, depth, refs, printer) {
function printObjectProperties (line 331) | function printObjectProperties(val, config, indentation, depth, refs, pr...
function _interopRequireDefault$9 (line 445) | function _interopRequireDefault$9(obj) {
function escapeHTML$1 (line 600) | function escapeHTML$1(str) {
function _interopRequireDefault$8 (line 611) | function _interopRequireDefault$8(obj) {
function nodeIsText$1 (line 727) | function nodeIsText$1(node) {
function nodeIsComment$1 (line 731) | function nodeIsComment$1(node) {
function nodeIsFragment$1 (line 735) | function nodeIsFragment$1(node) {
function getRecordEntries (line 805) | function getRecordEntries(val) {
function isValidElementType (line 990) | function isValidElementType(type) {
function typeOf (line 1009) | function typeOf(object) {
function isAsyncMode (line 1064) | function isAsyncMode(object) {
function isConcurrentMode (line 1075) | function isConcurrentMode(object) {
function isContextConsumer (line 1086) | function isContextConsumer(object) {
function isContextProvider (line 1090) | function isContextProvider(object) {
function isElement (line 1094) | function isElement(object) {
function isForwardRef (line 1098) | function isForwardRef(object) {
function isFragment (line 1102) | function isFragment(object) {
function isLazy (line 1106) | function isLazy(object) {
function isMemo (line 1110) | function isMemo(object) {
function isPortal (line 1114) | function isPortal(object) {
function isProfiler (line 1118) | function isProfiler(object) {
function isStrictMode (line 1122) | function isStrictMode(object) {
function isSuspense (line 1126) | function isSuspense(object) {
function _getRequireWildcardCache (line 1172) | function _getRequireWildcardCache(nodeInterop) {
function _interopRequireWildcard (line 1181) | function _interopRequireWildcard(obj, nodeInterop) {
function _interopRequireDefault$7 (line 1385) | function _interopRequireDefault$7(obj) {
class PrettyFormatPluginError (line 1420) | class PrettyFormatPluginError extends Error {
method constructor (line 1421) | constructor(message, stack) {
function isToStringedArrayType (line 1429) | function isToStringedArrayType(toStringed) {
function printNumber (line 1433) | function printNumber(val) {
function printBigInt (line 1437) | function printBigInt(val) {
function printFunction (line 1441) | function printFunction(val, printFunctionName) {
function printSymbol (line 1449) | function printSymbol(val) {
function printError (line 1453) | function printError(val) {
function printBasicValue (line 1462) | function printBasicValue(val, printFunctionName, escapeRegex, escapeStri...
function printComplexValue (line 1548) | function printComplexValue(val, config, indentation, depth, refs, hasCal...
function isNewPlugin (line 1585) | function isNewPlugin(plugin) {
function printPlugin (line 1589) | function printPlugin(plugin, val, config, indentation, depth, refs) {
function findPlugin (line 1612) | function findPlugin(plugins, val) {
function printer (line 1626) | function printer(val, config, indentation, depth, refs, hasCalledToJSON) {
function validateOptions (line 1666) | function validateOptions(options) {
function createIndent (line 1735) | function createIndent(indent) {
function format (line 1745) | function format(val, options) {
method DEFAULT_OPTIONS (line 1782) | get DEFAULT_OPTIONS () { return DEFAULT_OPTIONS_1; }
method plugins (line 1784) | get plugins () { return plugins_1; }
method default (line 1785) | get default () { return default_1; }
function escapeHTML (line 1795) | function escapeHTML(str) {
function nodeIsText (line 1873) | function nodeIsText(node) {
function nodeIsComment (line 1877) | function nodeIsComment(node) {
function nodeIsFragment (line 1881) | function nodeIsFragment(node) {
function createDOMElementFilter (line 1885) | function createDOMElementFilter(filterNode) {
function getCodeFrame (line 1929) | function getCodeFrame(frame) {
function getUserCodeFrame (line 1955) | function getUserCodeFrame() {
function jestFakeTimersAreEnabled (line 1974) | function jestFakeTimersAreEnabled() {
function getDocument (line 1987) | function getDocument() {
function getWindowFromNode (line 1996) | function getWindowFromNode(node) {
function checkContainerType (line 2020) | function checkContainerType(container) {
function filterCommentsAndDefaultIgnoreTagsTags (line 2043) | function filterCommentsAndDefaultIgnoreTagsTags(value) {
function prettyDOM (line 2047) | function prettyDOM(dom, maxLength, options) {
method getElementError (line 2130) | getElementError(message, container) {
function runWithExpensiveErrorDiagnosticsDisabled (line 2140) | function runWithExpensiveErrorDiagnosticsDisabled(callback) {
function configure (line 2148) | function configure(newConfig) {
function getConfig (line 2160) | function getConfig() {
function getTextContent (line 2166) | function getTextContent(node) {
function getLabelContent (line 2175) | function getLabelContent(element) {
function getRealLabels (line 2188) | function getRealLabels(element) {
function isLabelable (line 2201) | function isLabelable(element) {
function getLabels$1 (line 2205) | function getLabels$1(container, element, _temp) {
function assertNotNullOrUndefined (line 2231) | function assertNotNullOrUndefined(matcher) {
function fuzzyMatches (line 2238) | function fuzzyMatches(textToMatch, node, matcher, normalizer) {
function matches (line 2255) | function matches(textToMatch, node, matcher, normalizer) {
function getDefaultNormalizer (line 2272) | function getDefaultNormalizer(_temp) {
function makeNormalizer (line 2295) | function makeNormalizer(_ref) {
function matchRegExp (line 2318) | function matchRegExp(matcher, text) {
function getNodeText (line 2329) | function getNodeText(node) {
function isCallable (line 2343) | function isCallable(fn) {
function toInteger (line 2347) | function toInteger(value) {
function toLength (line 2363) | function toLength(value) {
function arrayFrom (line 2380) | function arrayFrom(arrayLike, mapFn) {
function _classCallCheck (line 2432) | function _classCallCheck(instance, Constructor) {
function _defineProperties (line 2438) | function _defineProperties(target, props) {
function _createClass (line 2448) | function _createClass(Constructor, protoProps, staticProps) {
function _defineProperty$2 (line 2457) | function _defineProperty$2(obj, key, value) {
function SetLike (line 2474) | function SetLike() {
function getLocalName (line 2539) | function getLocalName(element) {
function hasGlobalAriaAttributes (line 2615) | function hasGlobalAriaAttributes(element, role) {
function ignorePresentationalRole (line 2629) | function ignorePresentationalRole(element, implicitRole) {
function getRole (line 2634) | function getRole(element) {
function getImplicitRole (line 2648) | function getImplicitRole(element) {
function getExplicitRole (line 2727) | function getExplicitRole(element) {
function isElement (line 2742) | function isElement(node) {
function isHTMLTableCaptionElement (line 2745) | function isHTMLTableCaptionElement(node) {
function isHTMLInputElement (line 2748) | function isHTMLInputElement(node) {
function isHTMLOptGroupElement (line 2751) | function isHTMLOptGroupElement(node) {
function isHTMLSelectElement (line 2754) | function isHTMLSelectElement(node) {
function isHTMLTableElement (line 2757) | function isHTMLTableElement(node) {
function isHTMLTextAreaElement (line 2760) | function isHTMLTextAreaElement(node) {
function safeWindow (line 2763) | function safeWindow(node) {
function isHTMLFieldSetElement (line 2773) | function isHTMLFieldSetElement(node) {
function isHTMLLegendElement (line 2776) | function isHTMLLegendElement(node) {
function isHTMLSlotElement (line 2779) | function isHTMLSlotElement(node) {
function isSVGElement (line 2782) | function isSVGElement(node) {
function isSVGSVGElement (line 2785) | function isSVGSVGElement(node) {
function isSVGTitleElement (line 2788) | function isSVGTitleElement(node) {
function queryIdRefs (line 2798) | function queryIdRefs(node, attributeName) {
function hasAnyConcreteRoles (line 2814) | function hasAnyConcreteRoles(node, roles) {
function asFlatString (line 2835) | function asFlatString(s) {
function isHidden (line 2846) | function isHidden(node, getComputedStyleImplementation) {
function isControl (line 2864) | function isControl(node) {
function hasAbstractRole (line 2868) | function hasAbstractRole(node, role) {
function querySelectorAllSubtree (line 2888) | function querySelectorAllSubtree(element, selectors) {
function querySelectedOptions (line 2897) | function querySelectedOptions(listbox) {
function isMarkedPresentational (line 2906) | function isMarkedPresentational(node) {
function isNativeHostLanguageTextAlternativeElement (line 2921) | function isNativeHostLanguageTextAlternativeElement(node) {
function allowsNameFromContent (line 2929) | function allowsNameFromContent(node) {
function isDescendantOfNativeHostLanguageTextAlternativeElement (line 2937) | function isDescendantOfNativeHostLanguageTextAlternativeElement( // esli...
function getValueOfTextbox (line 2942) | function getValueOfTextbox(element) {
function getTextualContent (line 2951) | function getTextualContent(declaration) {
function isLabelableElement (line 2967) | function isLabelableElement(element) {
function findLabelableElement (line 2978) | function findLabelableElement(element) {
function getControlOfLabel (line 3002) | function getControlOfLabel(label) {
function getLabels (line 3022) | function getLabels(element) {
function getSlotContents (line 3049) | function getSlotContents(slot) {
function computeTextAlternative (line 3070) | function computeTextAlternative(root) {
function ownKeys (line 3412) | function ownKeys(object, enumerableOnly) {
function _objectSpread (line 3425) | function _objectSpread(target) {
function _defineProperty$1 (line 3438) | function _defineProperty$1(obj, key, value) {
function computeAccessibleDescription (line 3458) | function computeAccessibleDescription(root) {
function prohibitsNaming (line 3482) | function prohibitsNaming(node) {
function computeAccessibleName (line 3493) | function computeAccessibleName(root) {
function _slicedToArray$4 (line 3512) | function _slicedToArray$4(arr, i) {
function _nonIterableRest$4 (line 3516) | function _nonIterableRest$4() {
function _unsupportedIterableToArray$4 (line 3520) | function _unsupportedIterableToArray$4(o, minLen) {
function _arrayLikeToArray$4 (line 3529) | function _arrayLikeToArray$4(arr, len) {
function _iterableToArrayLimit$4 (line 3539) | function _iterableToArrayLimit$4(arr, i) {
function _arrayWithHoles$4 (line 3569) | function _arrayWithHoles$4(arr) {
function _slicedToArray$3 (line 3723) | function _slicedToArray$3(arr, i) {
function _nonIterableRest$3 (line 3727) | function _nonIterableRest$3() {
function _unsupportedIterableToArray$3 (line 3731) | function _unsupportedIterableToArray$3(o, minLen) {
function _arrayLikeToArray$3 (line 3740) | function _arrayLikeToArray$3(arr, len) {
function _iterableToArrayLimit$3 (line 3750) | function _iterableToArrayLimit$3(arr, i) {
function _arrayWithHoles$3 (line 3780) | function _arrayWithHoles$3(arr) {
function _interopRequireDefault$6 (line 4468) | function _interopRequireDefault$6(obj) {
function _interopRequireDefault$5 (line 7616) | function _interopRequireDefault$5(obj) {
function _interopRequireDefault$4 (line 9052) | function _interopRequireDefault$4(obj) {
function _interopRequireDefault$3 (line 9073) | function _interopRequireDefault$3(obj) {
function _defineProperty (line 9079) | function _defineProperty(obj, key, value) {
function _createForOfIteratorHelper (line 9094) | function _createForOfIteratorHelper(o, allowArrayLike) {
function _slicedToArray$2 (line 9151) | function _slicedToArray$2(arr, i) {
function _nonIterableRest$2 (line 9155) | function _nonIterableRest$2() {
function _unsupportedIterableToArray$2 (line 9159) | function _unsupportedIterableToArray$2(o, minLen) {
function _arrayLikeToArray$2 (line 9168) | function _arrayLikeToArray$2(arr, len) {
function _iterableToArrayLimit$2 (line 9178) | function _iterableToArrayLimit$2(arr, i) {
function _arrayWithHoles$2 (line 9208) | function _arrayWithHoles$2(arr) {
function _interopRequireDefault$2 (line 9309) | function _interopRequireDefault$2(obj) {
function _slicedToArray$1 (line 9315) | function _slicedToArray$1(arr, i) {
function _nonIterableRest$1 (line 9319) | function _nonIterableRest$1() {
function _unsupportedIterableToArray$1 (line 9323) | function _unsupportedIterableToArray$1(o, minLen) {
function _arrayLikeToArray$1 (line 9332) | function _arrayLikeToArray$1(arr, len) {
function _iterableToArrayLimit$1 (line 9342) | function _iterableToArrayLimit$1(arr, i) {
function _arrayWithHoles$1 (line 9372) | function _arrayWithHoles$1(arr) {
function _interopRequireDefault$1 (line 9471) | function _interopRequireDefault$1(obj) {
function _slicedToArray (line 9477) | function _slicedToArray(arr, i) {
function _nonIterableRest (line 9481) | function _nonIterableRest() {
function _unsupportedIterableToArray (line 9485) | function _unsupportedIterableToArray(o, minLen) {
function _arrayLikeToArray (line 9494) | function _arrayLikeToArray(arr, len) {
function _iterableToArrayLimit (line 9504) | function _iterableToArrayLimit(arr, i) {
function _arrayWithHoles (line 9534) | function _arrayWithHoles(arr) {
function _interopRequireDefault (line 9628) | function _interopRequireDefault(obj) {
function isSubtreeInaccessible (line 9651) | function isSubtreeInaccessible(element) {
function isInaccessible (line 9684) | function isInaccessible(element, options) {
function getImplicitAriaRoles (line 9711) | function getImplicitAriaRoles(currentNode) {
function buildElementRoleList (line 9726) | function buildElementRoleList(elementRolesMap) {
function getRoles (line 9805) | function getRoles(container, _temp) {
function prettyRoles (line 9833) | function prettyRoles(dom, _ref6) {
function computeAriaSelected (line 9881) | function computeAriaSelected(element) {
function computeAriaChecked (line 9897) | function computeAriaChecked(element) {
function computeAriaPressed (line 9918) | function computeAriaPressed(element) {
function computeAriaCurrent (line 9928) | function computeAriaCurrent(element) {
function computeAriaExpanded (line 9940) | function computeAriaExpanded(element) {
function checkBooleanAttribute (line 9945) | function checkBooleanAttribute(element, attribute) {
function computeHeadingLevel (line 9964) | function computeHeadingLevel(element) {
function escapeRegExp (line 9983) | function escapeRegExp(string) {
function getRegExpMatcher (line 9987) | function getRegExpMatcher(string) {
function makeSuggestion (line 9991) | function makeSuggestion(queryName, element, content, _ref) {
function canSuggest (line 10038) | function canSuggest(currentMethod, requestedMethod, data) {
function getSuggestedQuery (line 10042) | function getSuggestedQuery(element, variant, method) {
function copyStackTrace (line 10125) | function copyStackTrace(target, source) {
function waitFor (line 10129) | function waitFor(callback, _ref) {
function waitForWrapper (line 10297) | function waitForWrapper(callback, options) {
function getElementError (line 10311) | function getElementError(message, container) {
function getMultipleElementsFoundError (line 10315) | function getMultipleElementsFoundError(message, container) {
function queryAllByAttribute (line 10319) | function queryAllByAttribute(attribute, container, text, _temp) {
function queryByAttribute (line 10335) | function queryByAttribute(attribute, container, text, options) {
function makeSingleQuery (line 10348) | function makeSingleQuery(allQuery, getMultipleError) {
function getSuggestionError (line 10365) | function getSuggestionError(suggestion, container) {
function makeGetAllQuery (line 10371) | function makeGetAllQuery(allQuery, getMissingError) {
function makeFindQuery (line 10389) | function makeFindQuery(getter) {
function buildQueries (line 10453) | function buildQueries(queryAllBy, getMultipleError, getMissingError) {
function queryAllLabels (line 10478) | function queryAllLabels(container) {
function getTagNameOfElementAssociatedWithLabelViaFor (line 10600) | function getTagNameOfElementAssociatedWithLabelViaFor(container, label) {
function queryAllByRole (line 10768) | function queryAllByRole(container, role, _temp) {
function makeRoleSelector (line 10933) | function makeRoleSelector(role, exact, customNormalizer) {
function getQueriesForElement (line 11113) | function getQueriesForElement(element, queries$1, initialValue) {
function initialCheck (line 11133) | function initialCheck(elements) {
function waitForElementToBeRemoved (line 11139) | async function waitForElementToBeRemoved(callback, options) {
function fireEvent (line 11861) | function fireEvent(element, event) {
function createEvent (line 11875) | function createEvent(eventName, node, init, _temp) {
function setNativeValue (line 11977) | function setNativeValue(element, value) {
function getBaseValue (line 12027) | function getBaseValue(alphabet, character) {
function unindent (line 12609) | function unindent(string) {
function encode (line 12615) | function encode(value) {
function getPlaygroundUrl (line 12619) | function getPlaygroundUrl(markup) {
FILE: public/tests/lib/mocha/chai.js
function r (line 1) | function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==...
function Assertion (line 160) | function Assertion (obj, msg, ssfi, lockSsfi) {
function an (line 672) | function an (type, msg) {
function SameValueZero (line 835) | function SameValueZero(a, b) {
function includeChainingBehavior (line 839) | function includeChainingBehavior () {
function include (line 843) | function include (val, msg) {
function assertExist (line 1206) | function assertExist () {
function checkArguments (line 1342) | function checkArguments () {
function assertEqual (line 1400) | function assertEqual (val, msg) {
function assertEql (line 1465) | function assertEql(obj, msg) {
function assertAbove (line 1523) | function assertAbove (n, msg) {
function assertLeast (line 1628) | function assertLeast (n, msg) {
function assertBelow (line 1732) | function assertBelow (n, msg) {
function assertMost (line 1837) | function assertMost (n, msg) {
function assertInstanceOf (line 2038) | function assertInstanceOf (constructor, msg) {
function assertProperty (line 2187) | function assertProperty (name, val, msg) {
function assertOwnProperty (line 2276) | function assertOwnProperty (name, value, msg) {
function assertOwnPropertyDescriptor (line 2403) | function assertOwnPropertyDescriptor (name, descriptor, msg) {
function assertLengthChain (line 2491) | function assertLengthChain () {
function assertLength (line 2495) | function assertLength (n, msg) {
function assertMatch (line 2554) | function assertMatch(re, msg) {
function assertKeys (line 2710) | function assertKeys (keys) {
function assertThrows (line 3004) | function assertThrows (errorLike, errMsgMatcher, msg) {
function respondTo (line 3199) | function respondTo (method, msg) {
function satisfy (line 3279) | function satisfy (matcher, msg) {
function closeTo (line 3333) | function closeTo(expected, delta, msg) {
function isSubsetOf (line 3361) | function isSubsetOf(subset, superset, cmp, contains, ordered) {
function oneOf (line 3532) | function oneOf (list, msg) {
function assertChanges (line 3667) | function assertChanges (subject, prop, msg) {
function assertIncreases (line 3784) | function assertIncreases (subject, prop, msg) {
function assertDecreases (line 3903) | function assertDecreases (subject, prop, msg) {
function assertDelta (line 4009) | function assertDelta(delta, msg) {
function loadShould (line 7400) | function loadShould () {
function isObjectType (line 8171) | function isObjectType(obj) {
function addProperty (line 8306) | function addProperty(property) {
function inspect (line 8525) | function inspect(obj, showHidden, depth, colors) {
function isNaN (line 8554) | function isNaN(value) {
function stringDistanceCapped (line 9013) | function stringDistanceCapped(strA, strB, cap) {
function exclude (line 9142) | function exclude () {
function AssertionError (line 9181) | function AssertionError (message, _props, ssf) {
function compatibleInstance (line 9275) | function compatibleInstance(thrown, errorLike) {
function compatibleConstructor (line 9295) | function compatibleConstructor(thrown, errorLike) {
function compatibleMessage (line 9321) | function compatibleMessage(thrown, errMatcher) {
function getFunctionName (line 9345) | function getFunctionName(constructorFn) {
function getConstructorName (line 9371) | function getConstructorName(errorLike) {
function getMessage (line 9399) | function getMessage(errorLike) {
function FakeMap (line 9428) | function FakeMap() {
function memoizeCompare (line 9455) | function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) {
function memoizeSet (line 9478) | function memoizeSet(leftHandOperand, rightHandOperand, memoizeMap, resul...
function deepEqual (line 9512) | function deepEqual(leftHandOperand, rightHandOperand, options) {
function simpleEqual (line 9533) | function simpleEqual(leftHandOperand, rightHandOperand) {
function extensiveDeepEqual (line 9569) | function extensiveDeepEqual(leftHandOperand, rightHandOperand, options) {
function extensiveDeepEqualByType (line 9615) | function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, lef...
function regexpEqual (line 9667) | function regexpEqual(leftHandOperand, rightHandOperand) {
function entriesEqual (line 9680) | function entriesEqual(leftHandOperand, rightHandOperand, options) {
function iterableEqual (line 9708) | function iterableEqual(leftHandOperand, rightHandOperand, options) {
function generatorEqual (line 9734) | function generatorEqual(leftHandOperand, rightHandOperand, options) {
function hasIteratorFunction (line 9744) | function hasIteratorFunction(target) {
function getIteratorEntries (line 9758) | function getIteratorEntries(target) {
function getGeneratorEntries (line 9775) | function getGeneratorEntries(generator) {
function getEnumerableKeys (line 9791) | function getEnumerableKeys(target) {
function keysEqual (line 9809) | function keysEqual(leftHandOperand, rightHandOperand, keys, options) {
function objectEqual (line 9832) | function objectEqual(leftHandOperand, rightHandOperand, options) {
function isPrimitive (line 9871) | function isPrimitive(value) {
function getFuncName (line 9899) | function getFuncName(aFunc) {
function _typeof (line 9928) | function _typeof(obj) {
function _slicedToArray (line 9944) | function _slicedToArray(arr, i) {
function _arrayWithHoles (line 9948) | function _arrayWithHoles(arr) {
function _iterableToArrayLimit (line 9952) | function _iterableToArrayLimit(arr, i) {
function _unsupportedIterableToArray (line 9979) | function _unsupportedIterableToArray(o, minLen) {
function _arrayLikeToArray (line 9988) | function _arrayLikeToArray(arr, len) {
function _nonIterableRest (line 9996) | function _nonIterableRest() {
function colorise (line 10043) | function colorise(value, styleType) {
function normaliseOptions (line 10053) | function normaliseOptions() {
function truncate (line 10095) | function truncate(string, length) {
function inspectList (line 10112) | function inspectList(list, options, inspectItem) {
function quoteComplexKey (line 10167) | function quoteComplexKey(key) {
function inspectProperty (line 10175) | function inspectProperty(_ref2, options) {
function inspectArray (line 10193) | function inspectArray(array, options) {
function getFuncName (line 10233) | function getFuncName(aFunc) {
function inspectTypedArray (line 10268) | function inspectTypedArray(array, options) {
function inspectDate (line 10302) | function inspectDate(dateObject, options) {
function inspectFunction (line 10309) | function inspectFunction(func, options) {
function inspectMapEntry (line 10319) | function inspectMapEntry(_ref, options) {
function mapToEntries (line 10332) | function mapToEntries(map) {
function inspectMap (line 10340) | function inspectMap(map, options) {
function inspectNumber (line 10356) | function inspectNumber(number, options) {
function inspectBigInt (line 10376) | function inspectBigInt(number, options) {
function inspectRegExp (line 10382) | function inspectRegExp(value, options) {
function arrayFromSet (line 10389) | function arrayFromSet(set) {
function inspectSet (line 10397) | function inspectSet(set, options) {
function escape (line 10416) | function escape(char) {
function inspectString (line 10420) | function inspectString(string, options) {
function inspectSymbol (line 10428) | function inspectSymbol(value) {
function inspectObject (line 10466) | function inspectObject(object, options) {
function inspectClass (line 10499) | function inspectClass(value, options) {
function inspectArguments (line 10516) | function inspectArguments(args, options) {
function inspectObject$1 (line 10523) | function inspectObject$1(error, options) {
function inspectAttribute (line 10545) | function inspectAttribute(_ref, options) {
function inspectHTMLCollection (line 10558) | function inspectHTMLCollection(collection, options) {
function inspectHTML (line 10562) | function inspectHTML(element, options) {
function inspect (line 10691) | function inspect(value, options) {
function registerConstructor (line 10742) | function registerConstructor(constructor, inspector) {
function registerStringTag (line 10750) | function registerStringTag(stringTag, inspector) {
function hasProperty (line 10817) | function hasProperty(obj, name) {
function parsePath (line 10845) | function parsePath(path) {
function internalGetPathValue (line 10884) | function internalGetPathValue(obj, parsed, pathDepth) {
function internalSetPathValue (line 10921) | function internalSetPathValue(obj, val, parsed) {
function getPathInfo (line 10975) | function getPathInfo(obj, path) {
function getPathValue (line 11022) | function getPathValue(obj, path) {
function setPathValue (line 11060) | function setPathValue(obj, path, val) {
function typeDetect (line 11118) | function typeDetect(obj) {
FILE: public/tests/lib/mocha/mocha.js
function createCommonjsModule (line 12) | function createCommonjsModule(fn, basedir, module) {
function getCjsExportFromNamespace (line 22) | function getCjsExportFromNamespace (n) {
function commonjsRequire (line 26) | function commonjsRequire () {
function defaultSetTimout$1 (line 1674) | function defaultSetTimout$1() {
function defaultClearTimeout$1 (line 1678) | function defaultClearTimeout$1() {
function runTimeout$1 (line 1693) | function runTimeout$1(fun) {
function runClearTimeout$1 (line 1719) | function runClearTimeout$1(marker) {
function cleanUpNextTick$1 (line 1751) | function cleanUpNextTick$1() {
function drainQueue$1 (line 1769) | function drainQueue$1() {
function nextTick$1 (line 1797) | function nextTick$1(fun) {
function Item$1 (line 1813) | function Item$1(fun, array) {
function noop$1 (line 1833) | function noop$1() {}
function binding$1 (line 1842) | function binding$1(name) {
function cwd$1 (line 1845) | function cwd$1() {
function chdir$1 (line 1848) | function chdir$1(dir) {
function umask$1 (line 1851) | function umask$1() {
function hrtime$1 (line 1863) | function hrtime$1(previousTimestamp) {
function uptime$1 (line 1881) | function uptime$1() {
function F (line 1942) | function F() { /* empty */ }
function EventHandlers (line 2008) | function EventHandlers() {}
function EventEmitter$2 (line 2012) | function EventEmitter$2() {
function $getMaxListeners (line 2050) | function $getMaxListeners(that) {
function emitNone (line 2064) | function emitNone(handler, isFn, self) {
function emitOne (line 2075) | function emitOne(handler, isFn, self, arg1) {
function emitTwo (line 2086) | function emitTwo(handler, isFn, self, arg1, arg2) {
function emitThree (line 2097) | function emitThree(handler, isFn, self, arg1, arg2, arg3) {
function emitMany (line 2108) | function emitMany(handler, isFn, self, args) {
function _addListener (line 2183) | function _addListener(target, type, listener, prepend) {
function emitWarning$1 (line 2242) | function emitWarning$1(e) {
function _onceWrap (line 2256) | function _onceWrap(target, type, listener) {
function listenerCount$1 (line 2397) | function listenerCount$1(type) {
function spliceOne (line 2418) | function spliceOne(list, index) {
function arrayClone (line 2426) | function arrayClone(arr, i) {
function unwrapListeners (line 2436) | function unwrapListeners(arr) {
function _asyncIterator (line 2446) | function _asyncIterator(iterable) {
function AsyncFromSyncIterator (line 2461) | function AsyncFromSyncIterator(s) {
function ownKeys (line 2495) | function ownKeys(object, enumerableOnly) {
function _objectSpread2 (line 2508) | function _objectSpread2(target) {
function _typeof (line 2521) | function _typeof(obj) {
function asyncGeneratorStep (line 2531) | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, ar...
function _asyncToGenerator (line 2547) | function _asyncToGenerator(fn) {
function _classCallCheck (line 2567) | function _classCallCheck(instance, Constructor) {
function _defineProperties (line 2573) | function _defineProperties(target, props) {
function _createClass (line 2583) | function _createClass(Constructor, protoProps, staticProps) {
function _defineProperty (line 2592) | function _defineProperty(obj, key, value) {
function _inherits (line 2607) | function _inherits(subClass, superClass) {
function _getPrototypeOf (line 2625) | function _getPrototypeOf(o) {
function _setPrototypeOf (line 2632) | function _setPrototypeOf(o, p) {
function _isNativeReflectConstruct (line 2641) | function _isNativeReflectConstruct() {
function _assertThisInitialized (line 2654) | function _assertThisInitialized(self) {
function _possibleConstructorReturn (line 2662) | function _possibleConstructorReturn(self, call) {
function _createSuper (line 2672) | function _createSuper(Derived) {
function _toConsumableArray (line 2691) | function _toConsumableArray(arr) {
function _arrayWithoutHoles (line 2695) | function _arrayWithoutHoles(arr) {
function _iterableToArray (line 2699) | function _iterableToArray(iter) {
function _unsupportedIterableToArray (line 2703) | function _unsupportedIterableToArray(o, minLen) {
function _arrayLikeToArray (line 2712) | function _arrayLikeToArray(arr, len) {
function _nonIterableSpread (line 2720) | function _nonIterableSpread() {
function init$1 (line 5849) | function init$1() {
function toByteArray$1 (line 5862) | function toByteArray$1(b64) {
function tripletToBase64$1 (line 5905) | function tripletToBase64$1(num) {
function encodeChunk$1 (line 5909) | function encodeChunk$1(uint8, start, end) {
function fromByteArray$1 (line 5921) | function fromByteArray$1(uint8) {
function read$1 (line 5957) | function read$1(buffer, offset, isLE, mLen, nBytes) {
function write$1 (line 5990) | function write$1(buffer, value, offset, isLE, mLen, nBytes) {
function kMaxLength$1 (line 6078) | function kMaxLength$1() {
function createBuffer$1 (line 6082) | function createBuffer$1(that, length) {
function Buffer$1 (line 6113) | function Buffer$1(arg, encodingOrOffset, length) {
function from$1 (line 6137) | function from$1(that, value, encodingOrOffset, length) {
function assertSize$1 (line 6171) | function assertSize$1(size) {
function alloc$1 (line 6179) | function alloc$1(that, size, fill, encoding) {
function allocUnsafe$1 (line 6205) | function allocUnsafe$1(that, size) {
function fromString$1 (line 6234) | function fromString$1(that, string, encoding) {
function fromArrayLike$1 (line 6257) | function fromArrayLike$1(that, array) {
function fromArrayBuffer$1 (line 6268) | function fromArrayBuffer$1(that, array, byteOffset, length) {
function fromObject$1 (line 6299) | function fromObject$1(that, obj) {
function checked$1 (line 6329) | function checked$1(length) {
function internalIsBuffer$1 (line 6340) | function internalIsBuffer$1(b) {
function byteLength$1 (line 6422) | function byteLength$1(string, encoding) {
function slowToString$1 (line 6475) | function slowToString$1(encoding, start, end) {
function swap$1 (line 6548) | function swap$1(b, n, m) {
function bidirectionalIndexOf$1 (line 6695) | function bidirectionalIndexOf$1(buffer, val, byteOffset, encoding, dir) {
function arrayIndexOf$1 (line 6754) | function arrayIndexOf$1(arr, val, byteOffset, encoding, dir) {
function hexWrite$1 (line 6828) | function hexWrite$1(buf, string, offset, length) {
function utf8Write$1 (line 6859) | function utf8Write$1(buf, string, offset, length) {
function asciiWrite$1 (line 6863) | function asciiWrite$1(buf, string, offset, length) {
function latin1Write$1 (line 6867) | function latin1Write$1(buf, string, offset, length) {
function base64Write$1 (line 6871) | function base64Write$1(buf, string, offset, length) {
function ucs2Write$1 (line 6875) | function ucs2Write$1(buf, string, offset, length) {
function base64Slice$1 (line 6955) | function base64Slice$1(buf, start, end) {
function utf8Slice$1 (line 6963) | function utf8Slice$1(buf, start, end) {
function decodeCodePointsArray$1 (line 7051) | function decodeCodePointsArray$1(codePoints) {
function asciiSlice$1 (line 7069) | function asciiSlice$1(buf, start, end) {
function latin1Slice$1 (line 7080) | function latin1Slice$1(buf, start, end) {
function hexSlice$1 (line 7091) | function hexSlice$1(buf, start, end) {
function utf16leSlice$1 (line 7104) | function utf16leSlice$1(buf, start, end) {
function checkOffset$1 (line 7156) | function checkOffset$1(offset, ext, length) {
function checkInt$1 (line 7301) | function checkInt$1(buf, value, offset, ext, max, min) {
function objectWriteUInt16$1 (line 7358) | function objectWriteUInt16$1(buf, value, offset, littleEndian) {
function objectWriteUInt32$1 (line 7396) | function objectWriteUInt32$1(buf, value, offset, littleEndian) {
function checkIEEE754$1 (line 7563) | function checkIEEE754$1(buf, value, offset, ext, max, min) {
function writeFloat$1 (line 7568) | function writeFloat$1(buf, value, offset, littleEndian, noAssert) {
function writeDouble$1 (line 7585) | function writeDouble$1(buf, value, offset, littleEndian, noAssert) {
function base64clean$1 (line 7715) | function base64clean$1(str) {
function stringtrim$1 (line 7728) | function stringtrim$1(str) {
function toHex$1 (line 7733) | function toHex$1(n) {
function utf8ToBytes$1 (line 7738) | function utf8ToBytes$1(string, units) {
function asciiToBytes$1 (line 7803) | function asciiToBytes$1(str) {
function utf16leToBytes$1 (line 7814) | function utf16leToBytes$1(str, units) {
function base64ToBytes$1 (line 7830) | function base64ToBytes$1(str) {
function blitBuffer$1 (line 7834) | function blitBuffer$1(src, dst, offset, length) {
function isnan$1 (line 7843) | function isnan$1(val) {
function isBuffer$2 (line 7850) | function isBuffer$2(obj) {
function isFastBuffer$1 (line 7854) | function isFastBuffer$1(obj) {
function isSlowBuffer$1 (line 7859) | function isSlowBuffer$1(obj) {
function defaultSetTimout (line 7865) | function defaultSetTimout() {
function defaultClearTimeout (line 7869) | function defaultClearTimeout() {
function runTimeout (line 7884) | function runTimeout(fun) {
function runClearTimeout (line 7910) | function runClearTimeout(marker) {
function cleanUpNextTick (line 7942) | function cleanUpNextTick() {
function drainQueue (line 7960) | function drainQueue() {
function nextTick (line 7988) | function nextTick(fun) {
function Item (line 8005) | function Item(fun, array) {
function noop (line 8025) | function noop() {}
function binding (line 8035) | function binding(name) {
function cwd (line 8039) | function cwd() {
function chdir (line 8043) | function chdir(dir) {
function umask (line 8047) | function umask() {
function hrtime (line 8060) | function hrtime(previousTimestamp) {
function uptime (line 8080) | function uptime() {
function format$1 (line 8143) | function format$1(f) {
function deprecate$1 (line 8193) | function deprecate$1(fn, msg) {
function debuglog (line 8227) | function debuglog(set) {
function inspect (line 8256) | function inspect(obj, opts) {
function stylizeWithColor (line 8311) | function stylizeWithColor(str, styleType) {
function stylizeNoColor (line 8321) | function stylizeNoColor(str, styleType) {
function arrayToHash (line 8325) | function arrayToHash(array) {
function formatValue (line 8333) | function formatValue(ctx, value, recurseTimes) {
function formatPrimitive (line 8446) | function formatPrimitive(ctx, value) {
function formatError (line 8460) | function formatError(value) {
function formatArray (line 8464) | function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
function formatProperty (line 8483) | function formatProperty(ctx, value, recurseTimes, visibleKeys, key, arra...
function reduceToSingleString (line 8548) | function reduceToSingleString(output, base, braces) {
function isArray$1 (line 8563) | function isArray$1(ar) {
function isBoolean (line 8566) | function isBoolean(arg) {
function isNull (line 8569) | function isNull(arg) {
function isNullOrUndefined (line 8572) | function isNullOrUndefined(arg) {
function isNumber (line 8575) | function isNumber(arg) {
function isString$1 (line 8578) | function isString$1(arg) {
function isSymbol (line 8581) | function isSymbol(arg) {
function isUndefined (line 8584) | function isUndefined(arg) {
function isRegExp (line 8587) | function isRegExp(re) {
function isObject (line 8590) | function isObject(arg) {
function isDate (line 8593) | function isDate(d) {
function isError$1 (line 8596) | function isError$1(e) {
function isFunction (line 8599) | function isFunction(arg) {
function isPrimitive (line 8602) | function isPrimitive(arg) {
function isBuffer$1 (line 8606) | function isBuffer$1(maybeBuf) {
function objectToString (line 8610) | function objectToString(o) {
function pad (line 8614) | function pad(n) {
function timestamp (line 8620) | function timestamp() {
function log$1 (line 8627) | function log$1() {
function _extend (line 8630) | function _extend(origin, add) {
function hasOwnProperty (line 8643) | function hasOwnProperty(obj, prop) {
function init (line 8677) | function init() {
function toByteArray (line 8690) | function toByteArray(b64) {
function tripletToBase64 (line 8733) | function tripletToBase64(num) {
function encodeChunk (line 8737) | function encodeChunk(uint8, start, end) {
function fromByteArray (line 8749) | function fromByteArray(uint8) {
function read (line 8785) | function read(buffer, offset, isLE, mLen, nBytes) {
function write (line 8819) | function write(buffer, value, offset, isLE, mLen, nBytes) {
function kMaxLength (line 8915) | function kMaxLength() {
function createBuffer (line 8919) | function createBuffer(that, length) {
function Buffer (line 8950) | function Buffer(arg, encodingOrOffset, length) {
function from (line 8975) | function from(that, value, encodingOrOffset, length) {
function assertSize (line 9009) | function assertSize(size) {
function alloc (line 9017) | function alloc(that, size, fill, encoding) {
function allocUnsafe (line 9043) | function allocUnsafe(that, size) {
function fromString (line 9072) | function fromString(that, string, encoding) {
function fromArrayLike (line 9095) | function fromArrayLike(that, array) {
function fromArrayBuffer (line 9106) | function fromArrayBuffer(that, array, byteOffset, length) {
function fromObject (line 9137) | function fromObject(that, obj) {
function checked (line 9167) | function checked(length) {
function internalIsBuffer (line 9179) | function internalIsBuffer(b) {
function byteLength (line 9261) | function byteLength(string, encoding) {
function slowToString (line 9314) | function slowToString(encoding, start, end) {
function swap (line 9387) | function swap(b, n, m) {
function bidirectionalIndexOf (line 9534) | function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) {
function arrayIndexOf (line 9593) | function arrayIndexOf(arr, val, byteOffset, encoding, dir) {
function hexWrite (line 9667) | function hexWrite(buf, string, offset, length) {
function utf8Write (line 9698) | function utf8Write(buf, string, offset, length) {
function asciiWrite (line 9702) | function asciiWrite(buf, string, offset, length) {
function latin1Write (line 9706) | function latin1Write(buf, string, offset, length) {
function base64Write (line 9710) | function base64Write(buf, string, offset, length) {
function ucs2Write (line 9714) | function ucs2Write(buf, string, offset, length) {
function base64Slice (line 9794) | function base64Slice(buf, start, end) {
function utf8Slice (line 9802) | function utf8Slice(buf, start, end) {
function decodeCodePointsArray (line 9890) | function decodeCodePointsArray(codePoints) {
function asciiSlice (line 9908) | function asciiSlice(buf, start, end) {
function latin1Slice (line 9919) | function latin1Slice(buf, start, end) {
function hexSlice (line 9930) | function hexSlice(buf, start, end) {
function utf16leSlice (line 9943) | function utf16leSlice(buf, start, end) {
function checkOffset (line 9995) | function checkOffset(offset, ext, length) {
function checkInt (line 10140) | function checkInt(buf, value, offset, ext, max, min) {
function objectWriteUInt16 (line 10197) | function objectWriteUInt16(buf, value, offset, littleEndian) {
function objectWriteUInt32 (line 10235) | function objectWriteUInt32(buf, value, offset, littleEndian) {
function checkIEEE754 (line 10402) | function checkIEEE754(buf, value, offset, ext, max, min) {
function writeFloat (line 10407) | function writeFloat(buf, value, offset, littleEndian, noAssert) {
function writeDouble (line 10424) | function writeDouble(buf, value, offset, littleEndian, noAssert) {
function base64clean (line 10554) | function base64clean(str) {
function stringtrim (line 10567) | function stringtrim(str) {
function toHex (line 10572) | function toHex(n) {
function utf8ToBytes (line 10577) | function utf8ToBytes(string, units) {
function asciiToBytes (line 10642) | function asciiToBytes(str) {
function utf16leToBytes (line 10653) | function utf16leToBytes(str, units) {
function base64ToBytes (line 10669) | function base64ToBytes(str) {
function blitBuffer (line 10673) | function blitBuffer(src, dst, offset, length) {
function isnan (line 10682) | function isnan(val) {
function isBuffer (line 10689) | function isBuffer(obj) {
function isFastBuffer (line 10693) | function isFastBuffer(obj) {
function isSlowBuffer (line 10698) | function isSlowBuffer(obj) {
function BufferList (line 10702) | function BufferList() {
function assertEncoding (line 10789) | function assertEncoding(encoding) {
function StringDecoder (line 10803) | function StringDecoder(encoding) {
function passThroughWrite (line 10960) | function passThroughWrite(buffer) {
function utf16DetectIncompleteChar (line 10964) | function utf16DetectIncompleteChar(buffer) {
function base64DetectIncompleteChar (line 10969) | function base64DetectIncompleteChar(buffer) {
function prependListener (line 10978) | function prependListener(emitter, event, fn) {
function listenerCount (line 10992) | function listenerCount(emitter, type) {
function ReadableState (line 10996) | function ReadableState(options, stream) {
function Readable (line 11050) | function Readable(options) {
function readableAddChunk (line 11087) | function readableAddChunk(stream, state, chunk, encoding, addToFront) {
function needMoreData (line 11143) | function needMoreData(state) {
function computeNewHighWaterMark (line 11157) | function computeNewHighWaterMark(n) {
function howMuchToRead (line 11177) | function howMuchToRead(n, state) {
function chunkInvalid (line 11293) | function chunkInvalid(state, chunk) {
function onEofChunk (line 11303) | function onEofChunk(stream, state) {
function emitReadable (line 11323) | function emitReadable(stream) {
function emitReadable_ (line 11334) | function emitReadable_(stream) {
function maybeReadMore (line 11346) | function maybeReadMore(stream, state) {
function maybeReadMore_ (line 11353) | function maybeReadMore_(stream, state) {
function onunpipe (line 11399) | function onunpipe(readable) {
function onend (line 11407) | function onend() {
function cleanup (line 11420) | function cleanup() {
function ondata (line 11447) | function ondata(chunk) {
function onerror (line 11469) | function onerror(er) {
function onclose (line 11479) | function onclose() {
function onfinish (line 11486) | function onfinish() {
function unpipe (line 11494) | function unpipe() {
function pipeOnDrain (line 11510) | function pipeOnDrain(src) {
function nReadingNextTick (line 11594) | function nReadingNextTick(self) {
function resume (line 11613) | function resume(stream, state) {
function resume_ (line 11620) | function resume_(stream, state) {
function flow (line 11645) | function flow(stream) {
function fromList (line 11718) | function fromList(n, state) {
function fromListPartial (line 11736) | function fromListPartial(n, list, hasStrings) {
function copyFromBufferString (line 11758) | function copyFromBufferString(n, list) {
function copyFromBuffer (line 11792) | function copyFromBuffer(n, list) {
function endReadable (line 11824) | function endReadable(stream) {
function endReadableNT (line 11836) | function endReadableNT(state, stream) {
function forEach (line 11845) | function forEach(xs, f) {
function indexOf (line 11851) | function indexOf(xs, x) {
function nop (line 11863) | function nop() {}
function WriteReq (line 11865) | function WriteReq(chunk, encoding, cb) {
function WritableState (line 11872) | function WritableState(options, stream) {
function Writable (line 11961) | function Writable(options) {
function writeAfterEnd (line 11981) | function writeAfterEnd(stream, cb) {
function validChunk (line 11993) | function validChunk(stream, state, chunk, cb) {
function decodeChunk (line 12054) | function decodeChunk(state, chunk, encoding) {
function writeOrBuffer (line 12065) | function writeOrBuffer(stream, state, chunk, encoding, cb) {
function doWrite (line 12092) | function doWrite(stream, state, writev, len, chunk, encoding, cb) {
function onwriteError (line 12101) | function onwriteError(stream, state, sync, er, cb) {
function onwriteStateUpdate (line 12108) | function onwriteStateUpdate(state) {
function onwrite (line 12115) | function onwrite(stream, er) {
function afterWrite (line 12138) | function afterWrite(stream, state, finished, cb) {
function onwriteDrain (line 12148) | function onwriteDrain(stream, state) {
function clearBuffer (line 12156) | function clearBuffer(stream, state) {
function needFinish (line 12241) | function needFinish(state) {
function prefinish (line 12245) | function prefinish(stream, state) {
function finishMaybe (line 12252) | function finishMaybe(stream, state) {
function endWritable (line 12268) | function endWritable(stream, state, cb) {
function CorkedRequest (line 12282) | function CorkedRequest(state) {
function Duplex (line 12314) | function Duplex(options) {
function onend (line 12325) | function onend() {
function onEndNT (line 12334) | function onEndNT(self) {
function TransformState (line 12341) | function TransformState(stream) {
function afterTransform (line 12353) | function afterTransform(stream, er, data) {
function Transform (line 12369) | function Transform(options) {
function done (line 12442) | function done(stream, er) {
function PassThrough (line 12454) | function PassThrough(options) {
function Stream (line 12473) | function Stream() {
function ondata (line 12480) | function ondata(chunk) {
function ondrain (line 12490) | function ondrain() {
function onend (line 12506) | function onend() {
function onclose (line 12512) | function onclose() {
function onerror (line 12519) | function onerror(er) {
function cleanup (line 12530) | function cleanup() {
function BrowserStdout (line 12555) | function BrowserStdout(opts) {
function highlight (line 12585) | function highlight(js) {
function define (line 13436) | function define(obj, key, value) {
function wrap (line 13455) | function wrap(innerFn, outerFn, self, tryLocsList) {
function tryCatch (line 13477) | function tryCatch(fn, obj, arg) {
function Generator (line 13502) | function Generator() {}
function GeneratorFunction (line 13504) | function GeneratorFunction() {}
function GeneratorFunctionPrototype (line 13506) | function GeneratorFunctionPrototype() {} // This is a polyfill for %Iter...
function defineIteratorMethods (line 13531) | function defineIteratorMethods(prototype) {
function AsyncIterator (line 13568) | function AsyncIterator(generator, PromiseImpl) {
function makeInvokeMethod (line 13650) | function makeInvokeMethod(innerFn, self, context) {
function maybeInvokeDelegate (line 13728) | function maybeInvokeDelegate(delegate, context) {
function pushTryEntry (line 13821) | function pushTryEntry(locs) {
function resetTryEntry (line 13838) | function resetTryEntry(entry) {
function Context (line 13845) | function Context(tryLocsList) {
function values (line 13885) | function values(iterable) {
function doneResult (line 13925) | function doneResult() {
function handle (line 13973) | function handle(loc, caught) {
function normalizeArray (line 14176) | function normalizeArray(parts, allowAboveRoot) {
function resolve (line 14214) | function resolve() {
function normalize (line 14241) | function normalize(path) {
function isAbsolute (line 14260) | function isAbsolute(path) {
function join (line 14264) | function join() {
function relative (line 14276) | function relative(from, to) {
function dirname (line 14320) | function dirname(path) {
function basename (line 14337) | function basename(path, ext) {
function extname (line 14346) | function extname(path) {
function filter (line 14362) | function filter(xs, f) {
function Diff (line 14532) | function Diff() {}
function done (line 14547) | function done(value) {
function execEditLength (line 14583) | function execEditLength() {
function buildValues (line 14729) | function buildValues(diff, components, newString, oldString, useLongestT...
function clonePath (line 14782) | function clonePath(path) {
function diffChars (line 14791) | function diffChars(oldStr, newStr, options) {
function generateOptions (line 14795) | function generateOptions(options, defaults) {
function diffWords (line 14856) | function diffWords(oldStr, newStr, options) {
function diffWordsWithSpace (line 14863) | function diffWordsWithSpace(oldStr, newStr, options) {
function diffLines (line 14895) | function diffLines(oldStr, newStr, callback) {
function diffTrimmedLines (line 14899) | function diffTrimmedLines(oldStr, newStr, callback) {
function diffSentences (line 14912) | function diffSentences(oldStr, newStr, callback) {
function diffCss (line 14922) | function diffCss(oldStr, newStr, callback) {
function _typeof (line 14926) | function _typeof(obj) {
function _toConsumableArray (line 14942) | function _toConsumableArray(arr) {
function _arrayWithoutHoles (line 14946) | function _arrayWithoutHoles(arr) {
function _iterableToArray (line 14950) | function _iterableToArray(iter) {
function _unsupportedIterableToArray (line 14954) | function _unsupportedIterableToArray(o, minLen) {
function _arrayLikeToArray (line 14963) | function _arrayLikeToArray(arr, len) {
function _nonIterableSpread (line 14973) | function _nonIterableSpread() {
function diffJson (line 14998) | function diffJson(oldObj, newObj, options) {
function canonicalize (line 15004) | function canonicalize(obj, stack, replacementStack, replacer, key) {
function diffArrays (line 15081) | function diffArrays(oldArr, newArr, callback) {
function parsePatch (line 15085) | function parsePatch(uniDiff) {
function distanceIterator (line 15243) | function distanceIterator(start, minLine, maxLine) {
function applyPatch (line 15284) | function applyPatch(source, uniDiff) {
function applyPatches (line 15420) | function applyPatches(uniDiff, options) {
function structuredPatch (line 15453) | function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHe...
function formatPatch (line 15580) | function formatPatch(diff) {
function createTwoFilesPatch (line 15611) | function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, o...
function createPatch (line 15615) | function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, opt...
function arrayEqual (line 15619) | function arrayEqual(a, b) {
function arrayStartsWith (line 15627) | function arrayStartsWith(array, start) {
function calcLineCount (line 15641) | function calcLineCount(hunk) {
function merge (line 15659) | function merge(mine, theirs, base) {
function loadPatch (line 15735) | function loadPatch(param, base) {
function fileNameChanged (line 15751) | function fileNameChanged(patch) {
function selectField (line 15755) | function selectField(index, mine, theirs) {
function hunkBefore (line 15767) | function hunkBefore(test, check) {
function cloneHunk (line 15771) | function cloneHunk(hunk, offset) {
function mergeLines (line 15781) | function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) {
function mutualChange (line 15838) | function mutualChange(hunk, mine, their) {
function removal (line 15868) | function removal(hunk, mine, their, swap) {
function conflict (line 15881) | function conflict(hunk, mine, their) {
function insertLeading (line 15890) | function insertLeading(hunk, insert, their) {
function insertTrailing (line 15898) | function insertTrailing(hunk, insert) {
function collectChange (line 15905) | function collectChange(state) {
function collectContext (line 15927) | function collectContext(state, matchChanges) {
function allRemoves (line 15982) | function allRemoves(changes) {
function skipRemoveSuperset (line 15988) | function skipRemoveSuperset(state, removeChanges, delta) {
function calcOldNewLineCount (line 16001) | function calcOldNewLineCount(lines) {
function convertChangesToDMP (line 16041) | function convertChangesToDMP(changes) {
function convertChangesToXML (line 16063) | function convertChangesToXML(changes) {
function escapeHTML (line 16087) | function escapeHTML(s) {
function parse$1 (line 16167) | function parse$1(str) {
function fmtShort$1 (line 16242) | function fmtShort$1(ms) {
function fmtLong$1 (line 16272) | function fmtLong$1(ms) {
function plural$1 (line 16298) | function plural$1(ms, msAbs, n, name) {
function emptyRepresentation (line 21043) | function emptyRepresentation(value, typeHint) {
function jsonStringify (line 21189) | function jsonStringify(object, spaces, depth) {
function withStack (line 21293) | function withStack(value, fn) {
function isMochaInternal (line 21387) | function isMochaInternal(line) {
function isNodeInternal (line 21391) | function isNodeInternal(line) {
function Pending (line 21603) | function Pending(message) {
function parse (line 21652) | function parse(str) {
function fmtShort (line 21727) | function fmtShort(ms) {
function fmtLong (line 21757) | function fmtLong(ms) {
function plural (line 21783) | function plural(ms, msAbs, n, name) {
function setup (line 21793) | function setup(env) {
function useColors (line 22100) | function useColors() {
function formatArgs (line 22128) | function formatArgs(args) {
function save (line 22176) | function save(namespaces) {
function load (line 22195) | function load() {
function localstorage (line 22223) | function localstorage() {
function createNoFilesMatchPatternError (line 22483) | function createNoFilesMatchPatternError(message, pattern) {
function createInvalidReporterError (line 22499) | function createInvalidReporterError(message, reporter) {
function createInvalidInterfaceError (line 22516) | function createInvalidInterfaceError(message, ui) {
function createUnsupportedError$2 (line 22532) | function createUnsupportedError$2(message) {
function createMissingArgumentError$1 (line 22549) | function createMissingArgumentError$1(message, argument, expected) {
function createInvalidArgumentTypeError$1 (line 22564) | function createInvalidArgumentTypeError$1(message, argument, expected) {
function createInvalidArgumentValueError (line 22585) | function createInvalidArgumentValueError(message, argument, value, reaso...
function createInvalidExceptionError$2 (line 22603) | function createInvalidExceptionError$2(message, value) {
function createFatalError$1 (line 22620) | function createFatalError$1(message, value) {
function createInvalidLegacyPluginError (line 22639) | function createInvalidLegacyPluginError(message, pluginType, pluginId) {
function createInvalidPluginError (line 22664) | function createInvalidPluginError() {
function createMochaInstanceAlreadyDisposedError (line 22677) | function createMochaInstanceAlreadyDisposedError(message, cleanReference...
function createMochaInstanceAlreadyRunningError (line 22692) | function createMochaInstanceAlreadyRunningError(message, instance) {
function createMultipleDoneError$1 (line 22709) | function createMultipleDoneError$1(runnable, originalErr) {
function createForbiddenExclusivityError$1 (line 22748) | function createForbiddenExclusivityError$1(mocha) {
function createInvalidPluginDefinitionError (line 22763) | function createInvalidPluginDefinitionError(msg, pluginDef) {
function createInvalidPluginImplementationError (line 22781) | function createInvalidPluginImplementationError(msg) {
function createTimeoutError$1 (line 22802) | function createTimeoutError$1(msg, timeout, file) {
function createUnparsableFileError (line 22819) | function createUnparsableFileError(message, filename) {
function Runnable (line 22889) | function Runnable(title, fn) {
function multiple (line 23163) | function multiple(err) {
function done (line 23173) | function done(err) {
function callFn (line 23247) | function callFn(fn) {
function callFnAsync (line 23269) | function callFnAsync(fn) {
function Hook (line 23366) | function Hook(title, fn) {
function Suite (line 23483) | function Suite(title, parentContext, isRoot) {
function doReset (line 23529) | function doReset(thingToReset) {
function cleanArrReferences (line 24023) | function cleanArrReferences(arr) {
function Runner (line 24298) | function Runner(suite, opts) {
function next (line 24650) | function next(i) {
function next (line 24764) | function next(suite) {
function hookErr (line 24881) | function hookErr(_, errSuite, after) {
function next (line 24906) | function next(err, errSuite) {
function next (line 25058) | function next(errSuite) {
function done (line 25094) | function done(errSuite) {
function runAsync (line 25368) | function runAsync() {
function filterLeaks (line 25425) | function filterLeaks(ok, globals) {
function isError (line 25470) | function isError(err) {
function thrown2Error (line 25483) | function thrown2Error(err) {
function getBrowserWindowSize (line 25512) | function getBrowserWindowSize() {
function stringifyDiffObjs (line 25656) | function stringifyDiffObjs(err) {
function Base (line 25798) | function Base(runner, options) {
function pad (line 25880) | function pad(str, len) {
function inlineDiff (line 25894) | function inlineDiff(actual, expected) {
function unifiedDiff (line 25922) | function unifiedDiff(actual, expected) {
function errorDiff (line 25963) | function errorDiff(actual, expected) {
function colorLines (line 25986) | function colorLines(name, str) {
function sameType (line 26006) | function sameType(a, b) {
function Dot (line 26046) | function Dot(runner, options) {
function Doc (line 26123) | function Doc(runner, options) {
function TAP (line 26204) | function TAP(runner, options) {
function title (line 26250) | function title(test) {
function println (line 26262) | function println(format, varArgs) {
function createProducer (line 26277) | function createProducer(tapVersion) {
function TAPProducer (line 26302) | function TAPProducer() {}
function TAP12Producer (line 26389) | function TAP12Producer() {
function TAP13Producer (line 26425) | function TAP13Producer() {
function JSONReporter (line 26507) | function JSONReporter(runner) {
function clean (line 26573) | function clean(test) {
function cleanCycles (line 26599) | function cleanCycles(obj) {
function errorJSON (line 26623) | function errorJSON(err) {
function Progress (line 26784) | function Progress() {
function HTML (line 26938) | function HTML(runner, options) {
function makeUrl (line 27103) | function makeUrl(s) {
function error (line 27156) | function error(msg) {
function fragment (line 27166) | function fragment(html) {
function hideSuitesWithout (line 27190) | function hideSuitesWithout(classname) {
function unhide (line 27206) | function unhide() {
function text (line 27221) | function text(el, contents) {
function on (line 27233) | function on(el, event, fn) {
function List (line 27279) | function List(runner, options) {
function Min (line 27345) | function Min(runner, options) {
function Spec (line 27399) | function Spec(runner, options) {
function NyanCat (line 27484) | function NyanCat(runner, options) {
function draw (line 27549) | function draw(type, n) {
function write (line 27718) | function write(string) {
function XUnit (line 27764) | function XUnit(runner, options) {
function tag (line 27891) | function tag(name, attrs, close, content) {
function Markdown (line 27949) | function Markdown(runner, options) {
function Progress (line 28061) | function Progress(runner, options) {
function Landing (line 28170) | function Landing(runner, options) {
function JSONStream (line 28260) | function JSONStream(runner, options) {
function writeEvent (line 28295) | function writeEvent(event) {
function clean (line 28308) | function clean(test) {
function isPermitted (line 28433) | function isPermitted() {
function canNotify (line 28469) | function canNotify(value) {
function display (line 28486) | function display(runner) {
function notPermitted (line 28533) | function notPermitted(err) {
function createStatsCollector (line 28615) | function createStatsCollector(runner) {
function Test (line 28672) | function Test(title, fn) {
function shouldBeTested (line 28790) | function shouldBeTested(suite) {
function visit (line 29273) | function visit(obj, file) {
function Context (line 29333) | function Context() {}
function Mocha (line 29636) | function Mocha() {
function runGlobalSetup (line 30760) | function runGlobalSetup() {
function runGlobalTeardown (line 30811) | function runGlobalTeardown() {
function _runGlobalFixtures (line 30917) | function _runGlobalFixtures() {
function timeslice (line 31119) | function timeslice() {
Condensed preview — 352 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,865K chars).
[
{
"path": ".github/workflows/static.yml",
"chars": 1375,
"preview": "# Simple workflow for deploying static content to GitHub Pages\nname: Deploy static content to Pages\n\non:\n # Runs on pus"
},
{
"path": ".gitignore",
"chars": 42,
"preview": ".DS_Store\n*.pem\npublic/analytics.template\n"
},
{
"path": ".hintrc",
"chars": 578,
"preview": "{\n \"extends\": [\n \"development\"\n ],\n \"hints\": {\n \"compat-api/html\": [\n \"default\",\n {\n \"ignore\":"
},
{
"path": "LICENSE.md",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2024 Joeri Sebrechts\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 1797,
"preview": "# Plain Vanilla\n\nA website demonstrating how to do web development using only vanilla techniques: no tools, no framework"
},
{
"path": "eslint.config.cjs",
"chars": 499,
"preview": "/* eslint-disable no-undef */\nconst globals = require(\"globals\");\nconst js = require(\"@eslint/js\");\n\nmodule.exports = [\n"
},
{
"path": "public/blog/archive.html",
"chars": 1103,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Plain Vanilla Blog</title>\n <meta charset=\"utf-8\">\n <meta name="
},
{
"path": "public/blog/articles/2024-08-17-lets-build-a-blog/card.html",
"chars": 483,
"preview": "<ul class=\"cards\">\n <li class=\"card\">\n <img src=\"./articles/2024-08-17-lets-build-a-blog/image.webp\" aria-hidd"
},
{
"path": "public/blog/articles/2024-08-17-lets-build-a-blog/example.html",
"chars": 746,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>A spiffy title!</title>\n <meta name=\"viewport\" content=\"width=devi"
},
{
"path": "public/blog/articles/2024-08-17-lets-build-a-blog/generator.js",
"chars": 2467,
"preview": "customElements.define('blog-generator', class BlogGenerator extends HTMLElement {\n\n // ...\n\n async processArticle("
},
{
"path": "public/blog/articles/2024-08-17-lets-build-a-blog/index.html",
"chars": 13575,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Let's build a blog, vanilla-style!</title>\n <meta name=\"viewport\" "
},
{
"path": "public/blog/articles/2024-08-25-vanilla-entity-encoding/example1.js",
"chars": 454,
"preview": "class MyComponent extends HTMLElement {\n connectedCallback() {\n const btn = `<button>${this.getAttribute('foo'"
},
{
"path": "public/blog/articles/2024-08-25-vanilla-entity-encoding/example2.js",
"chars": 726,
"preview": "function htmlEncode(s) {\n return s.replace(/[&<>'\"]/g,\n tag => ({\n '&': '&',\n '<': '"
},
{
"path": "public/blog/articles/2024-08-25-vanilla-entity-encoding/example3.js",
"chars": 497,
"preview": "import { html } from './html.js';\n\nclass MyComponent extends HTMLElement {\n connectedCallback() {\n const btn ="
},
{
"path": "public/blog/articles/2024-08-25-vanilla-entity-encoding/html.js",
"chars": 971,
"preview": "class Html extends String { }\n\n/** \n * tag a string as html not to be encoded\n * @param {string} str\n * @returns {string"
},
{
"path": "public/blog/articles/2024-08-25-vanilla-entity-encoding/index.html",
"chars": 7684,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Vanilla entity encoding</title>\n <meta name=\"viewport\" content=\"wi"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/adder.html",
"chars": 339,
"preview": "<!doctype html>\n<head>\n <title>Adder example</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-s"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/adder.js",
"chars": 695,
"preview": "import { signal, computed } from './signals.js';\n\ncustomElements.define('x-adder', class extends HTMLElement {\n a = s"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/index.html",
"chars": 6349,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Poor man's signals</title>\n <meta name=\"viewport\" content=\"width=d"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/preact-example.js",
"chars": 402,
"preview": "import { signal, computed, effect } from \"@preact/signals\";\n\nconst name = signal(\"Jane\");\nconst surname = signal(\"Doe\");"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals.js",
"chars": 940,
"preview": "export class Signal extends EventTarget {\n #value;\n get value () { return this.#value; }\n set value (value) {\n "
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals1-use.js",
"chars": 131,
"preview": "const name = new Signal('Jane');\nname.addEventListener('change', () => console.log(name.value));\nname.value = 'John';\n//"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals1.js",
"chars": 326,
"preview": "class Signal extends EventTarget {\n #value;\n get value () { return this.#value; }\n set value (value) {\n "
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals2-use.js",
"chars": 121,
"preview": "const name = signal('Jane');\nname.effect(() => console.log(name.value));\n// Logs: Jane\nname.value = 'John';\n// Logs: Joh"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals2.js",
"chars": 595,
"preview": "class Signal extends EventTarget {\n #value;\n get value () { return this.#value; }\n set value (value) {\n "
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals3-use.js",
"chars": 344,
"preview": "const name = signal('Jane');\nconst surname = signal('Doe');\nconst fullName = computed(() => `${name} ${surname}`, [name,"
},
{
"path": "public/blog/articles/2024-08-30-poor-mans-signals/signals3.js",
"chars": 317,
"preview": "class Computed extends Signal {\n constructor (fn, deps) {\n super(fn(...deps));\n for (const dep of deps)"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/adder.svelte",
"chars": 153,
"preview": "<script>\n export let a;\n export let b;\n</script>\n\n<input type=\"number\" bind:value={a}>\n<input type=\"number\" bind:value"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/bind.js",
"chars": 4336,
"preview": "/**\n* Render a template (element or string) to an html fragment \n* while connecting it up to data using Vue-style shorth"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/bind1.js",
"chars": 1181,
"preview": "export const bind = (template) => {\n const fragment = template.content.cloneNode(true);\n // iterate over all nodes"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/bind2-partial.js",
"chars": 263,
"preview": "export const bind = (template, target) => {\n if (!template.content) {\n const text = template;\n template"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/bind3-partial.js",
"chars": 869,
"preview": "// check for custom event listener attributes\nif (attr.name.startsWith('@')) {\n const event = attr.name.slice(1);\n "
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/bind4-partial.js",
"chars": 1272,
"preview": "// ...\n if (attr.name.startsWith(':')) {\n // extract the name and value of the attribute/property\n let "
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-bind3/bind.js",
"chars": 2414,
"preview": "export const bind = (template, target) => {\n if (!template.content) {\n const text = template;\n template"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-bind3/example.html",
"chars": 380,
"preview": "<!doctype html>\n<head>\n <title>Binding example</title>\n <meta name=\"viewport\" content=\"width=device-width, initial"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-bind3/example.js",
"chars": 1391,
"preview": "import { bind } from './bind.js';\nimport { signal } from './signals.js';\n\ncustomElements.define('x-example', class Examp"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-bind3/signals.js",
"chars": 940,
"preview": "export class Signal extends EventTarget {\n #value;\n get value () { return this.#value; }\n set value (value) {\n "
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-combined/adder.js",
"chars": 677,
"preview": "import { bind } from './bind.js';\nimport { signal, computed } from './signals.js';\nimport { html } from './html.js';\n\ncu"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-combined/bind.js",
"chars": 4133,
"preview": "/**\n* Render a template (element or string) to an html fragment \n* while connecting it up to data using Vue-style shorth"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-combined/example.html",
"chars": 386,
"preview": "<!doctype html>\n<head>\n <title>Binding example</title>\n <meta name=\"viewport\" content=\"width=device-width, initial"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-combined/html.js",
"chars": 971,
"preview": "class Html extends String { }\n\n/** \n * tag a string as html not to be encoded\n * @param {string} str\n * @returns {string"
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/example-combined/signals.js",
"chars": 940,
"preview": "export class Signal extends EventTarget {\n #value;\n get value () { return this.#value; }\n set value (value) {\n "
},
{
"path": "public/blog/articles/2024-09-03-unix-philosophy/index.html",
"chars": 13584,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>A unix philosophy for web development</title>\n <meta name=\"viewpor"
},
{
"path": "public/blog/articles/2024-09-06-how-fast-are-web-components/index.html",
"chars": 14685,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>How fast are web components?</title>\n <meta name=\"viewport\" conten"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/error-boundary-partial.html",
"chars": 217,
"preview": "<x-error-boundary>\n <p slot=\"error\">Something went wrong</p>\n <x-suspense>\n <p slot=\"fallback\">Loading...</"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/error-boundary.js",
"chars": 1841,
"preview": "export class ErrorBoundary extends HTMLElement {\n\n static showError(sender, error) {\n if (!error) throw new Er"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/error-boundary.js",
"chars": 2493,
"preview": "/**\n * A vanilla version of react-error-boundary ( https://github.com/bvaughn/react-error-boundary )\n * \n * Usage:\n * \n "
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/error-message.js",
"chars": 457,
"preview": "class ErrorMessage extends HTMLElement {\n connectedCallback() {\n this.update();\n }\n\n static get observed"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/hello-world.js",
"chars": 906,
"preview": "import { Suspense } from '../suspense.js';\nimport { later } from './later.js';\n\nclass HelloWorldComponent extends HTMLEl"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/hello-world/later.js",
"chars": 119,
"preview": "export function later(delay) {\n return new Promise(function(resolve) {\n setTimeout(resolve, delay);\n });\n}"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/lazy.js",
"chars": 2342,
"preview": "import { Suspense } from './suspense.js';\n\n/**\n * A vanilla version of React's lazy() function\n * inspired by https://cs"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/components/suspense.js",
"chars": 2490,
"preview": "import { ErrorBoundary } from './error-boundary.js';\n\n/**\n * A vanilla version of React's Suspense\n * \n * Usage:\n * \n * "
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/index.html",
"chars": 549,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Lazy, Suspense and Error Boundary example</title>\n <me"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/example/index.js",
"chars": 1772,
"preview": "import { registerLazy } from './components/lazy.js';\nimport { registerSuspense } from './components/suspense.js';\nimport"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/index.html",
"chars": 9572,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Sweet Suspense</title>\n <meta name=\"viewport\" content=\"width=devic"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/lazy1.js",
"chars": 516,
"preview": "customElements.define('x-lazy', class extends HTMLElement {\n connectedCallback() {\n this.style.display = 'cont"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/lazy2-partial.js",
"chars": 487,
"preview": " #loadElement(element) {\n // strip leading x- off the name\n const cleanName = element.localName.replace"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/lazy3-partial.js",
"chars": 377,
"preview": " #loadLazy() {\n const elements = \n [...this.children].filter(_ => _.localName.includes('-'));\n "
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/suspense1-partial.html",
"chars": 117,
"preview": "<x-suspense>\n <p slot=\"fallback\">Loading...</p>\n <x-lazy><x-hello-world></x-hello-world></x-lazy>\n</x-suspense>"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/suspense1.js",
"chars": 811,
"preview": "export class Suspense extends HTMLElement {\n #fallbackSlot;\n #contentSlot;\n\n set loading(isLoading) {\n i"
},
{
"path": "public/blog/articles/2024-09-09-sweet-suspense/suspense2-partial.js",
"chars": 706,
"preview": " static waitFor(sender, ...promises) {\n const suspense = sender.closest('x-suspense');\n if (suspense) s"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/defined/example.html",
"chars": 1197,
"preview": "<!doctype html>\n<html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-sc"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/defined2/example.html",
"chars": 5830,
"preview": "<!doctype html>\n<html>\n <head>\n <title>defined custom elements</title>\n <meta name=\"viewport\" content=\""
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/index.html",
"chars": 16207,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>The life and times of a web component</title>\n <meta name=\"viewpor"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/observer/example.html",
"chars": 1552,
"preview": "<!doctype html>\n<html> \n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-s"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/shadowed/example.html",
"chars": 1954,
"preview": "<!doctype html>\n<html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-sc"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/undefined/example.css",
"chars": 1297,
"preview": "body { font-family: system-ui, sans-serif; margin: 1em; }\nbutton { user-select: none; }\n/* based on https://github.com/a"
},
{
"path": "public/blog/articles/2024-09-16-life-and-times-of-a-custom-element/undefined/example.html",
"chars": 404,
"preview": "<!doctype html>\n<html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-sc"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/AddTask.js",
"chars": 557,
"preview": "customElements.define('task-add', class extends HTMLElement {\n connectedCallback() {\n this.innerHTML = `\n "
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/App.js",
"chars": 314,
"preview": "customElements.define('tasks-app', class extends HTMLElement {\n connectedCallback() {\n this.innerHTML = `\n "
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TaskList.js",
"chars": 3601,
"preview": "customElements.define('task-list', class extends HTMLElement {\n get context() { return this.closest('tasks-context');"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/TasksContext.js",
"chars": 1287,
"preview": "customElements.define('tasks-context', class extends HTMLElement {\n #tasks = structuredClone(initialTasks);\n get t"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/index.html",
"chars": 323,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/index.js",
"chars": 280,
"preview": "import './App.js';\nimport './AddTask.js';\nimport './TaskList.js';\nimport './TasksContext.js';\n\nconst render = () => {\n "
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/complete/styles.css",
"chars": 520,
"preview": "* {\n box-sizing: border-box;\n}\n\nbody {\n font-family: sans-serif;\n margin: 20px;\n padding: 0;\n}\n\nh1 {\n margin-top: 0"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/index.html",
"chars": 13083,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>The unreasonable effectiveness of vanilla JS</title>\n <meta name=\""
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/package.json",
"chars": 379,
"preview": "{\n \"name\": \"react.dev\",\n \"version\": \"0.0.0\",\n \"main\": \"/src/index.js\",\n \"scripts\": {\n \"start\": \"react-scripts sta"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/public/index.html",
"chars": 218,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, in"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/AddTask.js",
"chars": 553,
"preview": "import { useState } from 'react';\nimport { useTasksDispatch } from './TasksContext.js';\n\nexport default function AddTask"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/App.js",
"chars": 290,
"preview": "import AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksProvider } from './TasksContext."
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TaskList.js",
"chars": 1504,
"preview": "import { useState } from 'react';\nimport { useTasks, useTasksDispatch } from './TasksContext.js';\n\nexport default functi"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/TasksContext.js",
"chars": 1357,
"preview": "import { createContext, useContext, useReducer } from 'react';\n\nconst TasksContext = createContext(null);\n\nconst TasksDi"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/index.js",
"chars": 256,
"preview": "import React, { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport \"./styles.css\";\n\nimport"
},
{
"path": "public/blog/articles/2024-09-28-unreasonable-effectiveness-of-vanilla-js/react/src/styles.css",
"chars": 520,
"preview": "* {\n box-sizing: border-box;\n}\n\nbody {\n font-family: sans-serif;\n margin: 20px;\n padding: 0;\n}\n\nh1 {\n margin-top: 0"
},
{
"path": "public/blog/articles/2024-09-30-lived-experience/index.html",
"chars": 14588,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Lived experience</title>\n <meta name=\"viewport\" content=\"width=dev"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/context-provider.js",
"chars": 1225,
"preview": "export class ContextProvider extends EventTarget {\n #value;\n get value() { return this.#value }\n set value(v) {"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/context-request.js",
"chars": 309,
"preview": "export class ContextRequestEvent extends Event {\n constructor(context, callback, subscribe) {\n super('context-"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/index.css",
"chars": 574,
"preview": "body {\n margin: 1em;\n font-family: system-ui, sans-serif;\n}\n\ntheme-panel {\n display: block;\n border: 1px dot"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/index.html",
"chars": 299,
"preview": "<!doctype html>\n<html>\n<head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>tiny-c"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/index.js",
"chars": 1990,
"preview": "import { ContextRequestEvent } from \"./context-request.js\";\nimport \"./theme-provider.js\"; // global provider on body\nimp"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/theme-context.js",
"chars": 438,
"preview": "import { ContextProvider } from \"./context-provider.js\";\n\ncustomElements.define('theme-context', class extends HTMLEleme"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/combined/theme-provider.js",
"chars": 370,
"preview": "// loaded with <script type=\"module\" src=\"theme-provider.js\"></script>\n\nimport { ContextProvider } from \"./context-provi"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/context-provider.js",
"chars": 1225,
"preview": "export class ContextProvider extends EventTarget {\n #value;\n get value() { return this.#value }\n set value(v) {"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/context-request-1.js",
"chars": 540,
"preview": "class ContextRequestEvent extends Event {\n constructor(context, callback, subscribe) {\n super('context-request"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/context-request-2.js",
"chars": 283,
"preview": "customElements.define('my-component', class extends HTMLElement {\n connectedCallback() {\n let theme = 'light';"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/context-request-3.js",
"chars": 409,
"preview": "customElements.define('my-component', class extends HTMLElement {\n #unsubscribe;\n connectedCallback() {\n th"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/context-request-4.js",
"chars": 347,
"preview": "customElements.define('my-component', class extends HTMLElement {\n connectedCallback() {\n let theme = 'light';"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/index.html",
"chars": 6568,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Needs more context</title>\n <meta name=\"viewport\" content=\"width=d"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/theme-context-fragment.html",
"chars": 94,
"preview": "<theme-context>\n <div>\n <my-subscriber></my-subscriber>\n </div>\n</theme-context>\n"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/theme-context.js",
"chars": 380,
"preview": "customElements.define('theme-context', class extends HTMLElement {\n themeProvider = new ContextProvider(this, 'theme'"
},
{
"path": "public/blog/articles/2024-10-07-needs-more-context/theme-provider.js",
"chars": 370,
"preview": "// loaded with <script type=\"module\" src=\"theme-provider.js\"></script>\n\nimport { ContextProvider } from \"./context-provi"
},
{
"path": "public/blog/articles/2024-10-20-editing-plain-vanilla/.hintrc",
"chars": 211,
"preview": "{\n \"extends\": [\n \"development\"\n ],\n \"hints\": {\n \"compat-api/html\": [\n \"default\",\n {\n \"ignore\":"
},
{
"path": "public/blog/articles/2024-10-20-editing-plain-vanilla/eslint.config.cjs",
"chars": 499,
"preview": "/* eslint-disable no-undef */\nconst globals = require(\"globals\");\nconst js = require(\"@eslint/js\");\n\nmodule.exports = [\n"
},
{
"path": "public/blog/articles/2024-10-20-editing-plain-vanilla/index.html",
"chars": 8758,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Editing Plain Vanilla</title>\n <meta charset=\"utf-8\">\n <meta na"
},
{
"path": "public/blog/articles/2024-12-16-caching-vanilla-sites/index.html",
"chars": 10339,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Caching vanilla sites</title>\n <meta charset=\"utf-8\">\n <meta na"
},
{
"path": "public/blog/articles/2024-12-16-caching-vanilla-sites/sw.js",
"chars": 2109,
"preview": "let cacheName = 'cache-worker-v1';\n// these are automatically cached when the site is first loaded\nlet initialAssets = ["
},
{
"path": "public/blog/articles/2025-01-01-new-years-resolve/example-index.js",
"chars": 171,
"preview": "import { registerAvatarComponent } from './components/avatar.js';\nconst app = () => {\n registerAvatarComponent();\n}\nd"
},
{
"path": "public/blog/articles/2025-01-01-new-years-resolve/index.html",
"chars": 8014,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>New year's resolve</title>\n <meta charset=\"utf-8\">\n <meta name="
},
{
"path": "public/blog/articles/2025-01-01-new-years-resolve/layout.js",
"chars": 411,
"preview": "class Layout extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n "
},
{
"path": "public/blog/articles/2025-01-01-new-years-resolve/layout.tsx",
"chars": 195,
"preview": "import styles from './styles.module.css'\n \nexport default function Layout({\n children,\n}: {\n children: React.ReactNode"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo1.html",
"chars": 639,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo1.js",
"chars": 162,
"preview": "customElements.define('my-hello', class extends HTMLElement {\n connectedCallback() {\n this.textContent = `Hell"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo2.html",
"chars": 639,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo2.js",
"chars": 178,
"preview": "customElements.define('my-hello', class extends HTMLElement {\n connectedCallback() {\n this.textContent = `Hell"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo3.html",
"chars": 943,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo3.js",
"chars": 356,
"preview": "customElements.define('my-hello', class extends HTMLElement {\n get value() {\n return this.getAttribute('value'"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo4.html",
"chars": 892,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo4.js",
"chars": 641,
"preview": "customElements.define('my-hello', class extends HTMLElement {\n get value() {\n return this.getAttribute('value'"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo5-before.js",
"chars": 263,
"preview": "// html:\n<my-hello value=\"world\"></my-hello>\n// js:\nconst myHello = document.querySelector('my-hello');\nmyHello.value = "
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo5.html",
"chars": 892,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/demo5.js",
"chars": 939,
"preview": "customElements.define('my-hello', class extends HTMLElement {\n get value() {\n return this.getAttribute('value'"
},
{
"path": "public/blog/articles/2025-04-21-attribute-property-duality/index.html",
"chars": 7505,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>The attribute/property duality</title>\n <meta charset=\"utf-8\">\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo1/index-partial.txt",
"chars": 235,
"preview": "<form>\n <p>\n My favorite colors are <input-inline name=\"color1\" value=\"green\"></input-inline> \n and <in"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo1/index.html",
"chars": 1307,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo1/input-inline.js",
"chars": 736,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n get value() {\n return this.getAttribut"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo2/index.html",
"chars": 1307,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo2/input-inline-partial.js",
"chars": 386,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #internals;\n\n /* ... */\n\n constructor()"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo2/input-inline.js",
"chars": 974,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #internals;\n\n get value() {\n return"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo3/index.html",
"chars": 1277,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo3/input-inline-partial.js",
"chars": 2431,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #shouldFireChange = false;\n \n /* ... */"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo3/input-inline.js",
"chars": 3365,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #shouldFireChange = false;\n #internals;\n\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo4/index.html",
"chars": 2648,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo4/input-inline-partial.js",
"chars": 1957,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n /* ... */\n\n #formDisabled = false;\n #va"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo4/input-inline.js",
"chars": 4723,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #shouldFireChange = false;\n #internals;\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo5/index.html",
"chars": 2381,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo5/input-inline.css",
"chars": 1909,
"preview": "/* default styling has lowest priority */\n@layer {\n :root {\n --input-inline-border-color: light-dark(rgb(118, "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo5/input-inline.js",
"chars": 4862,
"preview": "customElements.define('input-inline', class extends HTMLElement {\n \n #shouldFireChange = false;\n #internals;\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo6/index-partial.txt",
"chars": 210,
"preview": "<form>\n <p>\n My favorite color is <input-inline name=\"color1\" value=\"green\" required></input-inline>.\n </p>"
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo6/index.html",
"chars": 1781,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width="
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo6/input-inline-partial.js",
"chars": 2669,
"preview": "let VALUE_MISSING_MESSAGE = 'Please fill out this field.';\n(() => {\n const input = document.createElement('input');\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo6/input-inline.css",
"chars": 1909,
"preview": "/* default styling has lowest priority */\n@layer {\n :root {\n --input-inline-border-color: light-dark(rgb(118, "
},
{
"path": "public/blog/articles/2025-05-09-form-control/demo6/input-inline.js",
"chars": 7214,
"preview": "let VALUE_MISSING_MESSAGE = 'Please fill out this field.';\n(() => {\n const input = document.createElement('input');\n "
},
{
"path": "public/blog/articles/2025-05-09-form-control/index.html",
"chars": 10546,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Making a new form control</title>\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example1/index.html",
"chars": 825,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 1</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example1/index.js",
"chars": 292,
"preview": "function transition() {\n const square1 = document.getElementById('square1');\n if (document.startViewTransition) {\n"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example1/transitions.css",
"chars": 91,
"preview": "#square1 {\n background-color: orange;\n}\n#square1.toggled {\n background-color: blue;\n}"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example2/index.html",
"chars": 1089,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 2</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example2/index.js",
"chars": 442,
"preview": "function transition() {\n const square1 = document.getElementById('square1');\n const square2 = document.getElementB"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example2/transitions.css",
"chars": 334,
"preview": "#square2 {\n background-color: green;\n view-transition-name: slide;\n display: none;\n}\n#square2.toggled {\n dis"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example3/index.html",
"chars": 1548,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 3</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example3/index.js",
"chars": 270,
"preview": "import { startTransition } from './view-transition.js';\n\nexport function transition() {\n startTransition(() => {\n "
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example3/transitions.css",
"chars": 334,
"preview": "#square2 {\n background-color: green;\n view-transition-name: slide;\n display: none;\n}\n#square2.toggled {\n dis"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example3/view-transition.js",
"chars": 374,
"preview": "export const startTransition = (updateCallback) => {\n if (document.startViewTransition) {\n document.startViewT"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example4/index.html",
"chars": 1778,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 4</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example4/index.js",
"chars": 1167,
"preview": "import { startTransition } from './view-transition.js';\n\nlet currentRoute = '';\n\nexport function navigate() {\n curren"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example4/transitions.css",
"chars": 318,
"preview": ".route {\n display: none;\n}\n.route.active {\n display: inline-block;\n}\n.route#route2 {\n view-transition-name: sli"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example4/view-transition.js",
"chars": 374,
"preview": "export const startTransition = (updateCallback) => {\n if (document.startViewTransition) {\n document.startViewT"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example5/index.html",
"chars": 1778,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 5</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example5/index.js",
"chars": 1167,
"preview": "import { startTransition } from './view-transition.js';\n\nlet currentRoute = '';\n\nexport function navigate() {\n curren"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example5/transitions.css",
"chars": 318,
"preview": ".route {\n display: none;\n}\n.route.active {\n display: inline-block;\n}\n.route#route2 {\n view-transition-name: sli"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example5/view-transition-part.js",
"chars": 1650,
"preview": "// the currently animating view transition\nlet currentTransition = null;\n// the next transition to run (after currentTra"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example5/view-transition.js",
"chars": 4874,
"preview": "// the currently animating view transition\nlet currentTransition = null;\n// the next transition to run (after currentTra"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/index.html",
"chars": 448,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-widt"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/lib/html.js",
"chars": 971,
"preview": "class Html extends String { }\n\n/** \n * tag a string as html not to be encoded\n * @param {string} str\n * @returns {string"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/lib/view-route.js",
"chars": 4086,
"preview": "export const routerEvents = new EventTarget();\n\n// update routes on popstate (browser back/forward)\nexport const handleP"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/lib/view-transition.js",
"chars": 8557,
"preview": "let nextVTId = 0;\n\ncustomElements.define('view-transition', class extends HTMLElement {\n #defaultName = 'VT_' + nextV"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/App.js",
"chars": 424,
"preview": "import '../lib/view-transition.js';\nimport './Home.js';\nimport './Details.js';\n\ncustomElements.define('demo-app', class "
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/Details.js",
"chars": 2971,
"preview": "import { startTransition } from '../lib/view-transition.js';\nimport { html } from '../lib/html.js';\nimport { ChevronLeft"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/Home.js",
"chars": 3892,
"preview": "import { routerEvents } from \"../lib/view-route.js\";\nimport { startTransition } from \"../lib/view-transition.js\";\nimport"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/Icons.js",
"chars": 4608,
"preview": "export function ChevronLeft() {\n return (`\n <svg\n class=\"chevron-left\"\n xmlns=\"http://www.w3.org/2000/svg\""
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/Layout.js",
"chars": 1346,
"preview": "import { transitionEvents } from \"../lib/view-transition.js\";\n\ncustomElements.define('demo-page', class extends HTMLElem"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/LikeButton.js",
"chars": 935,
"preview": "import { Heart } from './Icons.js';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survi"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/Videos.js",
"chars": 1539,
"preview": "import { startTransition } from '../lib/view-transition.js';\nimport { PlayIcon, PauseIcon } from './Icons.js';\nimport '."
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/animations.css",
"chars": 2244,
"preview": "/* Slide animations for details content */\n::view-transition-old(details-fallback):only-child {\n animation: 150ms eas"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/data.js",
"chars": 1704,
"preview": "const videos = [\n {\n id: '1',\n title: 'First video',\n description: 'Video description',\n image: 'blue',\n }"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/index.js",
"chars": 1272,
"preview": "import { interceptNavigation, routerEvents, handlePopState, pushState } from '../lib/view-route.js';\nimport { startTrans"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/example6/src/styles.css",
"chars": 10546,
"preview": "* {\n box-sizing: border-box;\n}\n\nbody {\n font-family: sans-serif;\n margin: 20px;\n padding: 0;\n}\n\nh1 {\n margin-top: 0"
},
{
"path": "public/blog/articles/2025-06-12-view-transitions/index.html",
"chars": 13843,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Bringing React's <ViewTransition> to vanilla JS</title>\n <me"
},
{
"path": "public/blog/articles/2025-06-25-routing/example1/app.js",
"chars": 938,
"preview": "import { routerEvents, interceptNavigation } from './view-route.js';\n\ncustomElements.define('demo-app', class extends HT"
},
{
"path": "public/blog/articles/2025-06-25-routing/example1/index.html",
"chars": 324,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 1</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-25-routing/example1/view-route.js",
"chars": 745,
"preview": "export const routerEvents = new EventTarget();\n\nexport const interceptNavigation = (root) => {\n // convert link click"
},
{
"path": "public/blog/articles/2025-06-25-routing/example2/app.js",
"chars": 1060,
"preview": "import { routerEvents, interceptNavigation, matchesRoute } from './view-route.js';\n\ncustomElements.define('demo-app', cl"
},
{
"path": "public/blog/articles/2025-06-25-routing/example2/index.html",
"chars": 324,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 2</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-25-routing/example2/view-route-partial.js",
"chars": 376,
"preview": "const handleNavigate = (e) => {\n history.pushState(null, null, e.detail.url);\n routerEvents.dispatchEvent(new PopS"
},
{
"path": "public/blog/articles/2025-06-25-routing/example2/view-route-partial2.js",
"chars": 520,
"preview": "// ...\n\n// all routes will be relative to the document's base path\nconst baseURL = new URL(window.originalHref || docume"
},
{
"path": "public/blog/articles/2025-06-25-routing/example2/view-route.js",
"chars": 1604,
"preview": "export const routerEvents = new EventTarget();\n\n// all routes will be relative to the document's base path\nconst baseURL"
},
{
"path": "public/blog/articles/2025-06-25-routing/example3/app.js",
"chars": 753,
"preview": "import { interceptNavigation } from './view-route.js';\n\ncustomElements.define('demo-app', class extends HTMLElement {\n\n "
},
{
"path": "public/blog/articles/2025-06-25-routing/example3/index.html",
"chars": 324,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 3</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
},
{
"path": "public/blog/articles/2025-06-25-routing/example3/view-route-partial.js",
"chars": 1467,
"preview": "// ...\n\ncustomElements.define('view-route', class extends HTMLElement {\n\n #matches = [];\n\n get isActive() {\n "
},
{
"path": "public/blog/articles/2025-06-25-routing/example3/view-route.js",
"chars": 3064,
"preview": "export const routerEvents = new EventTarget();\n\n// all routes will be relative to the document's base path\nconst baseURL"
},
{
"path": "public/blog/articles/2025-06-25-routing/example4/404.html",
"chars": 594,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <script>\n var pathSegmentsToKeep = window.location.hostname === 'loca"
},
{
"path": "public/blog/articles/2025-06-25-routing/example4/index-partial.html",
"chars": 692,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <!-- ... -->\n</head>\n<body>\n <script type=\"text/javascript\">\n // p"
},
{
"path": "public/blog/articles/2025-06-25-routing/index.html",
"chars": 8664,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Clean client-side routing</title>\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "public/blog/articles/2025-07-13-history-architecture/index.html",
"chars": 25968,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>The history of web application architecture</title>\n <meta charset"
},
{
"path": "public/blog/articles/2025-07-16-local-first-architecture/example1.html",
"chars": 1634,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Latency simulator</title>\n <meta charset=\"utf-8\">\n <meta name=\""
},
{
"path": "public/blog/articles/2025-07-16-local-first-architecture/index.html",
"chars": 18719,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Local-first web application architecture</title>\n <meta charset=\"u"
},
{
"path": "public/blog/articles/2026-03-01-redesigning-plain-vanilla/index.html",
"chars": 6475,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Redesigning Plain Vanilla</title>\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "public/blog/articles/2026-03-01-redesigning-plain-vanilla/nav-menu.html",
"chars": 538,
"preview": "<nav id=\"menu-nav\" popover aria-label=\"main\">\n <ol>\n <li><a href=\"#\" aria-current=\"page\">Welcome</a></li>\n "
},
{
"path": "public/blog/articles/2026-03-09-details-matters/example1/index-partial.html",
"chars": 103,
"preview": "<details>\n <summary>A summary</summary>\n Some details to further explain the summary.\n</details>\n"
},
{
"path": "public/blog/articles/2026-03-09-details-matters/example1/index.html",
"chars": 362,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Example 1</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport"
}
]
// ... and 152 more files (download for full content)
About this extraction
This page contains the full source code of the jsebrech/plainvanilla GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 352 files (2.6 MB), approximately 694.6k tokens, and a symbol index with 1411 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.