Merge branch 'main' of github.com:ayoayco/web-component-base into feat/attach-effect
This commit is contained in:
commit
91a245ee2f
16 changed files with 154 additions and 51 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
20
examples/type-restore/Counter.mjs
Normal file
20
examples/type-restore/Counter.mjs
Normal 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);
|
14
examples/type-restore/HelloWorld.mjs
Normal file
14
examples/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
examples/type-restore/Object.mjs
Normal file
15
examples/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
examples/type-restore/Toggle.mjs
Normal file
18
examples/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
examples/type-restore/index.html
Normal file
31
examples/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>
|
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.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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -87,74 +87,79 @@ export class WebComponent extends HTMLElement {
|
||||||
this.onDestroy();
|
this.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
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] = {
|
|
||||||
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) {
|
||||||
|
case "string":
|
||||||
|
return value;
|
||||||
|
case "number":
|
||||||
|
case "boolean":
|
||||||
|
return JSON.parse(value);
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!(prop in typeMap)) {
|
#handler(setter, typeMap) {
|
||||||
typeMap[prop] = typeof newValue;
|
const getKebab = (str) => {
|
||||||
}
|
return str.replace(
|
||||||
|
/[A-Z]+(?![a-z])|[A-Z]/g,
|
||||||
|
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
obj[prop] = restoreType(newValue, typeMap[prop])
|
return {
|
||||||
|
set(obj, prop, value) {
|
||||||
|
const oldValue = obj[prop];
|
||||||
|
|
||||||
if (attributeChanged && !oldValue != newValue) {
|
if (!(prop in typeMap)) {
|
||||||
const kebab = getKebab(prop);
|
typeMap[prop] = typeof value;
|
||||||
setter(kebab, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getKebab(str) {
|
|
||||||
return str.replace(
|
|
||||||
/[A-Z]+(?![a-z])|[A-Z]/g,
|
|
||||||
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreType(value, type) {
|
|
||||||
switch(type) {
|
|
||||||
case 'string': return value;
|
|
||||||
case 'number': return parseInt(value);
|
|
||||||
case 'boolean': return JSON.parse(value);
|
|
||||||
default: return value
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (oldValue !== value) {
|
||||||
|
obj[prop] = value;
|
||||||
|
const kebab = getKebab(prop);
|
||||||
|
setter(kebab, 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
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue