feat: preserve props type

This commit is contained in:
Ayo 2023-11-26 03:43:41 +01:00
parent 0209095d0a
commit 6027f664b3
6 changed files with 146 additions and 43 deletions

View file

@ -87,75 +87,85 @@ export class WebComponent extends HTMLElement {
this.onDestroy(); this.onDestroy();
} }
/**
* @param {string} property
* @param {any} previousValue
* @param {any} currentValue
*/
attributeChangedCallback(property, previousValue, currentValue) { attributeChangedCallback(property, previousValue, currentValue) {
const camelCaps = this.#getCamelCaps(property); const camelCaps = this.#getCamelCaps(property);
if (previousValue !== currentValue) { if (previousValue !== currentValue) {
this[property] = currentValue === "" || currentValue; this[property] = currentValue === "" || currentValue;
this[camelCaps] = this[property]; // remove on v2 this[camelCaps] = this[property]; // remove on v2
this.props[camelCaps] = this[property];
this.#handleUpdateProp(camelCaps, currentValue)
this.render(); this.render();
this.onChanges({ property, previousValue, currentValue }); this.onChanges({ property, previousValue, currentValue });
} }
} }
/** #handleUpdateProp(key, value) {
* Converts a kebab-cased string into camelCaps
* @param {string} kebab string in kebab-case const restored = this.#restoreType(value, this.#typeMap[key])
* @returns {string}
*/ if (restored !== this.props[key])
this.props[key] = value;
}
#getCamelCaps(kebab) { #getCamelCaps(kebab) {
return kebab.replace(/-./g, (x) => x[1].toUpperCase()); return kebab.replace(/-./g, (x) => x[1].toUpperCase());
} }
/** #typeMap = {};
* Proxy handler for observed attribute - property counterpart
* @param {(qualifiedName: string, value: string) => void} setter
* @returns
*/
#handler = (setter) => ({
set(obj, prop, newValue) {
const oldValue = obj[prop];
obj[prop] = newValue; #restoreType = (value, type) => {
switch (type) {
/** case "string":
* Converts camelCaps string into kebab-case return value;
* @param {string} str case "number":
* @returns {string} case "boolean":
*/ return JSON.parse(value);
const getKebab = (str) => default:
str.replace( return value;
/[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
);
if (oldValue != newValue) {
const kebab = getKebab(prop);
setter(kebab, newValue);
} }
};
return true; #handler(setter, typeMap) {
}, const getKebab = (str) => {
}); return str.replace(
/[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
);
};
return {
set(obj, prop, value) {
const oldValue = obj[prop];
if (!(prop in typeMap)) {
typeMap[prop] = typeof value;
}
if (oldValue !== value) {
obj[prop] = value;
const kebab = getKebab(prop);
setter(kebab, value);
}
return true;
}
};
}
/**
* Initialize the `props` proxy object
*/
#initializeProps() { #initializeProps() {
if (!this.#props) { if (!this.#props) {
this.#props = new Proxy( this.#props = new Proxy(
{}, {},
this.#handler((key, value) => this.setAttribute(key, value)) this.#handler(
(key, value) => this.setAttribute(key, value),
this.#typeMap
)
); );
} }
} }
} }
export default WebComponent; export default WebComponent;

15
type-restore/Counter.mjs Normal file
View file

@ -0,0 +1,15 @@
// @ts-check
import WebComponent from "../src/WebComponent.js";
export class Counter extends WebComponent {
static properties = ["count"];
onInit() {
this.props.count = 0;
this.onclick = ()=> ++this.props.count
}
get template() {
return `<button>${this.props.count}</button>`;
}
}
customElements.define("my-counter", Counter);

View file

@ -0,0 +1,14 @@
import WebComponent from "../src/WebComponent.js";
export class HelloWorld extends WebComponent {
static properties = ["name"];
onInit() {
this.props.name = 'a';
this.onclick = ()=> this.props.name += 'a'
}
get template() {
return `<button>W${this.props.name}h!</button>`;
}
}
customElements.define("my-hello-world", HelloWorld);

15
type-restore/Object.mjs Normal file
View file

@ -0,0 +1,15 @@
import WebComponent from "../src/WebComponent.js";
export class ObjectText extends WebComponent {
static properties = ["object"];
onInit() {
this.props.object = {
hello: 'world'
};
}
get template() {
return `<textarea>${this.props.object}</textarea>`;
}
}
customElements.define("my-object", ObjectText);

18
type-restore/Toggle.mjs Normal file
View file

@ -0,0 +1,18 @@
// @ts-check
import WebComponent from "../src/WebComponent.js";
export class Toggle extends WebComponent {
static properties = ["toggle"];
onInit() {
this.props.toggle = false;
this.onclick = ()=>this.handleToggle()
}
handleToggle() {
this.props.toggle = !this.props.toggle;
}
get template() {
return `<button id="toggle">${this.props.toggle ? 'On':'Off'}</button>`;
}
}
customElements.define("my-toggle", Toggle);

31
type-restore/index.html Normal file
View file

@ -0,0 +1,31 @@
<!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="Toggle.mjs"></script>
<script type="module" src="HelloWorld.mjs"></script>
<script type="module" src="Object.mjs"></script>
<style>
* {
font-size: larger
}
</style>
</head>
<body>
<div>
Counter: <my-counter></my-counter>
</div>
<div>
Toggle: <my-toggle></my-toggle>
</div>
<div>
String: <my-hello-world />
</div>
<div>
Object: <my-object />
</div>
</body>
</html>