Skip to content
/ cell Public

☯ A frontend tool for easily writing UI components. Cell encourages the use of event handlers and signals (Demo: https://kt3k.github.io/cell )

License

Notifications You must be signed in to change notification settings

kt3k/cell

Repository files navigation

cell

Cell v0.7.7

A frontend UI tool, encourages local event handlers and signals

Features

  • Cell encourages local event handlers pattern
  • Cell encourages signals pattern for remote effects
  • Lightweight (< 1.5 kiB gzipped)
  • TypeScript friendly

Live examples

See the live demos.

TodoMVC

TodoMVC implementation is also available here.

Install

npx jsr add @kt3k/cell

Or, in Deno,

deno add @kt3k/cell

Hello world: How to use on

The below is an example of a cell component. A cell component is a function of the type (ctx: Context) => string | undefined.

Context includes handy helpers for implementing UI behavior easily and quickly.

The below example uses on helper, which registers the event handler to the mounted dom element, which is <button> element in this case.

import { type Context, register } from "@kt3k/cell"

function MyComponent({ on }: Context) {
  on("click", () => {
    alert("hello")
  })
}

register(MyComponent, "js-hello")
<button class="js-hello">Click</button>

When you click this button, it alerts "hello".

Mirroring the inputs: How to use query

The component below shows how to copy the input text into other dom element.

query is helper to query by the selector inside your component. query(".dest") is equivalent of el.querySelector(".dest").

import { type Context, register } from "@kt3k/cell"

function Mirroring({ on, query }: Context) {
  on("input", () => {
    query(".dest").textContent = query(".src").value
  })
}

register(Mirroring, "js-mirroring")
<div class="js-mirroring">
  <input class="src" placeholder="type something" />
  <p class="dest"></p>
</div>

Event Delegation: Use the 2nd arg of on

If you pass a string (a selector) as the second argument of on function, the event handler is only invoked when the event comes from the element which matches the given selector.

import { type Context, register } from "@kt3k/cell"

function DelegateComponent({ on, query }: Context) {
  on("click", ".btn", () => {
    query(".result").textContext += " .btn clicked!"
  })
}

register(DelegateComponent, "js-delegate")

Outside events: onOutside helper

By calling onOutside(event, handler), you can handle the event outside of the component's DOM.

This is convenient, for example, when you like to close the modal dialog when the user clicking the outside of it.

import { type Context, register } from "@kt3k/cell"

function OutsideClickComponent({ onOutside }: Context) {
  onOutside("click", ({ e }) => {
    console.log("The outside of my-component has been clicked!")
  })
}

register(OutsideClickComponent, "js-outside-click")

Using Cell directly from the browser

You can import directly from https://kt3k.github.io/cell/dist.min.js to try cell in the browser.

<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>

Use signals when making remote effect

Cell encourages local event handling, but in many cases you would also need to make effects to remote elements.

If you need to affects the components in remote places (i.e. components not an ancestor or decendant of the component), we commend using signals for communicating with them.

signals are event emitter with values, whose events are triggered only when the values are changed.

import { Context, Signal } from "@kt3k/cell"

const sig = new Signal(0)

function Component({ el, subscribe }: Context) {
  subscribe(sig, (v) => {
    el.textContent = `The value is ${v}`
  })
}

sig.update(1)
sig.update(2)

Write unit tests of components

Use @b-fuse/deno-dom for polyfill document object. An example of basic test case of a component looks like the below:

import { DOMParser } from "@b-fuze/deno-dom"
import { assertEquals } from "@std/assert"
import { type Context, mount, register } from "@kt3k/cell"

Deno.test("A test case of Component", () => {
  function Component({ el }: Context) {
    el.textContent = "a"
  }

  register(Component, "js-component")

  globalThis.document = new DOMParser().parseFromString(
    `<body><div class="js-component"></div></body>`,
    "text/html",
    // deno-lint-ignore no-explicit-any
  ) as any
  mount()
  assertEquals(document.body.firstChild?.textContent, "a")
})

Notes

About local event handlers

We call DOM event handlers local when

  1. it only reads the data in the bound DOM element
  2. and it only manipulates DOM elements inside the bound DOM element

The Context helpers in cell like on, el, query, and queryAll have focus on the retrieval and manipulation of local data and makes it easier to write local event handlers

Prior art

Projects with similar concepts

  • Flight by twitter
    • Not under active development
  • eddy.js
    • Archived

History

  • 2024-06-18 Forked from capsule.

License

MIT

About

☯ A frontend tool for easily writing UI components. Cell encourages the use of event handlers and signals (Demo: https://kt3k.github.io/cell )

Resources

License

Stars

Watchers

Forks