feat: props Proxy for camelCase counterparts

- provide easy access to any observed attribute.
- update README examples
- update JSDoc examples
This commit is contained in:
Ayo 2023-11-17 23:10:02 +01:00
parent 003b72a1f2
commit 910a5096d0
6 changed files with 70 additions and 39 deletions

View file

@ -96,8 +96,10 @@ This mental model attempts to reduce the cognitive complexity of authoring compo
## Prop Access
Attributes are generally in `kebab-case`. You can access attribute properties in two ways
1. Use the camelCase counterpart: `this.myProp`, which is automatically filled.
A `WebComponent.props` read-only property exists to provide easy access to *any* observed attribute. This works like [`HTMLElement.dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) except `dataset` is only for attributes prefixed with `data-*`. Assigning a value to a camelCase counterpart using `WebComponent.props` will call `this.setAttribute` for any attribute name, with or without the `data-*` prefix.
You can access attribute properties in two ways:
1. Use the camelCase counterpart: `this.props.myProp`, which is automatically filled.
1. Or stick with kebab-case: `this["my-prop"]`
```js
@ -106,7 +108,7 @@ class HelloWorld extends WebComponent {
get template() {
return `
<h1>Hello ${this.myProp}</h1>
<h1>Hello ${this.props.myProp}</h1>
<h2>Hello ${this["my-prop"]}</h2>
`;
}

View file

@ -1,17 +1,10 @@
import WebComponent from "../src/WebComponent.js";
export class BooleanPropTest extends WebComponent {
isInline = false;
anotherone = false;
static properties = ["is-inline", "anotherone"];
onChanges(changes) {
console.log(">>> boolean prop test", changes);
}
get template() {
return `<p>is-inline: ${this.isInline}</p><p>another-one: ${this.anotherone}</p>`;
return `<p>is-inline: ${this.props.isInline}</p><p>another-one: ${this.props.anotherone}</p>`;
}
}

View file

@ -2,30 +2,16 @@
import WebComponent from "../src/index.js";
export class HelloWorld extends WebComponent {
dataName = "World";
emotion = "excited";
static properties = ["data-name", "emotion"];
static properties = ["my-name", "emotion"];
onInit() {
let count = 0;
this.onclick = () => {
this.setAttribute("data-name", `Clicked ${++count}x!`);
};
}
afterViewInit() {
console.log("afterViewInit", this.querySelector("h1"));
}
onChanges(changes) {
const { property, previousValue, currentValue } = changes;
console.log(`${property} changed`, { previousValue, currentValue });
this.onclick = () => (this.props.myName = `Clicked ${++count}`);
}
get template() {
return `<button id="btn">Hello ${this.dataName}${
this.emotion === "sad" ? ". 😭" : "! 🙌"
return `<button id="btn">Hello ${this.props.myName ?? "World"}${
this.props.emotion === "sad" ? ". 😭" : "! 🙌"
}</button>`;
}
}

View file

@ -16,7 +16,7 @@ class SimpleText extends WebComponent {
}
get template() {
return `<span>Click me!</span>`;
return `<span style="cursor:pointer">Click me!</span>`;
}
}

View file

@ -9,7 +9,7 @@
<script type="module" src="BooleanPropTest.mjs"></script>
</head>
<body>
<hello-world id="hey" data-name="Ayo" emotion="sad"></hello-world>
<hello-world></hello-world>
<p>
<simple-text></simple-text>
</p>

View file

@ -8,9 +8,6 @@
* import WebComponent from "https://unpkg.com/web-component-base/index.js";
*
* class HelloWorld extends WebComponent {
* // initialize prop
* dataName = 'World';
*
* // tell the browser which attributes to cause a render
* static properties = ["data-name", "emotion"];
*
@ -18,7 +15,7 @@
* // note: props have kebab-case & camelCase counterparts
* get template() {
* return `
* <h1>Hello ${this.dataName}${this.emotion === "sad" ? ". 😭" : "! 🙌"}</h1>`;
* <h1>Hello ${this.props.dataName}${this.props.emotion === "sad" ? ". 😭" : "! 🙌"}</h1>`;
* }
* }
*
@ -27,12 +24,15 @@
*/
export class WebComponent extends HTMLElement {
/**
* Array of strings that tells the browsers which attributes will cause a render
* @type Array<string>
*/
static properties = [];
/**
* Read-only string property that represents how the component will be rendered
* @returns string
* @see https://www.npmjs.com/package/web-component-base#template-vs-render
*/
get template() {
return "";
@ -43,19 +43,48 @@ export class WebComponent extends HTMLElement {
}
/**
* Triggered after view is initialized
* Proxy object containing camelCase counterparts of observed attributes.
* This works like HTMLElement.dataset except dataset is only for attributes prefixed with `data-`.
* Assigning a value to a camelCase counterpart using `props` will automatically call `this.setAttribute` under the hood for any attribute name, with or without the `data-` prefix.
* @see https://www.npmjs.com/package/web-component-base#prop-access
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
*
* @example
*
* class HelloWorld extends WebComponent {
* static properties = ["my-prop"];
* get template() {
* return `
* <h1>Hello ${this.props.myProp}</h1>
* <h2>Hello ${this["my-prop"]}</h2>
* `;
* }
* }
*/
props;
constructor() {
super();
if (!this.props) {
const { props, ...clone } = this;
this.props = new Proxy(clone, this.#handler(this));
}
}
/**
* Triggered after view is initialized. Best for querying DOM nodes that will only exist after render.
* @returns void
*/
afterViewInit() {}
/**
* Triggered when the component is connected to the DOM
* Triggered when the component is connected to the DOM. Best for initializing the component like attaching event handlers.
* @returns void
*/
onInit() {}
/**
* Triggered when the component is disconnected from the DOM
* Triggered when the component is disconnected from the DOM. Any initialization done in `onInit` must be undone here.
* @returns void
*/
onDestroy() {}
@ -89,9 +118,12 @@ export class WebComponent extends HTMLElement {
*/
attributeChangedCallback(property, previousValue, currentValue) {
const camelCaps = this.#getCamelCaps(property);
if (previousValue !== currentValue) {
this[property] = currentValue === "" || currentValue;
this[camelCaps] = currentValue === "" || currentValue;
this.props[camelCaps] = this[property];
this.render();
this.onChanges({ property, previousValue, currentValue });
}
@ -109,6 +141,24 @@ export class WebComponent extends HTMLElement {
#getCamelCaps(kebab) {
return kebab.replace(/-./g, (x) => x[1].toUpperCase());
}
#handler = (el) => ({
set(obj, prop, newval) {
obj[prop] = newval;
const kebabize = (str) =>
str.replace(
/[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
);
const kebab = kebabize(prop);
el.setAttribute(kebab, newval);
// Indicate success
return true;
},
});
}
export default WebComponent;