@mr_hugo/boredom
Version:
Another boring JavaScript framework.
372 lines (282 loc) ⢠10.1 kB
Markdown
# boreDOM
A JavaScript framework for building reactive web components with template-based
architecture and automatic state synchronization.
## Features
- š„± **Reactive State Management** - Automatic DOM updates when state changes
- š„± **Template-based Components** - Define components using HTML templates with
`data-component` attributes
- š„± **Hot Module Reloading** - Built-in dev server with file watching and
auto-reload
- š„± **Zero Configuration** - Works out of the box with sensible defaults
- š„± **CLI Tools** - Development server and build tools included
- š„± **TypeScript Support** - Full TypeScript definitions included
- š„± **Project Generator** - Quick project scaffolding with `create-boredom`
## Quick Start
### Installation
```bash
# Install the framework
pnpm install @mr_hugo/boredom
# Or create a new project
npx create-boredom my-app
cd my-app
pnpm dev
```
### Basic Usage
1. **Create an HTML file with component templates:**
```html
<html>
<head>
<title>My boreDOM App</title>
</head>
<body>
<h1>Counter Example</h1>
<simple-counter></simple-counter>
<template data-component="simple-counter">
<div>
<p>Count: <slot name="counter">0</slot></p>
<button onclick="['increase']">+</button>
<button onclick="['decrease']">-</button>
</div>
</template>
<script src="main.js" type="module"></script>
</body>
</html>
```
2. **Create the component logic in JavaScript:**
```javascript
// main.js
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
// Initialize with state
const uiState = await inflictBoreDOM({ count: 0 });
// simple-counter.js (or inline)
export const SimpleCounter = webComponent(({ on }) => {
on("increase", ({ state }) => {
state.count += 1;
});
on("decrease", ({ state }) => {
state.count -= 1;
});
return ({ state, slots }) => {
slots.counter = state.count;
};
});
```
## Development Server
boreDOM includes a built-in development server with hot reloading:
```bash
# Start dev server (watches for changes)
npx boredom
# Custom options
npx boredom --index ./src/index.html --html ./components --static ./public
```
The CLI will:
- Watch for file changes in components, HTML, and static files
- Automatically rebuild and inject components
- Serve your app with hot reloading
- Copy static files to the build directory
## API Reference
### Core Functions
#### `inflictBoreDOM(initialState, componentsLogic?)`
Initializes the boreDOM framework and creates reactive state.
- **`initialState`** - Initial application state object
- **`componentsLogic`** - Optional inline component definitions
- **Returns** - Proxified reactive state object
```javascript
const state = await inflictBoreDOM({
users: [],
selectedUser: null,
});
```
#### `webComponent(initFunction)`
Creates a web component with reactive behavior.
- **`initFunction`** - Component initialization function
- **Returns** - Component definition for use with boreDOM
```javascript
const MyComponent = webComponent(({ on, state, refs, self }) => {
// Setup event handlers
on("click", ({ state }) => {
state.clicked = true;
});
// Return render function
return ({ state, slots, refs }) => {
slots.content = `Clicked: ${state.clicked}`;
};
});
```
### Component API
Components receive these parameters:
#### Initialization Phase
- **`on(eventName, handler)`** - Register event listeners
- **`state`** - Reactive state accessor
- **`refs`** - DOM element references
- **`self`** - Component instance
- **`detail`** - Component-specific data
#### Render Phase
- **`state`** - Current state (read-only in render)
- **`slots`** - Named content slots for the template
- **`refs`** - DOM element references
- **`makeComponent(tag, options)`** - Create child components
### Template Syntax
Templates use standard HTML with special attributes:
```html
<template data-component="my-component">
<!-- Named slots for dynamic content -->
<div>
<h2><slot name="title">Default Title</slot></h2>
<p><slot name="content">Default content</slot></p>
</div>
<!-- Event dispatching -->
<button onclick="['save']">Save</button>
<button onclick="['cancel']">Cancel</button>
<!-- Reference elements -->
<input ref="userInput" type="text">
</template>
```
## How Templates Become Components
1. Declare a template with a tag name
```html
<simple-counter></simple-counter>
<template data-component="simple-counter" data-aria-label="Counter">
<p>Count: <slot name="count">0</slot></p>
<button onclick="['increment']">+</button>
<button onclick="['decrement']">-</button>
<!-- Any other data-* on the template is mirrored to the element -->
<!-- e.g., data-aria-label -> aria-label on <simple-counter> -->
<!-- Add shadowrootmode="open" to render into a ShadowRoot -->
<!-- <template data-component=\"simple-counter\" shadowrootmode=\"open\"> -->
<!-- Optional: external script for behavior -->
<script type="module" src="/simple-counter.js"></script>
</template>
```
2. Provide behavior (first export is used)
```js
// /simple-counter.js
import { webComponent } from "@mr_hugo/boredom";
export const SimpleCounter = webComponent(({ on }) => {
on("increment", ({ state }) => {
state.count += 1;
});
on("decrement", ({ state }) => {
state.count -= 1;
});
return ({ state, slots }) => {
slots.count = String(state.count);
};
});
```
3. Initialize once
```js
import { inflictBoreDOM } from "@mr_hugo/boredom";
await inflictBoreDOM({ count: 0 });
```
What happens under the hood
- The runtime scans `<template data-component>` and registers custom elements.
- It mirrors template `data-*` to host attributes and wires inline
`onclick="['...']"` to custom events ("[]" is the dispatch action).
- Scripts are dynamically imported and run for every matching instance in the
DOM (including multiple instances).
- Subsequent instances created programmatically use the same initialization via
`makeComponent()`.
## State and Subscriptions
Rendering subscribes to the state paths it reads, and mutations trigger batched
updates.
```js
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
export const Counter = webComponent(({ on }) => {
on("inc", ({ state }) => {
state.count++;
}); // mutable state in handlers
return ({ state, slots }) => { // read-only during render
slots.value = String(state.count); // reading subscribes to `count`
};
});
await inflictBoreDOM({ count: 0 });
```
- Subscriptions: Any property read in render (e.g., `state.count`) registers
that render as a subscriber to that path.
- Mutations: Changing arrays/objects (e.g., `state.todos.push(...)`,
`state.user.name = 'X'`) schedules a single rAF to call subscribed renders.
- Scope: Subscriptions are per component instance; only components that read a
path re-render when that path changes.
## Project Structure
A typical boreDOM project structure:
```
my-app/
āāā index.html # Main HTML file
āāā main.js # App initialization
āāā components/ # Component files
ā āāā user-card.html # Component template
ā āāā user-card.js # Component logic
ā āāā user-card.css # Component styles
āāā public/ # Static assets
ā āāā assets/
āāā build/ # Generated build files
```
## Examples
### Counter Component
```javascript
const Counter = webComponent(({ on }) => {
on("increment", ({ state }) => state.count++);
on("decrement", ({ state }) => state.count--);
return ({ state, slots }) => {
slots.value = state.count;
};
});
```
### Todo List Component
```javascript
const TodoList = webComponent(({ on }) => {
on("add-todo", ({ state, e }) => {
state.todos.push({ id: Date.now(), text: e.text, done: false });
});
on("toggle-todo", ({ state, e }) => {
const todo = state.todos.find((t) => t.id === e.id);
if (todo) todo.done = !todo.done;
});
return ({ state, slots, makeComponent }) => {
slots.items = state.todos.map((todo) =>
makeComponent("todo-item", { detail: { todo } })
).join("");
};
});
```
## CLI Reference
```bash
# Development server with file watching
npx boredom [options]
Options:
--index <path> Base HTML file (default: index.html)
--html <folder> Components folder (default: components)
--static <folder> Static files folder (default: public)
```
## TypeScript Support
boreDOM includes full TypeScript definitions:
```typescript
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
interface AppState {
count: number;
users: User[];
}
const state = await inflictBoreDOM<AppState>({
count: 0,
users: [],
});
const MyComponent = webComponent<AppState>(({ on, state }) => {
// TypeScript will infer correct types
on("increment", ({ state }) => {
state.count++; // ā Type-safe
});
return ({ state, slots }) => {
slots.count = state.count.toString();
};
});
```
## Resources
- **Official Documentation**:
[https://hugodaniel.com/pages/boredom/](https://hugodaniel.com/pages/boredom/)
- **Repository**:
[https://github.com/HugoDaniel/boreDOM](https://github.com/HugoDaniel/boreDOM)
- **Examples**: Check the `/examples` directory for complete examples
## License
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><span property="dct:title">boreDOM</span> by <span property="cc:attributionName">Hugo Daniel</span> is marked with <a href="https://creativecommons.org/publicdomain/zero/1.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC0 1.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt=""><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/zero.svg?ref=chooser-v1" alt=""></a></p>