Skip to content

Commit 59426a3

Browse files
committed
Updated README.md
1 parent 4eb5179 commit 59426a3

File tree

2 files changed

+103
-51
lines changed

2 files changed

+103
-51
lines changed

README.md

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<b>purify.js</b> is a 1.0kB <i>(minified, gzipped)</i> JavaScript UI building library that encourages the usage of pure JavaScript and DOM, while providing a thin layer of abstraction for the annoying parts for better DX <i>(developer experience)</i>. 🚀
1717
</p>
1818

19-
# Features 🌟
19+
# Features 🌟🚀
2020

2121
- 🔥 **Keeps you close to the DOM.**
2222
- ✍️ `HTMLElement` builder allows you to differentiate between attributes and properties.
@@ -33,13 +33,13 @@
3333

3434
---
3535

36-
## Compare 📏
36+
## Compare 📏⚖️
3737

38-
### Size ⚡
38+
### Size ⚡📊
3939

4040
| Library | .min.js | .min.js.gz |
4141
| --------------- | ------- | ---------- |
42-
| **purify.js** | 2.3kB | 1.0kB |
42+
| **purify.js** | 2.2kB | 1.0kB |
4343
| Preact 10.19.3 | 11.2kB | 4.5kB |
4444
| Solid 1.8.12 | 23kB | 8.1kB |
4545
| jQuery 3.7.1 | 85.1kB | 29.7kB |
@@ -49,7 +49,7 @@
4949

5050
---
5151

52-
## Installation 🍙
52+
## Installation 📦🍙
5353

5454
To install **purify.js**, follow the [jsr.io/@purifyjs/core](https://jsr.io/@purifyjs/core).
5555

@@ -65,53 +65,58 @@ import { computed, Lifecycle, ref, Signal, tags } from "@purifyjs/core";
6565
const { div, section, button, ul, li, input } = tags;
6666

6767
function App() {
68-
return div().id("app").children(Counter());
68+
return div().id("app").replaceChildren$(Counter());
6969
}
7070

7171
function Counter() {
7272
const count = ref(0);
7373
const double = count.derive((count) => count * 2);
7474
const half = computed(() => count.val * 0.5);
7575

76-
return div().children(
76+
return div().replaceChildren$(
7777
section({ class: "input" })
7878
.ariaLabel("Input")
79-
.children(
79+
.replaceChildren$(
8080
button()
8181
.title("Decrement by 1")
8282
.onclick(() => count.val--)
8383
.textContent("-"),
84-
input().type("number").effect(useBindNumber(count)).step("1"),
84+
input().type("number").$effect(useBindNumber(count)).step("1"),
8585
button()
8686
.title("Increment by 1")
8787
.onclick(() => count.val++)
8888
.textContent("+"),
8989
),
9090
section({ class: "output" })
9191
.ariaLabel("Output")
92-
.children(
93-
ul().children(
94-
li().children("Count: ", count),
95-
li().children("Double: ", double),
96-
li().children("Half: ", half),
92+
.replaceChildren$(
93+
ul().replaceChildren$(
94+
li().replaceChildren$("Count: ", count),
95+
li().replaceChildren$("Double: ", double),
96+
li().replaceChildren$("Half: ", half),
9797
),
9898
),
9999
);
100100
}
101101

102-
function useBindNumber(state: Signal.State<number>): Lifecycle.OnConnected<HTMLInputElement> {
102+
function useBindNumber(
103+
state: Signal.State<number>,
104+
): Lifecycle.OnConnected<HTMLInputElement> {
103105
return (element) => {
104106
const listener = () => (state.val = element.valueAsNumber);
105107
element.addEventListener("input", listener);
106-
const unfollow = state.follow((value) => (element.valueAsNumber = value), true);
108+
const unfollow = state.follow(
109+
(value) => (element.valueAsNumber = value),
110+
true,
111+
);
107112
return () => {
108113
element.removeEventListener("input", listener);
109114
unfollow();
110115
};
111116
};
112117
}
113118

114-
document.body.append(App().node);
119+
document.body.append(App().$node);
115120
```
116121

117122
### ShadowRoot
@@ -122,25 +127,25 @@ import { Builder, ref, tags } from "@purifyjs/core";
122127
const { div, button } = tags;
123128

124129
function App() {
125-
return div().id("app").children(Counter());
130+
return div().id("app").replaceChildren$(Counter());
126131
}
127132

128133
function Counter() {
129134
const host = div();
130-
const shadow = new Builder(host.node.attachShadow({ mode: "open" }));
135+
const shadow = new Builder(host.$node.attachShadow({ mode: "open" }));
131136

132137
const count = ref(0);
133138

134-
shadow.children(
139+
shadow.replaceChildren$(
135140
button()
136141
.title("Click me!")
137142
.onclick(() => count.val++)
138-
.children("Count:", count),
143+
.replaceChildren$("Count:", count),
139144
);
140145
return host;
141146
}
142147

143-
document.body.append(App().node);
148+
document.body.append(App().$node);
144149
```
145150

146151
### Web Components
@@ -151,7 +156,7 @@ import { Builder, ref, tags, WithLifecycle } from "@purifyjs/core";
151156
const { div, button } = tags;
152157

153158
function App() {
154-
return div().id("app").children(new CounterElement());
159+
return div().id("app").replaceChildren$(new CounterElement());
155160
}
156161

157162
declare global {
@@ -162,7 +167,7 @@ declare global {
162167

163168
class CounterElement extends WithLifecycle(HTMLElement) {
164169
static {
165-
customElements.define("x-counter", this);
170+
customElements.define("x-counter", CounterElement);
166171
}
167172

168173
#count = ref(0);
@@ -171,23 +176,23 @@ class CounterElement extends WithLifecycle(HTMLElement) {
171176
super();
172177
const self = new Builder<CounterElement>(this);
173178

174-
self.children(
179+
self.replaceChildren$(
175180
button()
176181
.title("Click me!")
177182
.onclick(() => this.#count.val++)
178-
.children("Count:", this.#count),
183+
.replaceChildren$("Count:", this.#count),
179184
);
180185
}
181186
}
182187

183-
document.body.append(App().node);
188+
document.body.append(App().$node);
184189
```
185190

186-
## Guide 🥡
191+
## Guide 📖🥡
187192

188193
Coming soon.
189194

190-
## Why Not JSX Templating? 🍕
195+
## Why Not JSX Templating? 🤔🍕
191196

192197
- **Lack of Type Safety**: An `<img>` element created with JSX cannot have the `HTMLImageElement` type because all JSX elements must return
193198
the same type. This causes issues if you expect a `HTMLImageElement` some where in the code but all JSX returns is `HTMLElement` or
@@ -199,30 +204,75 @@ Coming soon.
199204
- **Attributes vs. Properties**: In **purify.js**, I can differentiate between attributes and properties of an element while building it,
200205
which is not currently possible with JSX. This distinction enhances clarity and control when defining element characteristics.
201206

202-
## Current Limitations 🦀
203-
204-
- **Lifecycle and Reactivity**: Currently, I use Custom Elements to detect if an element is connected to the DOM. This means:
205-
206-
- Every element created by the `tags` proxy, are Custom Elements. But they look like normal `<div>`(s) and `<span>`(s) and etc on the
207-
DevTools, because they extend the original element and use the original tag name. This way we can follow the life cycle of every
208-
element. And it works amazingly.
209-
- But we also have signals, which might not return an HTMLElement. So we gotta wrap signals with something in the DOM. So we can follow
210-
its lifecycle and know where it starts and ends. Traditionally this is done via `Comment` `Node`(s). But there is no feasible and sync
211-
way to follow a `Comment` `Node` on the DOM while also allowing direct DOM manipulation
212-
([DOM#533](https://github.com/whatwg/dom/issues/533)). So instead of `Comment` `Node`(s), I used Custom Elements with
213-
`display: contents` style to wrap signal renders. This way, I can follow the lifecycle of the signal render in the DOM, and decide to
214-
follow or unfollow the signal. Since signal render itself is an `Element` this approach has limitations, such as `.parent > *` selector
215-
wouldn't select all children if some are inside a signal.
207+
JSX is not part of this library natively, but a wrapper can be made quite easily.
216208

217-
As another solution to this, a persistent DocumentFrament that acts similar to `Element` with `display: contents` style but also
218-
intentionally skipped by query selectors would also be useful. Similar: ([DOM#739](https://github.com/whatwg/dom/issues/736))
219-
220-
But as long as the developer is aware of this limitation or difference, it shouldn't cause any issues.
209+
## Limitations ⚠️🦀
221210

222211
- Since I use extended custom elements, safari doesn't support this yet, so if you care about safari for some reasons, use
223212
[ungap/custom-elements](https://github.com/ungap/custom-elements) polyfill. You can follow support at
224213
[caniuse](https://caniuse.com/mdn-html_global_attributes_is).
225214

226-
- Treat built in elements with `WithLifecycle` as normal built in elements, and for example don't check if an element is
227-
`instanceof WithLifecycle(HTMLDivElement)`. Because this way of doing things might be changed in the future with things like
228-
[WICG/webcomponents/issues/1029](https://github.com/WICG/webcomponents/issues/1029).
215+
## Future 🔮🦀
216+
217+
- Right now, when a `Signal` is connected to DOM via `Builder`, we update all of the children of the `ParentNode` with
218+
`ParentNode.prototype.replaceChildren()`.
219+
220+
This is obviously not that great, previously at `0.1.6` I was using a `<div>` element with the style `display:contents` to wrap a rendered
221+
`Signal` on the DOM. This was also allowing me to follow it's lifecyle via `connectedCallback`/`disconnectedCallback` which was allowing
222+
me to follow or unfollow the `Signal`, making cleanup easier.
223+
224+
But since we wrap it with an `HTMLElement` it was causing problems with CSS selectors, since now each `Signal` is an `HTMLElement` on the
225+
DOM.
226+
227+
So at `0.2.0` I made it so that all children of the `ParentNode` updates when a `Signal` child changes. Tho this issue can be escaped by
228+
seperating things while writing the code. Or make use of things like `.replaceChild()`. Since all support `Signal`(s) now.
229+
230+
But to solve the core of this issue JS needs a real `DocumentFragment` with persistent children.
231+
232+
This proposal might solve this issue:
233+
[DOM#739 Proposal: a DocumentFragment whose nodes do not get removed once inserted](https://github.com/whatwg/dom/issues/736).
234+
235+
In the puroposal they purpose on making the fragment undetactable with `childNodes` or `children` which I'm against and don't like at all.
236+
`DocumentFragment` should be a `ParentNode` should have it's own children, and can be `ChildNode` of other `ParentNode`. Normal hierarchy,
237+
no trasparency other than CSS.
238+
239+
But it's a good start, but just by having a real, working as intended, `DocumentFragment` we are not done.
240+
241+
Which brings be to the next point.
242+
243+
- We also need a native, sync and easy to use way to follow lifecycle of any DOM `ChildNode`, or at least all `Element` and this new
244+
persistent `DocumentFragment`. Because without a lifecycle feature we can't bind a `Signal` to the DOM, start, stop/cleanup them
245+
automatically.
246+
247+
An issue is open here [DOM#533 Make it possible to observe connected-ness of a node](https://github.com/whatwg/dom/issues/533).
248+
249+
Since DOM already has a sync way to follow lifecycle of custom `HTMLElement`(s). Since this is the only way, at this time we heavily relay
250+
on that. Currently we use auto created Custom Elements via `tags` proxy and `WithLifecycle` `HTMLElement` mixin. And allow `Signal`
251+
related things only on those elements.
252+
253+
- If this feature above doesn't come sooner we also keep an eye of this other proposal which has more attraction:
254+
[webcomponents#1029 Proposal: Custom attributes for all elements, enhancements for more complex use cases](https://github.com/WICG/webcomponents/issues/1029)
255+
256+
This proposal doesn't fix the issue with `DocumentFragment`(s), but improves and makes `HTMLElement` based lifecycles more modular and DX
257+
friendly.
258+
259+
Right now, we have a mixing function called `WithLifecycle` which can be used like:
260+
```ts
261+
WithLifecycle(HTMLElement); // or
262+
WithLifecycle(HTMLDivElement);
263+
```
264+
It adds a lifecycle function called `$effect()` to any `HTMLElement` type. Which can later be extended by a custom element like
265+
```ts
266+
class MyElement extends WithLifecycle(HTMLElement)
267+
```
268+
Allowing you to create your own custom `HTMLElement` type with lifecycle. the `tags` proxy also uses `WithLifecycle` in combination with
269+
`Builder` internally. so when you do `tags.div()` you are actually getting a `<div is="pure-div">` with a lifecycle. _But the `[is]`
270+
attribute is not visible in the DOM since this element created by JS, not HTML_.
271+
272+
Anyway since this method requires you to decide if something is an element with lifecycle ahead of time, and also requires use to create
273+
`pure-*` variant of native HTMLElement types in order to make them have lifecycle, it's kinda a lot. It makes sense. But it's kind of a
274+
lot.
275+
276+
So this new custom attributes proposal can let us have lifecycle on any `Element` easily by simily adding an attribute to it. And this can
277+
reshape a big portion of this codebase. And would make things connected to lifecyle of the `Element` more visible in the DOM. Which is
278+
great.

apps/vite/src/examples/web-component-example.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ declare global {
1313
}
1414

1515
class CounterElement extends WithLifecycle(HTMLElement) {
16-
static _ = customElements.define("x-counter", CounterElement);
16+
static {
17+
customElements.define("x-counter", CounterElement);
18+
}
1719

1820
#count = ref(0);
1921

0 commit comments

Comments
 (0)