https://github.com/tamasmajer/magic-box-ui
Think in boxes. A tiny (<2.5KB) library for building reactive UIs by composing simple HTML containers. Just declare your boxes, populate them with data, and assemble your interface piece by piece. No build tools required.
https://github.com/tamasmajer/magic-box-ui
client html js library reactive tag tag-ui tagui tiny ui vanjs
Last synced: about 1 month ago
JSON representation
Think in boxes. A tiny (<2.5KB) library for building reactive UIs by composing simple HTML containers. Just declare your boxes, populate them with data, and assemble your interface piece by piece. No build tools required.
- Host: GitHub
- URL: https://github.com/tamasmajer/magic-box-ui
- Owner: tamasmajer
- Created: 2025-09-13T08:51:25.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-10-05T16:18:08.000Z (9 months ago)
- Last Synced: 2025-10-10T02:25:15.012Z (9 months ago)
- Topics: client, html, js, library, reactive, tag, tag-ui, tagui, tiny, ui, vanjs
- Language: JavaScript
- Homepage:
- Size: 179 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# MagicBox
Think in boxes. A tiny (<2.5KB) library for building reactive UIs by composing simple HTML containers. Just declare your boxes, populate them with data, and assemble your interface piece by piece. No build tools required.
## Overview
### Core Concept
Put your HTML elements and your data into boxes, then compose them together.
**Name your HTML elements:**
```html
+
0
```
**Box your data:**
```javascript
const count = box(0)
const doubled = box(() => count.box * 2)
```
**Compose and react:**
```javascript
box.App(box.Counter({
Display: () => `${count.box} × 2 = ${doubled.box}`,
Increment: { onclick: () => count.box++ }
}))
```
When you use a box inside another box, MagicBox tracks the dependency. Update the inner box → outer box updates automatically.
### Working Example
```html
0
+
import box from 'magic-box.js'
const counter = box(0)
const { App, Counter } = box
App(
Counter({
Display: () => counter.box,
Increase: { onclick: () => counter.box += 1 },
})
)
```
### Disclaimer
This is a proof of concept - a personal experiment shared to gauge interest in simpler alternatives to today's frameworks.
### Getting Started
1. **Requirements**: Modern browser with ES6 module support, no build tools required
2. **Import**: `import box from 'https://cdn.jsdelivr.net/gh/tamasmajer/magic-box/magic-box.min.js'`
3. **Name your elements**: `
`
4. **Box your data**: `const count = box(0)`
5. **Compose them**: `box.App(box.Counter({ Display: count }))`
### Creating a Box
The type of the box depends on the type of the first argument.
```javascript
box(firstArgument, ...options)
```
| First Argument | Behavior | Example |
|----------------|----------|---------|
| **Data** | Box data we can update later | `box(0)`, `box({ name: 'John' })` |
| **Function** | Box a derived value | `box(() => count.box * 2)` |
| **Box + Function** | Update only when this box changes | `box(count, () => expensive())` |
| **Boxes + Function** | Update only when these boxes change | `box([count, user], () => expensive())` |
| **Storage Object** | Box localStorage namespace | `box(localStorage, 'app/')` |
| **`fetch`** | Create a remote box | `const remote = box(fetch)` |
| **DOM Node** | Box an existing element | `box(window, { onresize: handler })` |
| **Property Access** | Find boxes by name | `box.Button`, `box.div` |
### Examples
**Box your data:**
```javascript
const counter = box(0)
const user = box({ name: 'John' })
const doubled = box(() => counter.box * 2) // Computed (auto-deps)
const expensive = box(counter, () => heavyCalc()) // Updates only if 'counter' changes
const optimized = box([counter, user], () => compute()) // Updates only if 'counter' or 'user' changes
```
**Combine the contents of boxes:**
```javascript
const firstName = box('John')
const lastName = box('Doe')
const fullName = box(() => `${firstName.box} ${lastName.box}`)
firstName.box = 'Jane' // fullName automatically updates to "Jane Doe"
```
**Create boxes from localStorage:**
```javascript
const { settings } = box(localStorage) // Auto-sync to localStorage
const { prefs } = box(localStorage, 'myapp/') // With namespace prefix
```
**Create a remote box we can talk to:**
```javascript
const remote = box(fetch) // Box the ability to ask a server
const api = box(fetch, { url: '/api' }) // With default config
```
**Create boxes from DOM nodes:**
```javascript
box(window, { onresize: () => updateLayout() }) // Bind to window
box(document.body, { class: 'dark-theme' }) // Bind to body
box(myElement, { onclick: handler }, 'New content') // Bind + add children
box(myElement, { Title: 'New Title', Button: { onclick: newHandler } }) // Update descendants by box names
```
**Find boxes by name:**
```javascript
box.Counter({ count: () => counter.box }) // Use template
box.MyButton.box.focus() // Direct DOM access
const { div, span } = box // Create elements
```
### Return Values
**Data box** → Access with `.box`
```javascript
const count = box(0)
count.box = 5 // Update
console.log(count.box) // Read
```
**localStorage box** → Destructure boxes
```javascript
const { settings } = box(localStorage, 'app/')
settings.box = { darkMode: true } // Auto-saves
```
**Remote box** → Call to make requests
```javascript
const api = box(fetch, { url: '/api' })
api({ path: '/users' }) // Makes request
```
**DOM node box** → Returns the same node
```javascript
const elem = box(myDiv, { class: 'active' }).focus()
```
**Template box** → Returns a clone of the template node, a new DOM element
```javascript
const counter = box.Counter({ Display: () => count.box })
document.body.append(counter) // Add to page
```
**Element boxes** → Return new DOM elements
```javascript
const { div, span } = box
const widget = div({ class: 'widget' }, span('Hello'))
```
## Features
- [Data Boxes](#data-boxes)
- [Elements with Box Attributes](#elements-with-box-attributes)
- [Elements without Box Attributes](#elements-without-box-attributes)
- [Creating Elements with Tags](#creating-elements-with-tags)
- [List Handling](#list-handling)
- [Remote Boxes](#remote-boxes)
- [Local Box from/to Remote Box](#local-box-fromto-remote-box)
- [Best Practices](#best-practices)
- [VanJS Enhancements](#vanjs-enhancements)
### Data Boxes
Box your data to make it reactive.
**Basic usage:**
```javascript
const counter = box(0)
const user = box({ name: 'John', age: 25 })
// Access/modify with .box
counter.box = 10
user.box = { ...user.box, age: 26 } // Always replace, never mutate
```
**Computed values:**
```javascript
const price = box(100)
const quantity = box(2)
const total = box(() => price.box * quantity.box)
price.box = 150 // total automatically becomes 300
```
**Performance optimization:**
```javascript
const result = box([dep1, dep2], () => compute()) // Only updates when deps change
```
**Persistent boxes:**
```javascript
const { settings } = box(localStorage)
const { userPrefs } = box(localStorage, 'myapp/')
settings.box = { darkMode: true } // Auto-saves
```
### Elements with Box Attributes
**Template behavior:**
- `` → `box.Name()` clones the template
- `
` → `box.Name()` updates element in-place
**Naming convention:**
Use uppercase names: `box="Counter"` not `box="counter"`
**Element access:**
```javascript
const button = box.MyButton.box // Get the DOM element
button.focus()
```
**Template binding patterns:**
```javascript
box.Counter({
Display: 'text only', // Content only
Button: { onclick: handler }, // Properties only
Link: [{ href: '/page', class: 'active' }, 'Visit'] // Properties + content
})
// Mixed format: combine element properties with descendant updates
box.UserCard({
class: 'active', // lowercase = element property
UserName: user.box.name, // Uppercase = descendant content
EditBtn: { onclick: edit } // Uppercase = descendant properties
})
```
**Element operations:**
```javascript
// Properties only (keeps existing children)
box.Element({ onclick: handler, class: 'active' })
// Properties + replace all children
box.Element([{ onclick: handler }, 'new content'])
// Update descendants by box names
box.Element({
Child1: 'new text',
Child2: { class: 'highlight' }
})
// Mixed format: element properties + descendant updates (case sensitive)
box.Element({
class: 'container', // lowercase = element property
Title: 'New title', // Uppercase = descendant update
Button: { onclick: handler } // Uppercase = descendant update
})
```
### Elements without Box Attributes
Bind properties and events to existing DOM nodes.
**Direct node binding:**
```javascript
// Bind to global objects
box(window, {
onresize: () => updateLayout(),
onbeforeunload: (e) => e.preventDefault()
})
box(document, { onclick: handleGlobalClick })
box(document.body, { onkeydown: (e) => {
if (e.key === 'Escape') closeModal()
}})
// Bind to any DOM element
const myDiv = document.getElementById('myDiv')
box(myDiv, {
onclick: handleClick,
class: () => isActive.box ? 'active' : ''
})
```
### Creating Elements with Tags
Build UI elements programmatically.
**Element destructuring:**
```javascript
const { div, span, button, h1 } = box
const widget = div({ class: 'widget' },
h1('Title'),
span(() => counter.box),
button({ onclick: () => counter.box++ }, 'Increment')
)
```
**Content format:**
All content compiles to `[{ props }, ...children]`.
- `h1('text')` → `[{}, 'text']`
- `h1({ class: 'title' })` → `[{ class: 'title' }]`
- `h1({ class: 'title' }, 'text')` → `[{ class: 'title' }, 'text']`
**Reactive content:**
Elements update automatically when reactive dependencies change.
```javascript
span(() => counter.box) // Updates automatically
div({
class: () => counter.box % 2 ? 'odd' : 'even'
}, () => `Count: ${counter.box}`)
```
### List Handling
**Always replace, never mutate:**
```javascript
// ✅ Correct
items.box = [...items.box, newItem]
items.box = items.box.filter(item => item.id !== targetId)
// ❌ Wrong
items.box.push(newItem)
items.box[0] = newValue
```
**Rendering lists:**
Map arrays to DOM elements with empty state handling.
```javascript
box.TodoList(() =>
todos.box.length === 0
? div({ class: 'empty' }, 'No todos yet!')
: todos.box.map(todo =>
box.TodoItem({
Text: todo.text,
Delete: { onclick: () => removeTodo(todo.id) }
})
)
)
```
### Remote Boxes
Handle API calls with reactive loading states and error handling.
**Basic usage:**
```javascript
const remote = box(fetch)
const users = await remote({ url: '/api/users' })
```
**Default configuration:**
Create remote boxes with default settings.
```javascript
const api = box(fetch, {
headers: { Authorization: `Bearer ${token}` },
url: 'https://api.example.com'
})
// Use with specific overrides
const users = api({ path: '/users' }) // GET https://api.example.com/users
const createUser = api({ path: '/users', body: userData }) // POST (automatic when body present)
```
**Reactive callbacks:**
Use reactive callbacks for loading states.
```javascript
const load = (id, filter) => remote({
url: 'https://api.example.com',
path: '/append/' + id, // optional path append
query: { filter }, // optional query parameters
body: input.box, // POST body (auto-JSON if object, method: 'POST' automatic)
loading: url => loading.box = url ? 'loading' : '', // Called before/after
failed: ({ response, error }) => { // Error handling with more details
if (response) console.log('failed', response.status)
else console.log('error', error)
},
result: data => output.box = data // Success callback
})
```
**Method auto-detection:**
HTTP method is determined by request configuration.
```javascript
// GET request (no body)
remote({ url: '/api/users' })
// POST request (body present, method auto-detected)
remote({ url: '/api/users', body: { name: 'John' } })
// Explicit method override
remote({ url: '/api/users/123', method: 'PATCH', body: { name: 'Jane' } })
```
**Composable configuration:**
Build reusable request configurations.
```javascript
const remote = box(fetch)
const serverConfig = { url: 'https://api.example.com' }
const session = { ...serverConfig, headers: { Authorization: token } }
const endpoint = { ...session, path: '/notes' }
const saveNote = (note) => remote({
...endpoint,
body: note, // method: 'POST' automatic when body present
result: (data) => notes.box = [...notes.box, data]
})
```
### Local Box from/to Remote Box
**Remote to Local**
```javascript
// Automatically refetch when dependencies change
const remote = box(fetch)
const docId = box(1)
const doc = box(() => remote({
url: `https://api.example.com/posts/${docId.box}`
}))
// Usage in templates
box.PostView({
Title: () => doc.box?.title,
Content: () => doc.box?.content
})
```
**Editable Remote Doc**
```javascript
// Load remote doc when docId changes
const remote = box(fetch)
const docId = box(456)
const serverDoc = box(() => remote({
url: `/api/documents/${docId.box}`,
loading: url => isLoading.box = !!url
}))
// Create local
const localDoc = box({})
// Initialize local from remote
box(() => {
if (serverDoc.box && !localDoc.box.id) {
localDoc.box = { ...serverDoc.box }
}
})
// Save local to remote
const save = () => remote({
url: `/api/documents/${docId.box}`,
method: 'PATCH', // Explicit method for PATCH (not auto-detected)
body: localDoc.box,
result: () => showSaved.box = true
})
```
### Customizing the Library
Customize attribute and property names used throughout the framework.
**Create custom instances:**
Vue-like box attribute, box function, and .value:
```javascript
import Box from 'magic-box.js'
const box = new Box('box', 'value'), $boxes = box
//
$boxes.App($boxes.Counter(...))
const counter = box(1)
counter.value = 2
```
Box-def variant:
```javascript
import Box from 'magic-box.js'
const box = new Box('box', 'def'), def = box
//
box.App(box.Counter(...))
const counter = def(1)
counter.def = 2
```
UI variant:
```javascript
import Box from 'magic-box.js'
const ui = new Box('box', 'SIGNAL'), SIGNAL = ui
//
ui.App(ui.Counter(...))
const counter = SIGNAL(1)
counter.SIGNAL = 2
```
### Best Practices
1. **Template-first**: Write HTML templates with CSS, bind with minimal JavaScript
2. **Prefer templates**: HTML templates are more maintainable than DOM creation
3. **Always replace**: Use spread/filter/map - never mutate with push/splice
4. **Uppercase names**: `box="UserCard"` required to distinguish from attributes
5. **Explicit dependencies**: Use `box([deps], fn)` for expensive computations
6. **Direct DOM access**: `box.Element.box` gets the DOM element
7. **localStorage prefixes**: Use `box(localStorage, 'app/')` to avoid conflicts
### VanJS Enhancements
MagicBox is built on VanJS 1.5.3 and includes several enhancements that make reactive development more powerful:
**Fragment Support**
Reactive functions can return arrays of elements, enabling dynamic component composition:
```javascript
const renderItems = () => items.box.map(item =>
box.div({ class: 'item' }, item.name)
)
box.Container(renderItems) // Automatically handles array of elements
```
Fragment support also works with conditional rendering:
```javascript
const conditionalContent = () => [
isLoading.box && box.div('Loading...'),
hasError.box && box.div({ class: 'error' }, error.box),
data.box && box.div('Content loaded')
].filter(Boolean)
box.App(conditionalContent)
```
Templates with multiple root elements are automatically wrapped in document fragments. When a parent container only contains fragment children, the fragments unfold directly into the parent, preserving flexbox and grid layouts that require direct parent-child relationships.
**Explicit Updates**
Control when expensive computations run by explicitly declaring dependencies, perfect for tab interfaces and performance optimization:
```javascript
// Tab switching: only update when activeTab changes, not when content changes
const tabContent = box([activeTab], () =>
activeTab.box === 'users' ?
box.UserList({ users: allUsers.box }) : // Won't re-render when allUsers changes
activeTab.box === 'settings' ?
box.SettingsPanel({ config: appConfig.box }) : // Won't re-render when appConfig changes
box.div('Select a tab')
)
box.App(tabContent)
```
Without explicit dependencies, this would re-render whenever `allUsers` or `appConfig` changes, even when those tabs aren't visible. With explicit updates, it only re-renders when `activeTab` changes.
You can also force updates for stateless calls:
```javascript
// Force update on user action, regardless of other dependencies
const refreshData = box([forceUpdate], () => {
// This runs when forceUpdate changes, ignoring other state changes
return fetchAndRenderExpensiveData()
})
// Trigger refresh manually
const handleRefresh = () => forceUpdate.box = Date.now()
```
This prevents unnecessary re-renders and ensures proper cleanup of event listeners and DOM references in complex component hierarchies.
**Shorter Conditional Syntax**
MagicBox enables shorter conditional rendering by supporting the `&&` operator. It automatically filters out `false`, `null`, or `undefined` values but preserves the number zero. To handle zero values, use explicit comparisons like `value !== 0`:
```javascript
// Concise conditional syntax - no ternary needed
const message = () => user.box && `Welcome, ${user.box.name}!`
const errorDisplay = () => hasError.box && box.div({ class: 'error' }, 'Something went wrong')
const count = () => items.box.length > 0 && box.span(`${items.box.length} items`)
// Instead of verbose ternaries
const message = () => user.box ? `Welcome, ${user.box.name}!` : null
const errorDisplay = () => hasError.box ? box.div({ class: 'error' }, 'Something went wrong') : null
const count = () => items.box.length > 0 ? box.span(`${items.box.length} items`) : null
```
Smart value handling preserves meaningful content while filtering out display issues:
```javascript
// These become empty strings
box.div(null) // Empty div
box.span(undefined) // Empty span
box.p(false && 'text') // Empty paragraph
// These preserve the actual value (numbers and strings are kept)
box.h1(0) // Shows "0"
box.span('') // Shows empty string
box.div(-1) // Shows "-1"
```
This makes conditional rendering more concise while preventing `null`, `undefined`, or `false` from appearing as unwanted text in your UI.
**HTML-First Development**
Write component structure in HTML templates, then bind behavior with minimal JavaScript:
```html
Add
import box from 'magic-box.js'
const todos = box([])
const newTodo = box('')
// Bind behavior to HTML structure
box.App(
box.TodoApp({
NewTodo: {
oninput: e => newTodo.box = e.target.value,
value: () => newTodo.box
},
AddBtn: {
onclick: () => {
if (newTodo.box.trim()) {
todos.box = [...todos.box, { id: Date.now(), text: newTodo.box }]
newTodo.box = ''
}
}
},
TodoList: () => todos.box.map(todo =>
box.div({ class: 'todo-item' }, todo.text)
),
Summary: () => `${todos.box.length} todos`
})
)
```
This approach separates concerns cleanly: HTML handles structure and styling, JavaScript handles behavior and state management.