Boxes is a small UI framework where pure functions render DOM and handle messages explicitly without hidden lifecycles or magic.
https://raw.githubusercontent.com/stepan-mitkin/boxes/main/src/boxes.js
0.0.3
-
Rendering is pure. The DOM is described by a pure
renderfunction, similar in spirit to React. Given the same inputs,renderalways produces the same DOM description. -
Imperative DOM updates. The DOM can be updated surgically without calling
render. If only a small change is needed, Boxes updates exactly that part. -
Event handlers are pure. Event handlers do not mutate state or touch the DOM directly.
-
Event handlers can be async. Async event handlers may call the server and
awaitother long-running operations. -
Explicit handler semantics. Event handlers follow this shape:
(state, argument) → (newState, outputCommands)outputCommandsmay include:- messages to other UI nodes
- surgical DOM updates
Boxes keeps rendering and state changes explicit and separate.
In React, the render function is pure and pristine. Everything else is a hack.
React is perfect for static applications where there are no moving parts.
If your app does have moving parts, they go to dirty hacks, such as useState and useEffect.
That means that your app’s logic must be implemented as dirty hacks.
In Boxes, your app’s logic resides in event handlers that are first-class citizens. Event handlers are pure functions, too.
With Boxes, pure functions implement both business logic and rendering.
In short:
- Rendering describes UI structure.
- Event handlers describe change.
A box is a logical UI object:
- it may have local state
- it can render DOM
- it can surgically update DOM
- it can receive messages
- it can send messages to other boxes
Boxes form a tree, defined by a JSON-serializable UI specification.
Each box may define:
state: { ... }Rules:
- state belongs to the box
- state must be JSON-serializable
- state is updated only by returning instructions
No hidden mutations. No magic.
A box renders by returning a DOM description object.
No virtual DOM. No diffing in user code.
If you want to update text — update text. If you want to re-render — re-render.
Boxes communicate via messages:
- DOM events → event handlers
- event handlers → instructions
- instructions → state changes, DOM updates, or new event handler calls
There are also functions (pure queries over state).
No mount. No unmount. No effects.
Parents decide where children go.
Children render into the space they are given.
This makes layout:
- explicit
- deterministic
- easy to reason about
Boxes intentionally supports:
- direct DOM updates
- imperative message sending
- arbitrary HTML tags
Boxes does not try to prevent you from doing real work.
var boxes = createBoxes()A builder constructs a box.
boxes.registerBuilder("createSolid", createSolid)A builder:
- receives
props - returns a box object
boxes.start(uiSpec, containerElement)uiSpecmust be JSON-serializablecontainerElementis a DOM node
To send a message to another object, add messages to the emit property of the returned object of the event handler.
Do not use sendMessage to send messages from handlers.
sendMessage is an escape hatch for long-running multi-stage procedures or recurring jobs.
boxes.sendMessage(targetId, methodName, argument)A box is an object returned by a builder.
Example:
function createSolid(props) {
return {
state: { background: props.background },
setColor: function(ctx, color) {
return {
setLocalState: { background: color }
}
},
render: function(rc) {
return {
style: { background: rc.state.background }
}
}
}
}A box may define:
staterender(renderContext)- methods (message handlers)
- functions (pure state queries)
getActive: function(state) {
return state.active
}- receive
state - must not modify anything
- may return any value
Called via:
ctx.runFunction("boxId", "getActive")setActive: function(ctx, active) {
return {
setLocalState: { active }
}
}- receive
callbackContext - must not mutate it
- return instructions to the framework
A method may return:
{
setLocalState: { key: value }
}{
emit: [
{ target: "boxId", name: "methodName", arg: value }
]
}{
updates: [
{ elementId: "some-id", text: "New text" }
]
}This bypasses re-rendering.
setChildren should be an array of JS-objects that follow the format of UI specification.
{
setChildren: [
{
builder: "createSubWidget",
id: "sub-widget-1",
props: { color: "green" }
}
]
}function render(renderContext) {
return {
text: "Hello",
style: { padding: 10 }
}
}The DOM element already exists.
render describes how to configure it.
{
id,
props,
state,
children,
rect,
buildAbsElement,
buildInlineBlockElement
}var child = renderContext.buildAbsElement(childId, rect)var child = renderContext.buildInlineBlockElement(childId)These calls invoke the child’s render.
{
type: "abs",
rect: { left: 10, top: 10, width: 100, height: 50 },
style: { background: "cyan" }
}{
type: "inline-block",
text: "Hello"
}{
type: "tag",
tag: "input",
elementProps: { type: "checkbox" }
}Nested children are supported.
function createTextButton() {
return {
render: renderTextButton,
click: function(ctx) {
return {
emit: [{ target: "root", name: ctx.props.slot, arg: ctx.props.text }]
}
},
setText: function(ctx, text) {
return {
updates: [{
elementId: ctx.id + "-main-container",
text
}]
}
}
}
}This shows:
- DOM events → box methods
- methods → messages or direct DOM updates
Public domain https://unlicense.org