Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: smart diffing #40

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,13 @@ You can import everything separately, or in a single file each for the main expo

```js
// all in a single file

import { WebComponent, html, attachEffect } from "web-component-base";
import { WebComponent, html, attachEffect, render } from "web-component-base";

// in separate files

import { WebComponent } from "web-component-base/WebComponent.js";

import { html } from "web-component-base/html.js";

import { attachEffect } from "web-component-base/attach-effect.js";
import { html } from "web-component-base/html.js";
import { render } from "web-component-base/render.js";
```

### Utilities
Expand Down
41 changes: 0 additions & 41 deletions examples/demo/HelloWorld.mjs

This file was deleted.

2 changes: 1 addition & 1 deletion examples/demo/Toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Toggle extends WebComponent {
get template() {
return html`
<button onClick=${() => (this.props.toggle = !this.props.toggle)}>
${this.props.toggle}
${this.props.toggle ? "On" : "Off"}
</button>
`;
}
Expand Down
9 changes: 0 additions & 9 deletions examples/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WC demo</title>
<script type="module" src="./HelloWorld.mjs"></script>
<script type="module" src="./SimpleText.mjs"></script>
<script type="module" src="./BooleanPropTest.mjs"></script>
<script type="module" src="./Counter.mjs"></script>
Expand All @@ -14,20 +13,12 @@
<my-toggle></my-toggle>
<my-toggle toggle></my-toggle>
<hr />
<hello-world emotion="sad"></hello-world>
<my-counter></my-counter>
<p>
<simple-text></simple-text>
</p>
<p>
<boolean-prop-test is-inline anotherOne></boolean-prop-test>
</p>

<script type="module">
const helloWorld = document.querySelector("hello-world");
setTimeout(() => {
helloWorld.props.emotion = "excited";
}, 2500);
</script>
</body>
</html>
7 changes: 4 additions & 3 deletions examples/templating/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
<title>WC demo</title>
<script type="module" src="./index.js"></script>
<script type="module" src="./with-lit.js"></script>
<script type="module" src="./vanilla.js"></script>
</head>
<body>
<h2>With our html</h2>
<!-- <vanilla-test></vanilla-test> -->
<my-counter></my-counter>
<hr />
<!-- <hr />
<h2>With lit-html</h2>
<lit-counter></lit-counter>
<lit-counter></lit-counter> -->
</body>
</html>
34 changes: 30 additions & 4 deletions examples/templating/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// @ts-check
import { WebComponent, html } from "../../src/index.js";
import { WebComponent, attachEffect, html } from "../../src/index.js";

export class Counter extends WebComponent {
static props = {
count: 123,
count: 129,
sum: 0,
};

onInit() {
attachEffect(this.props.count, (count) => (this.props.sum = 3 + count));
}

get template() {
const list = ["a", "b", "c", "what"];
const links = [
Expand All @@ -19,6 +25,19 @@ export class Counter extends WebComponent {
];

return html`
<h1>false</h1>
<div>
<div>
<div>
<div>
<div><a>${this.props.sum}</a></div>
</div>
</div>
</div>
</div>
<div><a>${this.props.count}</a></div>
<div><a>hey</a></div>
<div><a>hey</a></div>
<button
class="hey"
id="btn"
Expand All @@ -32,16 +51,23 @@ export class Counter extends WebComponent {
</button>
<form style="margin: 1em 0;">
<label data-my-name="Ayo" for="the-input">Name</label>
<input id="the-input" type="foo" value="Name:" />
<input
onkeyup=${() => ++this.props.count}
id="the-input"
type="foo"
value="Name:"
/>
</form>
${this.props.count > 130 && html`<h1>Too much!</h1>`}
${list.map((item) => html`<p>${item}</p>`)}
<h3 about="Elephant">Links</h3>
<ul>
${links.map(
(link) =>
html`<li><a href=${link.url} target="_blank">${link.text}</a></li>`,
html`<li><a href=${link.url} target="_blank">${link.text}</a></li>`
)}
</ul>
<div>${this.props.count > 130 && html`<h1>Too much!</h1>`}</div>
`;
}
}
Expand Down
10 changes: 10 additions & 0 deletions examples/templating/vanilla.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { html, render } from "../../src/index.js";

class VanillaTest extends HTMLElement {
connectedCallback() {
const el = html`<button>hello</button>`;
render(el, this);
}
}

customElements.define("vanilla-test", VanillaTest);
12 changes: 12 additions & 0 deletions examples/todo-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TODO App</title>
<script type="module" src="./todo.js"></script>
</head>
<body>
<todo-app></todo-app>
</body>
</html>
57 changes: 57 additions & 0 deletions examples/todo-app/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { WebComponent, html } from "../../src/index.js";

class TodoApp extends WebComponent {
#tasks = [];
addTask() {
const newTask = this.querySelector("#task-field")?.value;
if (newTask) {
this.#tasks.push(newTask);
console.log(this.#tasks);
// manual render because #tasks is private + unobserved
this.render();
}
}
#existingTimer;
removeTask(index, checkbox) {
clearTimeout(this.#existingTimer);
// users can change their mind within 1 second :)
this.#existingTimer = setTimeout(() => {
if (checkbox.checked) {
this.#tasks.splice(index, 1);
this.render();
}
}, 1000);
}
get template() {
return html`
<form
onSubmit=${(e) => {
e.preventDefault();
this.addTask();
}}
>
<input placeholder="Buy milk" id="task-field" name="task-field" />
<button type="submit">Add</button>
</form>
<ul style="list-style:none">
${this.#tasks.map(
(t, i) => html`
<li>
<input
type="checkbox"
name="task-${i}"
id="task-${i}"
onChange=${(e) => this.removeTask(i, e.target)}
style="cursor:pointer"
/>
<label for="task-${i}" style="cursor:pointer">${t}</label>
</li>
`
)}
</ul>
<footer>just to make it more difficult</footer>
`;
}
}

customElements.define("todo-app", TodoApp);
31 changes: 9 additions & 22 deletions src/WebComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* @author Ayo Ayco <https://ayo.ayco.io>
*/

import { render as r } from "./render.js";
import {
createElement,
getKebabCase,
getCamelCase,
serialize,
Expand All @@ -17,7 +17,6 @@ import {
*/
export class WebComponent extends HTMLElement {
#host;
#prevDOM;
#props;
#typeMap = {};
#effectsMap = {};
Expand Down Expand Up @@ -119,15 +118,17 @@ export class WebComponent extends HTMLElement {
this[camelCaps] = this[property];

this.#handleUpdateProp(camelCaps, this[property]);

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary

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;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary

if (restored !== this.props[key]) {
this.props[key] = restored;
}
}

#handler(setter, meta) {
Expand Down Expand Up @@ -162,13 +163,13 @@ export class WebComponent extends HTMLElement {

return true;
},
get(obj, prop) {
get(obj, prop, receiver) {
// TODO: handle non-objects
if (obj[prop] !== null && obj[prop] !== undefined) {
Object.getPrototypeOf(obj[prop]).proxy = meta.#props;
Object.getPrototypeOf(obj[prop]).proxy = receiver;
Object.getPrototypeOf(obj[prop]).prop = prop;
}
return obj[prop];
return Reflect.get(...arguments);
},
};
}
Expand All @@ -195,20 +196,6 @@ export class WebComponent extends HTMLElement {
}

render() {
if (typeof this.template === "string") {
this.innerHTML = this.template;
} else if (typeof this.template === "object") {
const tree = this.template;

// TODO: smart diffing
if (JSON.stringify(this.#prevDOM) !== JSON.stringify(tree)) {
const el = createElement(tree);
if (el) {
if (Array.isArray(el)) this.#host.replaceChildren(...el);
else this.#host.replaceChildren(el);
}
this.#prevDOM = tree;
}
}
r(this.template, this.#host);
}
}
Loading