Merge branch 'main' of github.com:ayoayco/web-component-base into feat/attach-effect

This commit is contained in:
Ayo 2023-11-30 16:35:33 +01:00
commit 91a245ee2f
16 changed files with 154 additions and 51 deletions

View file

@ -135,7 +135,7 @@ this.setAttribute('my-name','hello');
Therefore, this will tell the browser that the UI needs a render if the attribute is one of the component's observed attributes we explicitly provided with `static properties = ['my-name']`; Therefore, this will tell the browser that the UI needs a render if the attribute is one of the component's observed attributes we explicitly provided with `static properties = ['my-name']`;
> The `props` property of `WebComponent` works like `HTMLElement.dataset`, except `dataset` is only for attributes prefixed with `data-`. A camelCase counterpart using `props` will give read/write access to any attribute, with or without the `data-` prefix. > The `props` property of `WebComponent` works like `HTMLElement.dataset`, except `dataset` is only for attributes prefixed with `data-`. A camelCase counterpart using `props` will give read/write access to any attribute, with or without the `data-` prefix.
> However, note that like `HTMLElement.dataset`, values assigned to properties using `WebComponent.props` is always converted into string. This will be improved in later versions. > Another advantage over `HTMLElement.dataset` is that `WebComponent.props` can hold primitive types `number`, `boolean`, and `string`.
### Alternatives ### Alternatives

View file

@ -0,0 +1,20 @@
// @ts-check
import WebComponent from "../src/WebComponent.js";
export class Counter extends WebComponent {
static properties = ["count"];
onInit() {
this.props.count = 1;
let i = 1
this.onclick = ()=> ++this.props.count
let double = () => i * 2;
console.log(double());
i = 3;
console.log(double());
}
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);

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);

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);

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>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "web-component-base", "name": "web-component-base",
"version": "1.13.2", "version": "1.13.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "web-component-base", "name": "web-component-base",
"version": "1.13.2", "version": "1.13.3",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"site" "site"

View file

@ -1,6 +1,6 @@
{ {
"name": "web-component-base", "name": "web-component-base",
"version": "1.13.2", "version": "1.13.3",
"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", "main": "WebComponent.js",
"type": "module", "type": "module",

View file

@ -16,7 +16,7 @@
& a { & a {
border: 3px solid var(--color-fade); border: 3px solid var(--color-fade);
padding: 0.75em; padding: 0.5em 0.75em;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
@ -28,7 +28,7 @@
&#primary { &#primary {
background: #3054bf; background: #3054bf;
color: white; color: white;
width: 40%; min-width: 35%;
} }
} }
} }

View file

@ -25,11 +25,11 @@
<span slot="description">{{ project.description }}</span> <span slot="description">{{ project.description }}</span>
</awesome-header> </awesome-header>
<main> <main>
<section style="font-size: larger;"> <section style="font-size: large;">
<p> <p>
By extending our <code-block inline>WebComponent</code-block> base class, you get an easy authoring experience as you would expect in writing your components</p> By extending our base class, you get an easy authoring experience as you would expect in writing your components:</p>
<ul> <ul>
<li>A signals-like <code-block inline>props</code-block> API that keeps your property changes and UI in sync</li> <li>A <code-block inline>props</code-block> API that synchronizes your components' property values and HTML attributes</li>
<li>Sensible life-cycle hooks that you understand and remember</li> <li>Sensible life-cycle hooks that you understand and remember</li>
<li>Extensible templates & renderer (examples in-progress)</li> <li>Extensible templates & renderer (examples in-progress)</li>
<li>Provided out-of-the-box with <a href="https://ayco.io/gh/McFly">McFly</a>, a powerful no-framework framework</li> <li>Provided out-of-the-box with <a href="https://ayco.io/gh/McFly">McFly</a>, a powerful no-framework framework</li>

View file

@ -93,68 +93,73 @@ export class WebComponent extends HTMLElement {
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] = {
attributeChanged: true, this.#handleUpdateProp(camelCaps, currentValue);
value: this[property],
}
this.render(); this.render();
this.onChanges({ property, previousValue, currentValue }); this.onChanges({ property, previousValue, currentValue });
} }
} }
#handleUpdateProp(key, value) {
const restored = this.#restoreType(value, this.#typeMap[key]);
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 = {} #typeMap = {};
#handler = (setter, typeMap) => ({
set(obj, prop, value) {
const attributeChanged = value?.attributeChanged ?? false;
let newValue = attributeChanged ? value.value : value;
const oldValue = obj[prop];
console.log(">>>", newValue, oldValue); #restoreType = (value, type) => {
switch (type) {
if (!(prop in typeMap)) { case "string":
typeMap[prop] = typeof newValue; return value;
case "number":
case "boolean":
return JSON.parse(value);
default:
return value;
} }
};
obj[prop] = restoreType(newValue, typeMap[prop]) #handler(setter, typeMap) {
const getKebab = (str) => {
if (attributeChanged && !oldValue != newValue) {
const kebab = getKebab(prop);
setter(kebab, newValue);
}
function getKebab(str) {
return str.replace( return str.replace(
/[A-Z]+(?![a-z])|[A-Z]/g, /[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase() ($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
); );
};
return {
set(obj, prop, value) {
const oldValue = obj[prop];
if (!(prop in typeMap)) {
typeMap[prop] = typeof value;
} }
function restoreType(value, type) { if (oldValue !== value) {
switch(type) { obj[prop] = value;
case 'string': return value; const kebab = getKebab(prop);
case 'number': return parseInt(value); setter(kebab, value);
case 'boolean': return JSON.parse(value);
default: return value
} }
}
return true; return true;
}, },
}); };
}
#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.#typeMap) this.#handler(
(key, value) => this.setAttribute(key, value),
this.#typeMap
)
); );
} }
} }