feat: attach-effect (#2)
We are now able to attach "side effects" to property value changes, from inside the component and outside.
This commit is contained in:
parent
54a3819b83
commit
73dd374a3e
21 changed files with 186 additions and 42 deletions
12
README.md
12
README.md
|
@ -40,7 +40,7 @@ The result is a reactive UI on property changes. [View on CodePen ↗](https://c
|
||||||
Import using [unpkg](https://unpkg.com/web-component-base) in your vanilla JS component. We will use this in the rest of our [usage examples](#usage).
|
Import using [unpkg](https://unpkg.com/web-component-base) in your vanilla JS component. We will use this in the rest of our [usage examples](#usage).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation via npm
|
## Installation via npm
|
||||||
|
@ -56,7 +56,7 @@ In your component class:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// HelloWorld.mjs
|
// HelloWorld.mjs
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
|
|
||||||
class HelloWorld extends WebComponent {
|
class HelloWorld extends WebComponent {
|
||||||
static properties = ["my-name", "emotion"];
|
static properties = ["my-name", "emotion"];
|
||||||
|
@ -153,7 +153,7 @@ Here is an example of using a custom element in a single .html file.
|
||||||
<head>
|
<head>
|
||||||
<title>WC Base Test</title>
|
<title>WC Base Test</title>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
|
|
||||||
class HelloWorld extends WebComponent {
|
class HelloWorld extends WebComponent {
|
||||||
static properties = ["my-name"];
|
static properties = ["my-name"];
|
||||||
|
@ -186,7 +186,7 @@ Define behavior when certain events in the component's life cycle is triggered b
|
||||||
- Best for setting up the component
|
- Best for setting up the component
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
|
|
||||||
class ClickableText extends WebComponent {
|
class ClickableText extends WebComponent {
|
||||||
// gets called when the component is used in an HTML document
|
// gets called when the component is used in an HTML document
|
||||||
|
@ -223,7 +223,7 @@ class ClickableText extends WebComponent {
|
||||||
- best for undoing any setup done in `onInit()`
|
- best for undoing any setup done in `onInit()`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
|
|
||||||
class ClickableText extends WebComponent {
|
class ClickableText extends WebComponent {
|
||||||
|
|
||||||
|
@ -250,7 +250,7 @@ class ClickableText extends WebComponent {
|
||||||
- Triggered when an attribute value changed
|
- Triggered when an attribute value changed
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import WebComponent from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
import { WebComponent } from "https://unpkg.com/web-component-base@latest/WebComponent.min.js";
|
||||||
|
|
||||||
class ClickableText extends WebComponent {
|
class ClickableText extends WebComponent {
|
||||||
// gets called when an attribute value changes
|
// gets called when an attribute value changes
|
||||||
|
|
25
attach-effect/Counter.mjs
Normal file
25
attach-effect/Counter.mjs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// @ts-check
|
||||||
|
import WebComponent from "../src/WebComponent.js";
|
||||||
|
import { attachEffect } from "../src/attach-effect.js";
|
||||||
|
|
||||||
|
export class Counter extends WebComponent {
|
||||||
|
static properties = ["count"];
|
||||||
|
onInit() {
|
||||||
|
this.props.count = 0;
|
||||||
|
this.onclick = () => ++this.props.count;
|
||||||
|
attachEffect(
|
||||||
|
this.props.count,
|
||||||
|
(count) => console.log(count)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterViewInit(){
|
||||||
|
attachEffect(this.props.count, (count) => console.log(count + 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
get template() {
|
||||||
|
return `<button id="btn">${this.props.count}</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("my-counter", Counter);
|
13
attach-effect/index.html
Normal file
13
attach-effect/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Attach Effect Test</h1>
|
||||||
|
<my-counter></my-counter>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,4 +1,4 @@
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class BooleanPropTest extends WebComponent {
|
export class BooleanPropTest extends WebComponent {
|
||||||
static properties = ["is-inline", "anotherone"];
|
static properties = ["is-inline", "anotherone"];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class Counter extends WebComponent {
|
export class Counter extends WebComponent {
|
||||||
static properties = ["count"];
|
static properties = ["count"];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class HelloWorld extends WebComponent {
|
export class HelloWorld extends WebComponent {
|
||||||
static properties = ["count", "emotion"];
|
static properties = ["count", "emotion"];
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
import { WebComponent } from "../src/WebComponent.js";
|
|
||||||
|
|
||||||
class SimpleText extends WebComponent {
|
class SimpleText extends WebComponent {
|
||||||
clickCallback() {
|
clickCallback() {
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>WC demo</title>
|
<title>WC demo</title>
|
||||||
<script type="module" src="HelloWorld.mjs"></script>
|
<script type="module" src="./HelloWorld.mjs"></script>
|
||||||
<script type="module" src="SimpleText.mjs"></script>
|
<script type="module" src="./SimpleText.mjs"></script>
|
||||||
<script type="module" src="BooleanPropTest.mjs"></script>
|
<script type="module" src="./BooleanPropTest.mjs"></script>
|
||||||
<script type="module" src="Counter.mjs"></script>
|
<script type="module" src="./Counter.mjs"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<hello-world emotion="sad"></hello-world>
|
<hello-world emotion="sad"></hello-world>
|
||||||
|
|
58
examples/pens/counter-toggle.html
Normal file
58
examples/pens/counter-toggle.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<!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>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module">
|
||||||
|
import WebComponent from "https://unpkg.com/web-component-base@1.13.3/WebComponent.min.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://ayco.io/n/web-component-base
|
||||||
|
*/
|
||||||
|
class Counter extends WebComponent {
|
||||||
|
static properties = ["count"];
|
||||||
|
onInit() {
|
||||||
|
this.props.count = 0;
|
||||||
|
this.onclick = () => ++this.props.count;
|
||||||
|
}
|
||||||
|
onChanges(changes) {
|
||||||
|
console.log(changes);
|
||||||
|
// now click the button & check your devtools console
|
||||||
|
}
|
||||||
|
get template() {
|
||||||
|
return `<button>${this.props.count}</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Toggle extends WebComponent {
|
||||||
|
static properties = ["toggle"];
|
||||||
|
onInit() {
|
||||||
|
this.props.toggle = false;
|
||||||
|
this.onclick = () => (this.props.toggle = !this.props.toggle);
|
||||||
|
}
|
||||||
|
get template() {
|
||||||
|
return `<button>${this.props.toggle ? "On" : "Off"}</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("my-counter", Counter);
|
||||||
|
customElements.define("my-toggle", Toggle);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
Counter:
|
||||||
|
<my-counter />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Toggle:
|
||||||
|
<my-toggle />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,5 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class Counter extends WebComponent {
|
export class Counter extends WebComponent {
|
||||||
static properties = ["count"];
|
static properties = ["count"];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class HelloWorld extends WebComponent {
|
export class HelloWorld extends WebComponent {
|
||||||
static properties = ["name"];
|
static properties = ["name"];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class ObjectText extends WebComponent {
|
export class ObjectText extends WebComponent {
|
||||||
static properties = ["object"];
|
static properties = ["object"];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import WebComponent from "../src/WebComponent.js";
|
import WebComponent from "../../src/WebComponent.js";
|
||||||
|
|
||||||
export class Toggle extends WebComponent {
|
export class Toggle extends WebComponent {
|
||||||
static properties = ["toggle"];
|
static properties = ["toggle"];
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>WC demo</title>
|
<title>WC demo</title>
|
||||||
<script type="module" src="Counter.mjs"></script>
|
<script type="module" src="./Counter.mjs"></script>
|
||||||
<script type="module" src="Toggle.mjs"></script>
|
<script type="module" src="./Toggle.mjs"></script>
|
||||||
<script type="module" src="HelloWorld.mjs"></script>
|
<script type="module" src="./HelloWorld.mjs"></script>
|
||||||
<script type="module" src="Object.mjs"></script>
|
<script type="module" src="./Object.mjs"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
font-size: larger
|
font-size: larger
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "web-component-base",
|
"name": "web-component-base",
|
||||||
"version": "1.13.3",
|
"version": "1.14.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web-component-base",
|
"name": "web-component-base",
|
||||||
"version": "1.13.3",
|
"version": "1.14.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"site"
|
"site"
|
||||||
|
|
27
package.json
27
package.json
|
@ -1,9 +1,27 @@
|
||||||
{
|
{
|
||||||
"name": "web-component-base",
|
"name": "web-component-base",
|
||||||
"version": "1.13.3",
|
"version": "1.14.6",
|
||||||
"description": "A zero-dependency, ~600 Bytes (minified & gzipped), JS base class for creating reactive custom elements easily",
|
"description": "A zero-dependency, ~600 Bytes (minified & gzipped), JS base class for creating reactive custom elements easily",
|
||||||
"main": "WebComponent.js",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "WebComponent.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"import": "./index.js"
|
||||||
|
},
|
||||||
|
"./WebComponent": {
|
||||||
|
"types": "./WebComponent.d.ts",
|
||||||
|
"import": "./WebComponent.js"
|
||||||
|
},
|
||||||
|
"./WebComponent.min.js": {
|
||||||
|
"types": "./WebComponent.d.ts",
|
||||||
|
"import": "./WebComponent.min.js"
|
||||||
|
},
|
||||||
|
"./attach-effect": {
|
||||||
|
"types": "./attach-effect.d.ts",
|
||||||
|
"import": "./attach-effect.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx simple-server .",
|
"start": "npx simple-server .",
|
||||||
"demo": "npx simple-server .",
|
"demo": "npx simple-server .",
|
||||||
|
@ -14,9 +32,12 @@
|
||||||
"generate:types": "tsc --allowJs src/* --outDir dist --declaration --emitDeclarationOnly",
|
"generate:types": "tsc --allowJs src/* --outDir dist --declaration --emitDeclarationOnly",
|
||||||
"copy:meta": "node prepare.js && cp README.md ./dist && cp LICENSE ./dist",
|
"copy:meta": "node prepare.js && cp README.md ./dist && cp LICENSE ./dist",
|
||||||
"copy:source": "cp ./src/* ./dist",
|
"copy:source": "cp ./src/* ./dist",
|
||||||
"pub": "npm run clean && npm run build && cd ./dist && npm publish --access public",
|
"pub": "npm run clean && npm run build && cd ./dist && npm publish",
|
||||||
|
"pub:beta": "npm run clean && npm run build && cd ./dist && npm publish --tag beta",
|
||||||
"publish:patch": "npm version patch && npm run pub",
|
"publish:patch": "npm version patch && npm run pub",
|
||||||
|
"publish:patch:beta": "npm version patch && npm run pub:beta",
|
||||||
"publish:minor": "npm version minor && npm run pub",
|
"publish:minor": "npm version minor && npm run pub",
|
||||||
|
"publish:minor:beta": "npm version minor && npm run pub:beta",
|
||||||
"check:size": "npm run build && size-limit ./dist/WebComponent.js"
|
"check:size": "npm run build && size-limit ./dist/WebComponent.js"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/ayoayco/web-component-base",
|
"repository": "https://github.com/ayoayco/web-component-base",
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
shamefully-hoist=true
|
|
||||||
strict-peer-dependencies=false
|
|
|
@ -1,2 +1 @@
|
||||||
import McFly from "@mcflyjs/config";
|
export default defineNitroConfig({ extends: '@mcflyjs/config' });
|
||||||
export default defineNitroConfig({ ...McFly() });
|
|
||||||
|
|
|
@ -22,9 +22,9 @@ export class WebComponent extends HTMLElement {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only property containing camelCase counterparts of observed attributes.
|
* Read-only property containing camelCase counterparts of observed attributes.
|
||||||
|
* @typedef {{[name: string]: any}} PropStringMap
|
||||||
* @see https://www.npmjs.com/package/web-component-base#prop-access
|
* @see https://www.npmjs.com/package/web-component-base#prop-access
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
|
||||||
* @typedef {{[name: string]: any}} PropStringMap
|
|
||||||
* @type {PropStringMap}
|
* @type {PropStringMap}
|
||||||
*/
|
*/
|
||||||
get props() {
|
get props() {
|
||||||
|
@ -53,11 +53,12 @@ export class WebComponent extends HTMLElement {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when an attribute value changes
|
* Triggered when an attribute value changes
|
||||||
* @param {{
|
* @typedef {{
|
||||||
* property: string,
|
* property: string,
|
||||||
* previousValue: any,
|
* previousValue: any,
|
||||||
* currentValue: any
|
* currentValue: any
|
||||||
* }} changes
|
* }} Changes
|
||||||
|
* @param {Changes} changes
|
||||||
*/
|
*/
|
||||||
onChanges(changes) {}
|
onChanges(changes) {}
|
||||||
|
|
||||||
|
@ -94,7 +95,7 @@ export class WebComponent extends HTMLElement {
|
||||||
this[property] = currentValue === "" || currentValue;
|
this[property] = currentValue === "" || currentValue;
|
||||||
this[camelCaps] = this[property]; // remove on v2
|
this[camelCaps] = this[property]; // remove on v2
|
||||||
|
|
||||||
this.#handleUpdateProp(camelCaps, currentValue);
|
this.#handleUpdateProp(camelCaps, this[property]);
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.onChanges({ property, previousValue, currentValue });
|
this.onChanges({ property, previousValue, currentValue });
|
||||||
|
@ -125,7 +126,11 @@ export class WebComponent extends HTMLElement {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#handler(setter, typeMap) {
|
#effectsMap = {};
|
||||||
|
|
||||||
|
#handler(setter, meta) {
|
||||||
|
const effectsMap = meta.#effectsMap;
|
||||||
|
const typeMap = meta.#typeMap;
|
||||||
const getKebab = (str) => {
|
const getKebab = (str) => {
|
||||||
return str.replace(
|
return str.replace(
|
||||||
/[A-Z]+(?![a-z])|[A-Z]/g,
|
/[A-Z]+(?![a-z])|[A-Z]/g,
|
||||||
|
@ -141,14 +146,28 @@ export class WebComponent extends HTMLElement {
|
||||||
typeMap[prop] = typeof value;
|
typeMap[prop] = typeof value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldValue !== value) {
|
if (value.attach === "effect") {
|
||||||
|
if (!effectsMap[prop]) {
|
||||||
|
effectsMap[prop] = [];
|
||||||
|
}
|
||||||
|
effectsMap[prop].push(value.callback);
|
||||||
|
} else if (oldValue !== value) {
|
||||||
obj[prop] = value;
|
obj[prop] = value;
|
||||||
|
effectsMap[prop]?.forEach((f) => f(value));
|
||||||
const kebab = getKebab(prop);
|
const kebab = getKebab(prop);
|
||||||
setter(kebab, value);
|
setter(kebab, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
get(obj, prop) {
|
||||||
|
// TODO: handle non-objects
|
||||||
|
if(obj[prop] !== null && obj[prop] !== undefined) {
|
||||||
|
Object.getPrototypeOf(obj[prop]).proxy = meta.#props;
|
||||||
|
Object.getPrototypeOf(obj[prop]).prop = prop;
|
||||||
|
}
|
||||||
|
return obj[prop];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,10 +175,7 @@ export class WebComponent extends HTMLElement {
|
||||||
if (!this.#props) {
|
if (!this.#props) {
|
||||||
this.#props = new Proxy(
|
this.#props = new Proxy(
|
||||||
{},
|
{},
|
||||||
this.#handler(
|
this.#handler((key, value) => this.setAttribute(key, value), this)
|
||||||
(key, value) => this.setAttribute(key, value),
|
|
||||||
this.#typeMap
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/attach-effect.js
Normal file
13
src/attach-effect.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
};
|
||||||
|
}
|
2
src/index.js
Normal file
2
src/index.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export {attachEffect} from './attach-effect.js';
|
||||||
|
export {default as WebComponent} from './WebComponent.js';
|
Loading…
Reference in a new issue