feat: remove attach-effect

This commit is contained in:
Ayo Ayco 2025-03-09 00:34:10 +01:00
parent d41affd709
commit 9ead44babe
12 changed files with 2135 additions and 681 deletions

View file

@ -52,9 +52,9 @@ The result is a reactive UI on property changes.
## Project Status ## Project Status
It is ready for many cases we see people use custom elements for. If you have a cool project built on **WebComponent.io** we'd love to know! :) It is ready for many simple cases we see people use custom elements for. If you have a cool project built on **WebComponent.io** we'd love to know! :)
For building some advanced interactions, we have a few issues that are still open: [#24 smart diffing](https://github.com/ayoayco/web-component-base/issues/24) & [#4 attachEffect improvements](https://github.com/ayoayco/web-component-base/issues/4) For building advanced interactions, we have an in-progress work on smart diffing to prevent component children being wiped on interaction.
In the mean time, if you have some complex needs, we recommend using the `WebComponent` base class with a more mature rendering approach like `lit-html`... and here's a demo for that: [View on CodePen ↗](https://codepen.io/ayoayco-the-styleful/pen/ZEwNJBR?editors=1010). In the mean time, if you have some complex needs, we recommend using the `WebComponent` base class with a more mature rendering approach like `lit-html`... and here's a demo for that: [View on CodePen ↗](https://codepen.io/ayoayco-the-styleful/pen/ZEwNJBR?editors=1010).
@ -91,15 +91,13 @@ You can import everything separately, or in a single file each for the main expo
```js ```js
// all in a single file // all in a single file
import { WebComponent, html, attachEffect } from 'web-component-base' import { WebComponent, html } from 'web-component-base'
// in separate files // in separate files
import { WebComponent } from 'web-component-base/WebComponent.js' import { WebComponent } from 'web-component-base/WebComponent.js'
import { html } from 'web-component-base/html.js' import { html } from 'web-component-base/html.js'
import { attachEffect } from 'web-component-base/attach-effect.js'
``` ```
### Utilities ### Utilities
@ -221,7 +219,6 @@ Some feature-specific demos:
1. [Using dynamic style objects](https://codepen.io/ayoayco-the-styleful/pen/bGzXjwQ?editors=1010) 1. [Using dynamic style objects](https://codepen.io/ayoayco-the-styleful/pen/bGzXjwQ?editors=1010)
1. [Using the Shadow DOM](https://codepen.io/ayoayco-the-styleful/pen/VwRYVPv?editors=1010) 1. [Using the Shadow DOM](https://codepen.io/ayoayco-the-styleful/pen/VwRYVPv?editors=1010)
1. [Using tagged templates in your vanilla custom element](https://codepen.io/ayoayco-the-styleful/pen/bGzJQJg?editors=1010) 1. [Using tagged templates in your vanilla custom element](https://codepen.io/ayoayco-the-styleful/pen/bGzJQJg?editors=1010)
1. [Using attachEffect (experimental)](https://codepen.io/ayoayco-the-styleful/pen/ExrdWPv?editors=1011)
## `template` vs `render()` ## `template` vs `render()`
@ -448,9 +445,9 @@ class ClickableText extends WebComponent {
All the functions and the base class in the library are minimalist by design and only contains what is needed for their purpose. All the functions and the base class in the library are minimalist by design and only contains what is needed for their purpose.
As of v2.0.0, the main export (with `WebComponent` + `html` + `attachEffect`) is 1.7 kB (min + gzip) according to [bundlephobia.com](https://bundlephobia.com/package/web-component-base@2.0.0), and the `WebComponent` base class is just 1.1 kB (min + brotli) according to [size-limit](http://github.com/ai/size-limit). As of v3, the main export (with `WebComponent` + `html`) is 1.7 kB (min + gzip) according to [bundlephobia.com](https://bundlephobia.com/package/web-component-base@2.0.0), and the `WebComponent` base class is just 1.08 kB (min + brotli) according to [size-limit](http://github.com/ai/size-limit).
There is an increase in size compared to that of before this release, primarily because of advanced features (e.g., effects, html tagged templates, and props blueprints) in building complex applications. There is an increase in size compared to that of before this release, primarily because of advanced features (e.g., html tagged templates, and props blueprints) in building complex applications.
## Inspirations and thanks ## Inspirations and thanks

View file

@ -1,23 +0,0 @@
// @ts-check
import { WebComponent, attachEffect, html } from '../../src/index.js'
export class Counter extends WebComponent {
static props = {
count: 0,
}
onInit() {
attachEffect(this.props.count, (count) => console.log(count))
}
afterViewInit() {
attachEffect(this.props.count, (count) => console.log(count + 100))
}
get template() {
return html`<button onclick=${() => ++this.props.count} id="btn">
${this.props.count}
</button>`
}
}
customElements.define('my-counter', Counter)

View file

@ -1,24 +0,0 @@
// @ts-check
import { WebComponent, attachEffect, html } from '../../src/index.js'
export class Decrease extends WebComponent {
static props = {
count: 999,
}
onInit() {
attachEffect(this.props.count, (count) => console.log(count))
}
afterViewInit() {
attachEffect(this.props.count, (count) => console.log(count + 100))
}
get template() {
return html`<button onclick=${() => --this.props.count} id="btn">
${this.props.count}
</button>`
}
}
customElements.define('my-decrement', Decrease)

View file

@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WC demo</title>
<script type="module" src="./Counter.mjs"></script>
<script type="module" src="./Decrease.mjs"></script>
</head>
<body>
<h1>Attach Effect Test</h1>
<my-counter></my-counter>
<my-decrement></my-decrement>
</body>
</html>

View file

@ -1,36 +1,44 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta charset="UTF-8" />
<title>WC demo</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="./Counter.mjs"></script> <title>WC demo</title>
<script type="module" src="./Toggle.mjs"></script> <script type="module" src="./Counter.mjs"></script>
<script type="module" src="./HelloWorld.mjs"></script> <script type="module" src="./Toggle.mjs"></script>
<script type="module" src="./Object.mjs"></script> <script type="module" src="./HelloWorld.mjs"></script>
<style> <script type="module" src="./Object.mjs"></script>
* { <style>
font-size: larger; * {
} font-size: larger;
</style> }
</head> </style>
<body> </head>
<div>Counter: <my-counter></my-counter></div>
<div>Toggle: <my-toggle></my-toggle></div> <body>
<div>String: <my-hello-world></my-hello-world></div> <div>Counter: <my-counter></my-counter></div>
<div> <div>Toggle: <my-toggle></my-toggle></div>
<my-object></my-object> <div>String: <my-hello-world></my-hello-world></div>
<p id="display-panel"></p> <div>
</div> <my-object></my-object>
<script type="module"> <p id="display-panel"></p>
import { attachEffect } from '../../src/index.js' </div>
const myObjectEl = document.querySelector('my-object') <script type="module">
const objectProp = myObjectEl.props.object /**
const displayPanelEl = document.querySelector('#display-panel') * TODO: fix using custom events
displayPanelEl.textContent = JSON.stringify(objectProp) */
attachEffect(objectProp, (object) => {
displayPanelEl.textContent = JSON.stringify(object) // import { attachEffect } from '../../src/index.js'
}) // const myObjectEl = document.querySelector('my-object')
</script> // const objectProp = myObjectEl.props.object
</body> // const displayPanelEl = document.querySelector('#display-panel')
</html> // displayPanelEl.textContent = JSON.stringify(objectProp)
// attachEffect(objectProp, (object) => {
// displayPanelEl.textContent = JSON.stringify(object)
// })
// console.log(JSON.stringify(object))
</script>
</body>
</html>

View file

@ -101,5 +101,8 @@
"path": "./dist/utils/get-kebab-case.js", "path": "./dist/utils/get-kebab-case.js",
"limit": "0.5 KB" "limit": "0.5 KB"
} }
] ],
"dependencies": {
"release-it": "^18.1.2"
}
} }

File diff suppressed because it is too large Load diff

View file

@ -25,13 +25,6 @@ class FeatureSet extends WebComponent {
'Use the built-in JSX-like syntax or bring your own custom templating', 'Use the built-in JSX-like syntax or bring your own custom templating',
url: 'https://codepen.io/ayoayco-the-styleful/pen/ZEwNJBR?editors=1010', url: 'https://codepen.io/ayoayco-the-styleful/pen/ZEwNJBR?editors=1010',
}, },
{
icon: '️🛜',
title: 'Powerful.',
description:
"Attach 'side effects' that gets triggered on property value changes",
url: '',
},
] ]
/** /**

View file

@ -1,126 +1,122 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<!-- <!--
Hello! This page is an example McFly page. Hello! This page is an example McFly page.
See more on https://ayco.io/gh/McFly See more on https://ayco.io/gh/McFly
--> -->
<my-head> <my-head>
<title>WebComponent.io: Web Components in Easy Mode</title> <title>WebComponent.io: Web Components in Easy Mode</title>
<link rel="stylesheet" href="prism.css" /> <link rel="stylesheet" href="prism.css" />
<script src="prism.js" defer></script> <script src="prism.js" defer></script>
<script server:setup> <script server:setup>
const project = { const project = {
name: 'WebComponent.io', name: 'WebComponent.io',
description: 'A simple reactivity system for web components', description: 'A simple reactivity system for web components',
}; };
const author = { const author = {
name: 'Ayo Ayco', name: 'Ayo Ayco',
url: 'https://ayco.io', url: 'https://ayco.io',
year: '2023', year: '2023',
}; };
</script> </script>
<style> <style>
@counter-style publish-icons { @counter-style publish-icons {
system: cyclic; system: cyclic;
symbols: '️✅'; symbols: '️✅';
suffix: ' '; suffix: ' ';
} }
main {
font-size: large;
& section ul { main {
list-style: publish-icons; font-size: large;
& li { & section ul {
margin-bottom: 0.5em; list-style: publish-icons;
}
}
& section.jumbo { & li {
& .hero-statement { margin-bottom: 0.5em;
font-size: 2em;
text-align: center;
}
}
& code-block {
overflow: auto;
} }
} }
</style>
</my-head> & section.jumbo {
<body> & .hero-statement {
<awesome-header> font-size: 2em;
<span>{{ project.name }}</span> text-align: center;
<span slot="description">{{ project.description }}</span> }
</awesome-header> }
<main>
<section class="jumbo"> & code-block {
<p class="hero-statement"> overflow: auto;
Build lightweight custom elements that browsers & }
<em>you</em> understand. }
</p> </style>
<call-to-action></call-to-action> </my-head>
<div>
<feature-set> <body>
<h2>Features</h2> <awesome-header>
<ul> <span>{{ project.name }}</span>
<li> <span slot="description">{{ project.description }}</span>
A robust API for synchronizing your component's UI and </awesome-header>
properties <main>
</li> <section class="jumbo">
<li> <p class="hero-statement">
~1 kB base class (minified, compressed) with versatile utilities Build lightweight custom elements that browsers &
</li> <em>you</em> understand.
<li> </p>
Sensible life-cycle hooks that you understand and remember <call-to-action></call-to-action>
</li> <div>
<li> <feature-set>
Use the built-in JSX-like syntax or bring your own custom <h2>Features</h2>
templating <ul>
</li> <li>
<li> A robust API for synchronizing your component's UI and
Attach 'side effects' that gets triggered on property value properties
changes </li>
</li> <li>
</ul> ~1 kB base class (minified, compressed) with versatile utilities
</feature-set> </li>
</div> <li>
</section> Sensible life-cycle hooks that you understand and remember
<section> </li>
<h2>Why use this base class?</h2> <li>
<p> Use the built-in JSX-like syntax or bring your own custom
Often times, when simple websites need a quick templating
<a </li>
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements" </ul>
target="_blank" </feature-set>
>custom element</a </div>
>, the best way is still to create one extending from </section>
<code-block inline>HTMLElement</code-block>. However, it can quickly <section>
reach a point where writing the code from scratch can seem confusing <h2>Why use this base class?</h2>
and hard to maintain especially when compared to other projects with <p>
more advanced setups. Often times, when simple websites need a quick
</p> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements"
<p> target="_blank">custom element</a>, the best way is still to create one extending from
Also, when coming from frameworks with an easy declarative coding <code-block inline>HTMLElement</code-block>. However, it can quickly
experience (using templates), the default imperative way (e.g., reach a point where writing the code from scratch can seem confusing
creating instances of elements, manually attaching event handlers, and and hard to maintain especially when compared to other projects with
other DOM manipulations) is a frequent pain point we see. more advanced setups.
</p> </p>
<p> <p>
This project aims to address these with virtually zero tooling Also, when coming from frameworks with an easy declarative coding
required and thin abstractions from vanilla custom element APIs experience (using templates), the default imperative way (e.g.,
giving you only the bare minimum code to be productive. creating instances of elements, manually attaching event handlers, and
</p> other DOM manipulations) is a frequent pain point we see.
<p> </p>
It works on current-day browsers without needing compilers, <p>
transpilers, or polyfills. This project aims to address these with virtually zero tooling
</p> required and thin abstractions from vanilla custom element APIs
<p> giving you only the bare minimum code to be productive.
Here's an interactive custom element: </p>
<my-counter></my-counter> <p>
</p> It works on current-day browsers without needing compilers,
<code-block language="js"> transpilers, or polyfills.
<pre> </p>
<p>
Here's an interactive custom element:
<my-counter></my-counter>
</p>
<code-block language="js">
<pre>
import { WebComponent, html } from &quot;https://esm.sh/web-component-base&quot;; import { WebComponent, html } from &quot;https://esm.sh/web-component-base&quot;;
export class Counter extends WebComponent { export class Counter extends WebComponent {
@ -136,20 +132,18 @@ export class Counter extends WebComponent {
} }
} }
customElements.define(&quot;my-counter&quot;, Counter);</pre customElements.define(&quot;my-counter&quot;, Counter);</pre>
> </code-block>
</code-block> </section>
</section> </main>
</main> <my-footer>
<my-footer> <small>
<small> <a href="https://ayco.io/sh/wcb/tree/main/site">Website</a>
<a href="https://ayco.io/sh/wcb/tree/main/site" built with <a href="https://mcfly.js.org">McFly</a>.<br />
>Website</a Copyright &copy; {{author.year}}
> <a href="{{ author.url }}">{{ author.name }}</a>.
built with <a href="https://mcfly.js.org">McFly</a>.<br /> </small>
Copyright &copy; {{author.year}} </my-footer>
<a href="{{ author.url }}">{{ author.name }}</a>. </body>
</small>
</my-footer> </html>
</body>
</html>

View file

@ -20,7 +20,6 @@ export class WebComponent extends HTMLElement {
#prevDOM #prevDOM
#props #props
#typeMap = {} #typeMap = {}
#effectsMap = {}
/** /**
* Array of strings that tells the browsers which attributes will cause a render * Array of strings that tells the browsers which attributes will cause a render
@ -130,7 +129,6 @@ export class WebComponent extends HTMLElement {
} }
#handler(setter, meta) { #handler(setter, meta) {
const effectsMap = meta.#effectsMap
const typeMap = meta.#typeMap const typeMap = meta.#typeMap
return { return {
@ -141,12 +139,7 @@ export class WebComponent extends HTMLElement {
typeMap[prop] = typeof value typeMap[prop] = typeof value
} }
if (value.attach === 'effect') { if (typeMap[prop] !== typeof value) {
if (!effectsMap[prop]) {
effectsMap[prop] = []
}
effectsMap[prop].push(value.callback)
} else if (typeMap[prop] !== typeof value) {
throw TypeError( throw TypeError(
`Cannot assign ${typeof value} to ${ `Cannot assign ${typeof value} to ${
typeMap[prop] typeMap[prop]
@ -154,7 +147,6 @@ export class WebComponent extends HTMLElement {
) )
} else if (oldValue !== value) { } else if (oldValue !== value) {
obj[prop] = value obj[prop] = value
effectsMap[prop]?.forEach((f) => f(value))
const kebab = getKebabCase(prop) const kebab = getKebabCase(prop)
setter(kebab, serialize(value)) setter(kebab, serialize(value))
} }

View file

@ -1,13 +0,0 @@
/**
* Attach a "side effect" function that gets triggered on property value changes
* @param {Object} obj
* @param {(newValue: any) => void} callback
*/
export function attachEffect(obj, callback) {
const { proxy, prop } = Object.getPrototypeOf(obj)
proxy[prop] = {
attach: 'effect',
callback,
}
}

View file

@ -1,3 +1,2 @@
export { attachEffect } from './attach-effect.js'
export { WebComponent } from './WebComponent.js' export { WebComponent } from './WebComponent.js'
export { html } from './html.js' export { html } from './html.js'