chore: format code with prettier (#14)

This commit is contained in:
Ayo Ayco 2023-12-10 10:58:46 +01:00 committed by GitHub
parent 6eb93b5dd3
commit 488743e011
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 951 additions and 316 deletions

View file

@ -1,3 +1,4 @@
{ {
"js/ts.implicitProjectConfig.checkJs": true "js/ts.implicitProjectConfig.checkJs": true,
} "editor.formatOnSave": true
}

View file

@ -5,13 +5,10 @@ export class Counter extends WebComponent {
onInit() { onInit() {
this.props.count = 0; this.props.count = 0;
this.onclick = () => ++this.props.count; this.onclick = () => ++this.props.count;
attachEffect( attachEffect(this.props.count, (count) => console.log(count));
this.props.count,
(count) => console.log(count)
);
} }
afterViewInit(){ afterViewInit() {
attachEffect(this.props.count, (count) => console.log(count + 100)); attachEffect(this.props.count, (count) => console.log(count + 100));
} }

View file

@ -6,13 +6,10 @@ export class Decrease extends WebComponent {
onInit() { onInit() {
this.props.count = 999; this.props.count = 999;
this.onclick = () => --this.props.count; this.onclick = () => --this.props.count;
attachEffect( attachEffect(this.props.count, (count) => console.log(count));
this.props.count,
(count) => console.log(count)
);
} }
afterViewInit(){ afterViewInit() {
attachEffect(this.props.count, (count) => console.log(count + 100)); attachEffect(this.props.count, (count) => console.log(count + 100));
} }

View file

@ -1,4 +1,4 @@
import { WebComponent } from "../../src/index.js" import { WebComponent } from "../../src/index.js";
export class BooleanPropTest extends WebComponent { export class BooleanPropTest extends WebComponent {
static properties = ["is-inline", "anotherone"]; static properties = ["is-inline", "anotherone"];

View file

@ -1,10 +1,10 @@
// @ts-check // @ts-check
import { WebComponent, html } from "../../src/index.js" import { WebComponent, html } from "../../src/index.js";
export class Counter extends WebComponent { export class Counter extends WebComponent {
static props = { static props = {
count: 0 count: 0,
} };
get template() { get template() {
return html` return html`
<button onClick=${() => ++this.props.count} id="btn"> <button onClick=${() => ++this.props.count} id="btn">

View file

@ -1,5 +1,5 @@
// @ts-check // @ts-check
import { WebComponent } from "../../src/index.js" import { WebComponent } from "../../src/index.js";
export class HelloWorld extends WebComponent { export class HelloWorld extends WebComponent {
static properties = ["count", "emotion"]; static properties = ["count", "emotion"];

View file

@ -1,5 +1,5 @@
// @ts-check // @ts-check
import { WebComponent } from "../../src/index.js" import { WebComponent } from "../../src/index.js";
class SimpleText extends WebComponent { class SimpleText extends WebComponent {
clickCallback() { clickCallback() {

View file

@ -2,7 +2,7 @@ import { WebComponent } from "../../src/index.js";
export class HelloWorld extends WebComponent { export class HelloWorld extends WebComponent {
static props = { static props = {
myName: 'World', myName: "World",
}; };
get template() { get template() {
return `<p>Hello ${this.props.myName}</p>`; return `<p>Hello ${this.props.myName}</p>`;

View file

@ -6,18 +6,17 @@ export class Counter extends WebComponent {
count: 123, count: 123,
}; };
get template() { get template() {
const list = ["a", "b", "c", "what"];
const list = ['a', 'b', 'c', 'what']
const links = [ const links = [
{ {
url: 'https://ayco.io', url: "https://ayco.io",
text: 'Ayo Ayco' text: "Ayo Ayco",
}, },
{ {
url: 'https://ayco.io/gh/McFly', url: "https://ayco.io/gh/McFly",
text: 'McFly' text: "McFly",
} },
] ];
return html` return html`
<button <button
@ -35,14 +34,13 @@ export class Counter extends WebComponent {
<label data-my-name="Ayo" for="the-input">Name</label> <label data-my-name="Ayo" for="the-input">Name</label>
<input id="the-input" type="foo" value="Name:" /> <input id="the-input" type="foo" value="Name:" />
</form> </form>
${ ${list.map((item) => html`<p>${item}</p>`)}
list.map(item => html`<p>${item}</p>`)
}
<h3 about="Elephant">Links</h3> <h3 about="Elephant">Links</h3>
<ul> <ul>
${ ${links.map(
links.map(link => html`<li><a href=${link.url} target="_blank">${link.text}</a></li>`) (link) =>
} html`<li><a href=${link.url} target="_blank">${link.text}</a></li>`,
)}
</ul> </ul>
`; `;
} }

View file

@ -1,12 +1,12 @@
// @ts-check // @ts-check
import { WebComponent } from "../../src/WebComponent.js"; import { WebComponent, html } from "../../src/index.js";
export class Counter extends WebComponent { export class Counter extends WebComponent {
static properties = ["count"]; static properties = ["count"];
onInit() { onInit() {
this.props.count = 1; this.props.count = 1;
let i = 1 let i = 1;
this.onclick = ()=> ++this.props.count this.onclick = () => ++this.props.count;
let double = () => i * 2; let double = () => i * 2;
console.log(double()); console.log(double());
i = 3; i = 3;

View file

@ -1,10 +1,10 @@
import { WebComponent } from "../../src/WebComponent.js"; import { WebComponent } from "../../src/index.js";
export class HelloWorld extends WebComponent { export class HelloWorld extends WebComponent {
static properties = ["name"]; static properties = ["name"];
onInit() { onInit() {
this.props.name = 'a'; this.props.name = "a";
this.onclick = ()=> this.props.name += 'a' this.onclick = () => (this.props.name += "a");
} }
get template() { get template() {
return `<button>W${this.props.name}h!</button>`; return `<button>W${this.props.name}h!</button>`;

View file

@ -1,43 +1,43 @@
import { WebComponent } from "../../src/WebComponent.js"; import { WebComponent } from "../../src/index.js";
export class ObjectText extends WebComponent { export class ObjectText extends WebComponent {
// static properties = ["object"]; // static properties = ["object"];
static props = { static props = {
object: { object: {
hello: 'worldzz', hello: "worldzz",
age: 2 age: 2,
} },
} };
onChanges() { onChanges() {
console.log('>>> object', this.props.object) console.log(">>> object", this.props.object);
} }
get template() { get template() {
const greeting = document.createElement('textarea') const greeting = document.createElement("textarea");
greeting.innerHTML = this.props.object.hello; greeting.innerHTML = this.props.object.hello;
greeting.setAttribute('id', 'greeting-field'); greeting.setAttribute("id", "greeting-field");
const greetingLabel = document.createElement('label'); const greetingLabel = document.createElement("label");
greetingLabel.setAttribute('for', 'greeting-field'); greetingLabel.setAttribute("for", "greeting-field");
greetingLabel.textContent = 'Hello'; greetingLabel.textContent = "Hello";
greeting.onkeyup = () => { greeting.onkeyup = () => {
this.props.object = { this.props.object = {
...this.props.object, ...this.props.object,
hello: greeting.value hello: greeting.value,
}; };
} };
const ageField = document.createElement('input'); const ageField = document.createElement("input");
ageField.value = this.props.object.age; ageField.value = this.props.object.age;
ageField.setAttribute('id', 'age-field'); ageField.setAttribute("id", "age-field");
const ageLabel = document.createElement('label') const ageLabel = document.createElement("label");
ageLabel.setAttribute('for', 'age-field'); ageLabel.setAttribute("for", "age-field");
ageLabel.textContent = 'Age' ageLabel.textContent = "Age";
ageField.onkeyup = () => { ageField.onkeyup = () => {
this.props.object = { this.props.object = {
...this.props.object, ...this.props.object,
age: ageField.value age: ageField.value,
} };
} };
const form = document.createElement('form'); const form = document.createElement("form");
form.append(greetingLabel, greeting, ageLabel, ageField) form.append(greetingLabel, greeting, ageLabel, ageField);
return form; return form;
} }

View file

@ -1,17 +1,17 @@
// @ts-check // @ts-check
import { WebComponent } from "../../src/WebComponent.js"; import { WebComponent } from "../../src/index.js";
export class Toggle extends WebComponent { export class Toggle extends WebComponent {
static properties = ["toggle"]; static properties = ["toggle"];
onInit() { onInit() {
this.props.toggle = false; this.props.toggle = false;
this.onclick = ()=>this.handleToggle() this.onclick = () => this.handleToggle();
} }
handleToggle() { handleToggle() {
this.props.toggle = !this.props.toggle; this.props.toggle = !this.props.toggle;
} }
get template() { get template() {
return `<button id="toggle">${this.props.toggle ? 'On':'Off'}</button>`; return `<button id="toggle">${this.props.toggle ? "On" : "Off"}</button>`;
} }
} }

20
package-lock.json generated
View file

@ -1,18 +1,19 @@
{ {
"name": "web-component-base", "name": "web-component-base",
"version": "2.0.0", "version": "2.0.0-beta.21",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "web-component-base", "name": "web-component-base",
"version": "2.0.0", "version": "2.0.0-beta.21",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"site" "site"
], ],
"devDependencies": { "devDependencies": {
"@size-limit/preset-small-lib": "^11.0.0", "@size-limit/preset-small-lib": "^11.0.0",
"prettier": "^3.1.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"uglify-js": "^3.17.4" "uglify-js": "^3.17.4"
} }
@ -3284,6 +3285,21 @@
"pathe": "^1.1.0" "pathe": "^1.1.0"
} }
}, },
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": { "node_modules/pretty-bytes": {
"version": "6.1.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",

View file

@ -41,7 +41,8 @@
"publish:patch:beta": "npm version patch && npm run pub:beta", "publish:patch:beta": "npm version patch && npm run pub:beta",
"publish:minor": "npm version minor && npm run pub", "publish:minor": "npm version minor && npm run pub",
"publish:minor:beta": "npm version minor && npm run pub:beta", "publish:minor:beta": "npm version minor && npm run pub:beta",
"check:size": "npm run build && size-limit ./dist/WebComponent.js" "check:size": "npm run build && size-limit ./dist/WebComponent.js",
"pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
}, },
"repository": "https://github.com/ayoayco/web-component-base", "repository": "https://github.com/ayoayco/web-component-base",
"homepage": "https://WebComponent.io", "homepage": "https://WebComponent.io",
@ -58,6 +59,7 @@
}, },
"devDependencies": { "devDependencies": {
"@size-limit/preset-small-lib": "^11.0.0", "@size-limit/preset-small-lib": "^11.0.0",
"prettier": "^3.1.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"uglify-js": "^3.17.4" "uglify-js": "^3.17.4"
}, },

View file

@ -1 +1 @@
export default defineNitroConfig({ extends: '@mcflyjs/config' }); export default defineNitroConfig({ extends: "@mcflyjs/config" });

File diff suppressed because one or more lines are too long

View file

@ -26,12 +26,12 @@ class CodeBlockComponent extends HTMLElement {
margin: "1em 0", margin: "1em 0",
fontSize: "large", fontSize: "large",
overflow: "auto", overflow: "auto",
borderRadius: '5px' borderRadius: "5px",
}; };
if (inline) { if (inline) {
style.display = 'inline'; style.display = "inline";
style.padding = '0.25em 0.3em'; style.padding = "0.25em 0.3em";
} }
Object.keys(style).forEach((rule) => { Object.keys(style).forEach((rule) => {

View file

@ -9,11 +9,13 @@ class MyHelloWorld extends WebComponent {
// Triggered when the component is connected to the DOM // Triggered when the component is connected to the DOM
onInit() { onInit() {
let count = 0; let count = 0;
this.onclick = () => this.props.myName = `Clicked ${++count}x` this.onclick = () => (this.props.myName = `Clicked ${++count}x`);
} }
// give readonly template // give readonly template
get template() { get template() {
return `<button style="cursor:pointer">Hello ${this.props.myName ?? 'World'}!</button>`; return `<button style="cursor:pointer">Hello ${
this.props.myName ?? "World"
}!</button>`;
} }
} }

View file

@ -1,11 +1,11 @@
class MyQuote extends WebComponent { class MyQuote extends WebComponent {
static properties = ['type']; static properties = ["type"];
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
get wrapper() { get wrapper() {
return this.querySelector('#wrapper'); return this.querySelector("#wrapper");
} }
afterViewInit() { afterViewInit() {
@ -18,13 +18,12 @@ class MyQuote extends WebComponent {
margin: "1em 0", margin: "1em 0",
fontSize: "large", fontSize: "large",
overflow: "auto", overflow: "auto",
borderRadius: '5px' borderRadius: "5px",
}; };
Object.keys(style).forEach((rule) => { Object.keys(style).forEach((rule) => {
this.wrapper.style[rule] = style[rule]; this.wrapper.style[rule] = style[rule];
}); });
} }
get template() { get template() {

View file

@ -5,10 +5,10 @@ class HelloWorld extends HTMLElement {
connectedCallback() { connectedCallback() {
let count = 0; let count = 0;
const currentName = this.getAttribute('my-name'); const currentName = this.getAttribute("my-name");
if (!currentName) { if (!currentName) {
this.setAttribute('my-name', 'World') this.setAttribute("my-name", "World");
} }
this.onclick = () => this.setAttribute("my-name", `Clicked ${++count}x`); this.onclick = () => this.setAttribute("my-name", `Clicked ${++count}x`);

View file

@ -1,4 +1,10 @@
import { createElement, getKebabCase, getCamelCase, serialize, deserialize } from "./utils/index.js"; import {
createElement,
getKebabCase,
getCamelCase,
serialize,
deserialize,
} from "./utils/index.js";
/** /**
* A minimal base class to reduce the complexity of creating reactive custom elements * A minimal base class to reduce the complexity of creating reactive custom elements
@ -69,20 +75,17 @@ export class WebComponent extends HTMLElement {
*/ */
onChanges(changes) {} onChanges(changes) {}
constructor() { constructor() {
super(); super();
this.#initializeProps(); this.#initializeProps();
} }
static get observedAttributes() { static get observedAttributes() {
const propKeys = this.props ? Object.keys(this.props).map(camelCase => getKebabCase(camelCase)) : []; const propKeys = this.props
? Object.keys(this.props).map((camelCase) => getKebabCase(camelCase))
: [];
return [...( return [...new Set([...this.properties, ...propKeys])];
new Set([
...this.properties,
...propKeys
])
)]
} }
connectedCallback() { connectedCallback() {
@ -135,7 +138,11 @@ export class WebComponent extends HTMLElement {
} }
effectsMap[prop].push(value.callback); effectsMap[prop].push(value.callback);
} else if (typeMap[prop] !== typeof value) { } else if (typeMap[prop] !== typeof value) {
throw TypeError(`Cannot assign ${typeof value} to ${typeMap[prop]} property (setting '${prop}' of ${meta.constructor.name})`) throw TypeError(
`Cannot assign ${typeof value} to ${
typeMap[prop]
} property (setting '${prop}' of ${meta.constructor.name})`,
);
} else if (oldValue !== value) { } else if (oldValue !== value) {
obj[prop] = value; obj[prop] = value;
effectsMap[prop]?.forEach((f) => f(value)); effectsMap[prop]?.forEach((f) => f(value));
@ -157,19 +164,19 @@ export class WebComponent extends HTMLElement {
} }
#initializeProps() { #initializeProps() {
let initialProps = {} let initialProps = {};
if(this.constructor.props) { if (this.constructor.props) {
initialProps = this.constructor.props; initialProps = this.constructor.props;
Object.keys(initialProps).forEach(camelCase => { Object.keys(initialProps).forEach((camelCase) => {
const value = initialProps[camelCase] const value = initialProps[camelCase];
this.#typeMap[camelCase] = typeof value this.#typeMap[camelCase] = typeof value;
this.setAttribute(getKebabCase(camelCase), serialize(value)) this.setAttribute(getKebabCase(camelCase), serialize(value));
}) });
} }
if (!this.#props) { if (!this.#props) {
this.#props = new Proxy( this.#props = new Proxy(
initialProps, initialProps,
this.#handler((key, value) => this.setAttribute(key, value), this) this.#handler((key, value) => this.setAttribute(key, value), this),
); );
} }
} }
@ -179,7 +186,7 @@ export class WebComponent extends HTMLElement {
if (typeof this.template === "string") { if (typeof this.template === "string") {
this.innerHTML = this.template; this.innerHTML = this.template;
return; return;
} else if (typeof this.template === 'object') { } else if (typeof this.template === "object") {
const tree = this.template; const tree = this.template;
// TODO: smart diffing // TODO: smart diffing

View file

@ -2,10 +2,79 @@
* htm -- https://github.com/developit/htm * htm -- https://github.com/developit/htm
* For license information please see ./vendors/htm/LICENSE.txt * For license information please see ./vendors/htm/LICENSE.txt
*/ */
const htm=(new Map,function(n){for(var e,l,s=arguments,t=1,u="",r="",o=[0],f=function(n){1===t&&(n||(u=u.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?o.push(n?s[n]:u):3===t&&(n||u)?(o[1]=n?s[n]:u,t=2):2===t&&"..."===u&&n?o[2]=Object.assign(o[2]||{},s[n]):2===t&&u&&!n?(o[2]=o[2]||{})[u]=!0:t>=5&&(5===t?((o[2]=o[2]||{})[l]=n?u?u+s[n]:s[n]:u,t=6):(n||u)&&(o[2][l]+=n?u+s[n]:u)),u=""},i=0;i<n.length;i++){i&&(1===t&&f(),f(i));for(var p=0;p<n[i].length;p++)e=n[i][p],1===t?"<"===e?(f(),o=[o,"",null],t=3):u+=e:4===t?"--"===u&&">"===e?(t=1,u=""):u=e+u[0]:r?e===r?r="":u+=e:'"'===e||"'"===e?r=e:">"===e?(f(),t=1):t&&("="===e?(t=5,l=u,u=""):"/"===e&&(t<5||">"===n[i][p+1])?(f(),3===t&&(o=o[0]),t=o,(o=o[0]).push(this.apply(null,t.slice(1))),t=0):" "===e||"\t"===e||"\n"===e||"\r"===e?(f(),t=2):u+=e),3===t&&"!--"===u&&(t=4,o=o[0])}return f(),o.length>2?o.slice(1):o[1]}); const htm =
(new Map(),
function (n) {
for (
var e,
l,
s = arguments,
t = 1,
u = "",
r = "",
o = [0],
f = function (n) {
1 === t && (n || (u = u.replace(/^\s*\n\s*|\s*\n\s*$/g, "")))
? o.push(n ? s[n] : u)
: 3 === t && (n || u)
? ((o[1] = n ? s[n] : u), (t = 2))
: 2 === t && "..." === u && n
? (o[2] = Object.assign(o[2] || {}, s[n]))
: 2 === t && u && !n
? ((o[2] = o[2] || {})[u] = !0)
: t >= 5 &&
(5 === t
? (((o[2] = o[2] || {})[l] = n
? u
? u + s[n]
: s[n]
: u),
(t = 6))
: (n || u) && (o[2][l] += n ? u + s[n] : u)),
(u = "");
},
i = 0;
i < n.length;
i++
) {
i && (1 === t && f(), f(i));
for (var p = 0; p < n[i].length; p++)
(e = n[i][p]),
1 === t
? "<" === e
? (f(), (o = [o, "", null]), (t = 3))
: (u += e)
: 4 === t
? "--" === u && ">" === e
? ((t = 1), (u = ""))
: (u = e + u[0])
: r
? e === r
? (r = "")
: (u += e)
: '"' === e || "'" === e
? (r = e)
: ">" === e
? (f(), (t = 1))
: t &&
("=" === e
? ((t = 5), (l = u), (u = ""))
: "/" === e && (t < 5 || ">" === n[i][p + 1])
? (f(),
3 === t && (o = o[0]),
(t = o),
(o = o[0]).push(this.apply(null, t.slice(1))),
(t = 0))
: " " === e || "\t" === e || "\n" === e || "\r" === e
? (f(), (t = 2))
: (u += e)),
3 === t && "!--" === u && ((t = 4), (o = o[0]));
}
return f(), o.length > 2 ? o.slice(1) : o[1];
});
function h(type, props, ...children) { function h(type, props, ...children) {
return {type, props, children}; return { type, props, children };
} }
export const html = htm.bind(h); export const html = htm.bind(h);

View file

@ -2,7 +2,7 @@ export function createElement(tree) {
if (tree.type === undefined) { if (tree.type === undefined) {
if (Array.isArray(tree)) { if (Array.isArray(tree)) {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
frag.replaceChildren(...tree.map(leaf => createElement(leaf))) frag.replaceChildren(...tree.map((leaf) => createElement(leaf)));
return frag; return frag;
} }
return document.createTextNode(tree); return document.createTextNode(tree);
@ -14,7 +14,7 @@ export function createElement(tree) {
if (domProp in el) { if (domProp in el) {
el[domProp] = tree.props[prop]; el[domProp] = tree.props[prop];
} else { } else {
el.setAttribute(prop, tree.props[prop]) el.setAttribute(prop, tree.props[prop]);
} }
}); });
} }

View file

@ -1,6 +1,6 @@
export function getKebabCase(str) { export function getKebabCase(str) {
return str.replace( return str.replace(
/[A-Z]+(?![a-z])|[A-Z]/g, /[A-Z]+(?![a-z])|[A-Z]/g,
($, ofs) => (ofs ? "-" : "") + $.toLowerCase() ($, ofs) => (ofs ? "-" : "") + $.toLowerCase(),
); );
} }

View file

@ -2,4 +2,4 @@ export { serialize } from "./serialize.mjs";
export { deserialize } from "./deserialize.mjs"; export { deserialize } from "./deserialize.mjs";
export { getCamelCase } from "./get-camel-case.mjs"; export { getCamelCase } from "./get-camel-case.mjs";
export { getKebabCase } from "./get-kebab-case.mjs"; export { getKebabCase } from "./get-kebab-case.mjs";
export {createElement} from "./create-element.mjs"; export { createElement } from "./create-element.mjs";

View file

@ -1,200 +0,0 @@
import { createElement } from "./utils/create-element.mjs";
import { getCamelCase } from "./utils/get-camel-case.mjs";
import { getKebabCase } from "./utils/get-kebab-case.mjs";
import { serialize } from "./utils/serialize.mjs";
/**
* A minimal base class to reduce the complexity of creating reactive custom elements
* @license MIT <https://opensource.org/licenses/MIT>
* @author Ayo Ayco <https://ayo.ayco.io>
* @see https://www.npmjs.com/package/web-component-base#readme
*/
export class WebComponent extends HTMLElement {
/**
* Array of strings that tells the browsers which attributes will cause a render
* @type {Array<string>}
*/
static properties = [];
/**
* Blueprint for the Proxy props
*/
static props;
/**
* Read-only string property that represents how the component will be rendered
* @returns {string | Node | (string | Node)[]}
* @see https://www.npmjs.com/package/web-component-base#template-vs-render
*/
get template() {
return "";
}
/**
* Read-only property containing camelCase counterparts of observed attributes.
* @typedef {{[name: string]: any}} PropStringMap
* @see https://www.npmjs.com/package/web-component-base#prop-access
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
* @type {PropStringMap}
*/
get props() {
return this.#props;
}
/**
* @type {PropStringMap}
*/
#props;
/**
* Triggered after view is initialized
*/
afterViewInit() {}
/**
* Triggered when the component is connected to the DOM
*/
onInit() {}
/**
* Triggered when the component is disconnected from the DOM
*/
onDestroy() {}
/**
* Triggered when an attribute value changes
* @typedef {{
* property: string,
* previousValue: any,
* currentValue: any
* }} Changes
* @param {Changes} changes
*/
onChanges(changes) {}
constructor() {
super();
this.#initializeProps();
}
static get observedAttributes() {
const propKeys = this.props ? Object.keys(this.props).map(camelCase => getKebabCase(camelCase)) : [];
return [...(
new Set([
...this.properties,
...propKeys
])
)]
}
connectedCallback() {
this.onInit();
this.render();
this.afterViewInit();
}
disconnectedCallback() {
this.onDestroy();
}
attributeChangedCallback(property, previousValue, currentValue) {
const camelCaps = getCamelCase(property);
if (previousValue !== currentValue) {
this[property] = currentValue === "" || currentValue;
this[camelCaps] = this[property];
this.#handleUpdateProp(camelCaps, this[property]);
this.render();
this.onChanges({ property, previousValue, currentValue });
}
}
#handleUpdateProp(key, stringifiedValue) {
const restored = deserialize(stringifiedValue, this.#typeMap[key]);
if (restored !== this.props[key]) this.props[key] = restored;
}
#typeMap = {};
#effectsMap = {};
#handler(setter, meta) {
const effectsMap = meta.#effectsMap;
const typeMap = meta.#typeMap;
return {
set(obj, prop, value) {
const oldValue = obj[prop];
if (!(prop in typeMap)) {
typeMap[prop] = typeof value;
}
if (value.attach === "effect") {
if (!effectsMap[prop]) {
effectsMap[prop] = [];
}
effectsMap[prop].push(value.callback);
} else if (typeMap[prop] !== typeof value) {
throw TypeError(`Cannot assign ${typeof value} to ${typeMap[prop]} property (setting '${prop}' of ${meta.constructor.name})`)
} else if (oldValue !== value) {
obj[prop] = value;
effectsMap[prop]?.forEach((f) => f(value));
const kebab = getKebabCase(prop);
setter(kebab, serialize(value));
}
return true;
},
get(obj, prop) {
// TODO: handle non-objects
if (obj[prop] !== null && obj[prop] !== undefined) {
Object.getPrototypeOf(obj[prop]).proxy = meta.#props;
Object.getPrototypeOf(obj[prop]).prop = prop;
}
return obj[prop];
},
};
}
#initializeProps() {
let initialProps = {}
if(this.constructor.props) {
initialProps = this.constructor.props;
Object.keys(initialProps).forEach(camelCase => {
const value = initialProps[camelCase]
this.#typeMap[camelCase] = typeof value
this.setAttribute(getKebabCase(camelCase), serialize(value))
})
}
if (!this.#props) {
this.#props = new Proxy(
initialProps,
this.#handler((key, value) => this.setAttribute(key, value), this)
);
}
}
#prevDOM;
render() {
if (typeof this.template === "string") {
this.innerHTML = this.template;
return;
} else if (typeof this.template === 'object') {
const tree = this.template;
// TODO: smart diffing
if (JSON.stringify(this.#prevDOM) !== JSON.stringify(tree)) {
// render
const el = createElement(tree);
if (el) {
if (Array.isArray(el)) this.replaceChildren(...el);
else this.replaceChildren(el);
}
this.#prevDOM = tree;
}
}
}
}

View file

@ -1,4 +1,3 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
@ -6,8 +5,5 @@
"declaration": true, "declaration": true,
"emitDeclarationOnly": true "emitDeclarationOnly": true
}, },
"include": [ "include": ["src/*", "src/utils/*"]
"src/*",
"src/utils/*"
],
} }