diff --git a/README.md b/README.md
index 2ffccc1..c331081 100644
--- a/README.md
+++ b/README.md
@@ -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 `
-
Hello ${this.myProp}
+ Hello ${this.props.myProp}
Hello ${this["my-prop"]}
`;
}
diff --git a/demo/BooleanPropTest.mjs b/demo/BooleanPropTest.mjs
index aa91e7b..0b0eba8 100644
--- a/demo/BooleanPropTest.mjs
+++ b/demo/BooleanPropTest.mjs
@@ -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 `is-inline: ${this.isInline}
another-one: ${this.anotherone}
`;
+ return `is-inline: ${this.props.isInline}
another-one: ${this.props.anotherone}
`;
}
}
diff --git a/demo/HelloWorld.mjs b/demo/HelloWorld.mjs
index 3eae46d..ab3a471 100644
--- a/demo/HelloWorld.mjs
+++ b/demo/HelloWorld.mjs
@@ -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 `Hello ${this.dataName}${
- this.emotion === "sad" ? ". ðŸ˜" : "! 🙌"
+ return `Hello ${this.props.myName ?? "World"}${
+ this.props.emotion === "sad" ? ". ðŸ˜" : "! 🙌"
} `;
}
}
diff --git a/demo/SimpleText.mjs b/demo/SimpleText.mjs
index dd898b3..59efb8d 100644
--- a/demo/SimpleText.mjs
+++ b/demo/SimpleText.mjs
@@ -16,7 +16,7 @@ class SimpleText extends WebComponent {
}
get template() {
- return `Click me! `;
+ return `Click me! `;
}
}
diff --git a/demo/index.html b/demo/index.html
index 4a62c82..cb061d0 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/src/WebComponent.js b/src/WebComponent.js
index a31d3d4..0083172 100644
--- a/src/WebComponent.js
+++ b/src/WebComponent.js
@@ -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 `
- * Hello ${this.dataName}${this.emotion === "sad" ? ". ðŸ˜" : "! 🙌"} `;
+ * Hello ${this.props.dataName}${this.props.emotion === "sad" ? ". ðŸ˜" : "! 🙌"} `;
* }
* }
*
@@ -27,12 +24,15 @@
*/
export class WebComponent extends HTMLElement {
/**
+ * Array of strings that tells the browsers which attributes will cause a render
* @type Array
*/
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 `
+ * Hello ${this.props.myProp}
+ * Hello ${this["my-prop"]}
+ * `;
+ * }
+ * }
+ */
+ 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;