feat: props Proxy for camelCase counterparts
- provide easy access to any observed attribute. - update README examples - update JSDoc examples
This commit is contained in:
parent
003b72a1f2
commit
910a5096d0
6 changed files with 70 additions and 39 deletions
|
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue