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 ## Prop Access
Attributes are generally in `kebab-case`. You can access attribute properties in two ways 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.
1. Use the camelCase counterpart: `this.myProp`, which is automatically filled.
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"]` 1. Or stick with kebab-case: `this["my-prop"]`
```js ```js
@ -106,7 +108,7 @@ class HelloWorld extends WebComponent {
get template() { get template() {
return ` return `
<h1>Hello ${this.myProp}</h1> <h1>Hello ${this.props.myProp}</h1>
<h2>Hello ${this["my-prop"]}</h2> <h2>Hello ${this["my-prop"]}</h2>
`; `;
} }

View file

@ -1,17 +1,10 @@
import WebComponent from "../src/WebComponent.js"; import WebComponent from "../src/WebComponent.js";
export class BooleanPropTest extends WebComponent { export class BooleanPropTest extends WebComponent {
isInline = false;
anotherone = false;
static properties = ["is-inline", "anotherone"]; static properties = ["is-inline", "anotherone"];
onChanges(changes) {
console.log(">>> boolean prop test", changes);
}
get template() { 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"; import WebComponent from "../src/index.js";
export class HelloWorld extends WebComponent { export class HelloWorld extends WebComponent {
dataName = "World"; static properties = ["my-name", "emotion"];
emotion = "excited";
static properties = ["data-name", "emotion"];
onInit() { onInit() {
let count = 0; let count = 0;
this.onclick = () => { this.onclick = () => (this.props.myName = `Clicked ${++count}`);
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 });
} }
get template() { get template() {
return `<button id="btn">Hello ${this.dataName}${ return `<button id="btn">Hello ${this.props.myName ?? "World"}${
this.emotion === "sad" ? ". 😭" : "! 🙌" this.props.emotion === "sad" ? ". 😭" : "! 🙌"
}</button>`; }</button>`;
} }
} }

View file

@ -16,7 +16,7 @@ class SimpleText extends WebComponent {
} }
get template() { 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> <script type="module" src="BooleanPropTest.mjs"></script>
</head> </head>
<body> <body>
<hello-world id="hey" data-name="Ayo" emotion="sad"></hello-world> <hello-world></hello-world>
<p> <p>
<simple-text></simple-text> <simple-text></simple-text>
</p> </p>

View file

@ -8,9 +8,6 @@
* import WebComponent from "https://unpkg.com/web-component-base/index.js"; * import WebComponent from "https://unpkg.com/web-component-base/index.js";
* *
* class HelloWorld extends WebComponent { * class HelloWorld extends WebComponent {
* // initialize prop
* dataName = 'World';
*
* // tell the browser which attributes to cause a render * // tell the browser which attributes to cause a render
* static properties = ["data-name", "emotion"]; * static properties = ["data-name", "emotion"];
* *
@ -18,7 +15,7 @@
* // note: props have kebab-case & camelCase counterparts * // note: props have kebab-case & camelCase counterparts
* get template() { * get template() {
* return ` * 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 { export class WebComponent extends HTMLElement {
/** /**
* Array of strings that tells the browsers which attributes will cause a render
* @type Array<string> * @type Array<string>
*/ */
static properties = []; static properties = [];
/** /**
* Read-only string property that represents how the component will be rendered
* @returns string * @returns string
* @see https://www.npmjs.com/package/web-component-base#template-vs-render
*/ */
get template() { get template() {
return ""; 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 * @returns void
*/ */
afterViewInit() {} 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 * @returns void
*/ */
onInit() {} 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 * @returns void
*/ */
onDestroy() {} onDestroy() {}
@ -89,9 +118,12 @@ export class WebComponent extends HTMLElement {
*/ */
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] = currentValue === "" || currentValue;
this.props[camelCaps] = this[property];
this.render(); this.render();
this.onChanges({ property, previousValue, currentValue }); this.onChanges({ property, previousValue, currentValue });
} }
@ -109,6 +141,24 @@ export class WebComponent extends HTMLElement {
#getCamelCaps(kebab) { #getCamelCaps(kebab) {
return kebab.replace(/-./g, (x) => x[1].toUpperCase()); 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; export default WebComponent;