Event-driven DOM programming in a new style
- Supports event-driven DOM programming in a new way.
- Supports event delegation.
- Lightweight library. < 1.5 kiB gzipped.
- No dependencies.
- No build step.
- Uses No special syntax. Uses plain JavaScript and plain HTML.
- TypeScript friendly.
TodoMVC implementation is also available here.
See the live demos.
Deno
deno add @kt3k/cell
Node
npx jsr add @kt3k/cell
And
import { type Context, register } from "@kt3k/cell";
function MyComponent({ on }: Context) {
on.click = () => {
alert("hello");
};
}
register(MyComponent, "js-hello");
<div class="js-hello">Click</div>
Vanilla js (ES Module):
<script type="module">
import { register } from "https://kt3k.github.io/cell/dist.min.js";
function Mirroring({ on, query }) {
on.input = () => {
query(".dest").textContent = query(".src").value;
};
}
register(Mirroring, "js-mirroring");
</script>
<div class="js-mirroring">
<input class="src" placeholder="Type something" />
<p class="dest"></p>
</div>
Mirrors input value of <input>
element to another dom.
import { type Context, register } from "@kt3k/cell";
function Mirroring({ on, query }: Context) {
on.input = () => {
query(".src").textContent = query(".dest").value;
};
}
register(Mirroring, "js-mirroring");
Pubsub.
import { type Context, register } from "@kt3k/cell";
const EVENT = "my-event";
function PubComponent({ on, pub }: Context) {
on.click = () => {
pub(EVENT, { hello: "clicked!" });
};
}
function SubComponent({ on, sub, el }) {
sub(EVENT);
on[EVENT] = (e) => {
el.textContext += " " + e.detail.hello;
};
}
register(PubComponent, "js-pub");
register(SubComponent, "js-sub");
Prevent default, stop propagation.
import { type Context, register } from "@kt3k/cell";
function PrevetDefaultComponent({ on }: Context) {
on.click = (e) => {
// e is the native event object.
// You can call methods of Event object
e.stopPropagation();
e.preventDefault();
};
}
register(PreventDefaultComponent, "js-prevent-default");
Event delegation. You can assign handlers to on(selector).event
to use
event delegation
pattern.
import { register, type Context } from "@kt3k/cell";
function DelegateComponent({ on, query }: Context) {
on(".btn").click = () => {
query(".result").textContext += " .btn clicked!";
}
}
register(DelegateComponent, "js-delegate");
Outside event handler. By assigning on.outside.event
, you can handle the event
outside of the component dom.
import { type Context, register } from "@kt3k/cell";
function OutsideClickComponent({ on }: Context) {
on.outside.click = ({ e }) => {
console.log("The outside of my-component has been clicked!");
};
}
register(OutsideClickComponent, "js-outside-click");
Let's look at the below basic example.
import { type Context, register } from "@kt3k/cell";
function MyComponent({ on }: Context) {
on.click = () => {
console.log("clicked");
};
}
register(MyComponent, "my-component");
This code is roughly translated into jQuery like the below:
$(document).read(() => {
$(".my-component").each(function () {
$this = $(this);
if (isAlreadyInitialized($this)) {
return;
}
$this.click(() => {
console.log("clicked");
});
});
});
cell
can be seen as a syntax sugar for the above pattern (with a few more
utilities).
Virtual DOM frameworks are good for many use cases, but sometimes they are overkill for the use cases where you only need a little bit of event handlers and dom modifications.
This cell
library explores the new way of simple event-driven DOM programming
without virtual dom.
- Local query is good. Global query is bad.
- Define behaviors based on HTML classes.
- Use pubsub when making remote effect.
When people use jQuery, they often do:
$(".some-class").each(function () {
$(this).on("some-event", () => {
$(".some-target").each(function () {
// some effects on this element
});
});
});
This is very common pattern, and this is very bad.
The above code can been seen as a behavior of .some-class
elements, and they
use global query $(".some-target")
. Because they use global query here, they
depend on the entire DOM tree of the page. If the page change anything in it,
the behavior of the above code can potentially be changed.
This is so unpredictable because any change in the page can affect the behavior of the above class. You can predict what happens with the above code only when you understand every details of the entire application, and that's often impossible when the application is large size, and multiple people working on that app.
So how to fix this? We recommend you should use local queries.
Let's see this example:
$(".some-class").each(function () {
$(this).on("some-event", () => {
$(this).find(".some-target").each(function () {
// some effects on this element
});
});
});
The difference is $(this).find(".some-target")
part. This selects the elements
only under each .some-class
element. So this code only depends on the elements
inside it, which means there is no global dependencies here.
cell
enforces this pattern by providing query
function to the component
which only finds elements under the given element.
function MyComponent({ on, query }: Context) {
on.click = () => {
query(".some-target")!.textContent = "clicked";
};
}
Here query
is the alias of el.querySelector
and it finds .some-target
only
under it. So the dependency is local here.
From our observation, skilled jQuery developers always define DOM behaviors based on HTML classes.
We borrowed this pattern, and cell
allows you to define behavior only based on
HTML classes, not random combination of query selectors.
<div class="hello">John Doe</div>
function MyComponent({ on }: Context) {
alert(`Hello, I'm ${el.textContext}!`);
}
register(MyComponent, "js-hello");
We generally recommend using only local queries, but how to make effects to the remote elements?
We reommend using pubsub pattern here. By using this pattern, you can decouple those affecting and affected elements. If you decouple those elements, you can test those components independently by using events as I/O of those components.
cell
library provides pub
and sub
APIs for encouraging this pattern.
const EVENT = "my-event";
function PubComponent({ on, pub }: Context) {
on.click = () => {
pub(EVENT);
};
}
function SubComponent({ on, sub }: Context) {
sub(EVENT); // This adds sub:my-event class to the mounted element, which means it subscribes to that event.
on[EVENT] = () => {
alert(`Got ${EVENT}!`);
};
}
register(PubComponent, "js-pub-component");
register(SubComponent, "js-sub-component");
Note: cell
uses DOM Event as event payload, and sub:EVENT
HTML class as
registration to the event. When pub(EVENT)
is called the CustomEvent of
EVENT
type are dispatched to the elements which have sub:EVENT
class.
- 2024-06-18 Forked from capsule.
MIT