feat: preserve props type
This commit is contained in:
parent
0209095d0a
commit
6027f664b3
6 changed files with 146 additions and 43 deletions
|
@ -87,75 +87,85 @@ export class WebComponent extends HTMLElement {
|
|||
this.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} property
|
||||
* @param {any} previousValue
|
||||
* @param {any} currentValue
|
||||
*/
|
||||
attributeChangedCallback(property, previousValue, currentValue) {
|
||||
const camelCaps = this.#getCamelCaps(property);
|
||||
|
||||
if (previousValue !== currentValue) {
|
||||
this[property] = currentValue === "" || currentValue;
|
||||
this[camelCaps] = this[property]; // remove on v2
|
||||
this.props[camelCaps] = this[property];
|
||||
|
||||
this.#handleUpdateProp(camelCaps, currentValue)
|
||||
|
||||
this.render();
|
||||
this.onChanges({ property, previousValue, currentValue });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a kebab-cased string into camelCaps
|
||||
* @param {string} kebab string in kebab-case
|
||||
* @returns {string}
|
||||
*/
|
||||
#handleUpdateProp(key, value) {
|
||||
|
||||
const restored = this.#restoreType(value, this.#typeMap[key])
|
||||
|
||||
if (restored !== this.props[key])
|
||||
this.props[key] = value;
|
||||
}
|
||||
|
||||
#getCamelCaps(kebab) {
|
||||
return kebab.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
#typeMap = {};
|
||||
|
||||
obj[prop] = newValue;
|
||||
|
||||
/**
|
||||
* Converts camelCaps string into kebab-case
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
const getKebab = (str) =>
|
||||
str.replace(
|
||||
/[A-Z]+(?![a-z])|[A-Z]/g,
|
||||
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
|
||||
);
|
||||
|
||||
if (oldValue != newValue) {
|
||||
const kebab = getKebab(prop);
|
||||
setter(kebab, newValue);
|
||||
#restoreType = (value, type) => {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return value;
|
||||
case "number":
|
||||
case "boolean":
|
||||
return JSON.parse(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
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() {
|
||||
if (!this.#props) {
|
||||
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
15
type-restore/Counter.mjs
Normal 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);
|
14
type-restore/HelloWorld.mjs
Normal file
14
type-restore/HelloWorld.mjs
Normal 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
15
type-restore/Object.mjs
Normal 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
18
type-restore/Toggle.mjs
Normal 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
31
type-restore/index.html
Normal 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>
|
Loading…
Reference in a new issue