Overview
jsRibbon is a lightweight, dependency-free DOM component and data-binding library for progressive enhancement. It attaches reactive behavior to server-rendered HTML using data-bind attributes — treating the DOM as both the markup and the authoritative state container.
Unlike virtual DOM frameworks, jsRibbon performs targeted direct DOM mutations with zero diffing overhead. It's designed for server-rendered apps where the initial HTML is meaningful, SEO matters, and you want to add interactivity without a heavy build pipeline.
Installation
Include the two library files in order. No bundler, no npm install required.
<!-- Required: state helpers must come first -->
<script src="./jsRibbon-component-state.js"></script>
<script src="./jsRibbon.js"></script>
<!-- Optional: AJAX form enhancement -->
<script src="./jsRibbon-enhance-form.js"></script>
Quick Start
Register a component, then add markup. jsRibbon initializes on DOMContentLoaded and auto-picks up components injected via AJAX.
<script>
jsRibbon.component('MyCounter', ($state) => {
const increment = () => $state.count++;
const decrement = () => $state.count--;
return { increment, decrement };
});
</script>
<div data-bind="component:MyCounter">
<span data-bind="text:count">0</span>
<button data-bind="click:increment">+</button>
<button data-bind="click:decrement">−</button>
</div>
Components
Components are the primary unit of organization. Register a factory function using jsRibbon.component(name, factory). The factory receives the reactive $state and returns a context object with event handlers.
jsRibbon.component('UserCard', ($state, el) => {
// el is the component DOM element
const save = () => {
console.log('Saving:', $state.name);
};
return { save };
});
Multiple instances of the same component can exist on a page — each gets its own scoped state.
State & Reactivity
jsRibbon reads the initial state directly from the DOM. Text content, input values, and checked states become reactive state keys automatically.
data-bind="text:name"— reads initial value from element'stextContentdata-bind="value:age"withvalue="25"— reads from the input's value attribute- Checkbox
checkedattribute becomestrue/falsein state
State is a JavaScript Proxy. Setting any property on $state triggers all subscribed DOM bindings to update automatically.
MutationObserver
jsRibbon installs a MutationObserver on document.body at startup. Any HTML injected into the DOM (from AJAX, innerHTML, HTMX swaps, etc.) is automatically scanned and initialized — no manual rebind needed.
// Example: inject markup 2 seconds later, it just works
setTimeout(() => {
document.getElementById('container').innerHTML = `
<div data-bind="component:UserCard">
<span data-bind="text:name">Raheel</span>
</div>
`;
}, 2000);
text binding
Binds a state key to an element's textContent. Initial value is read from the DOM.
<span data-bind="text:name">Raheel Shan</span>
<!-- $state.name starts as "Raheel Shan" -->
value binding
Two-way binding for inputs, textareas, and selects. Supports update to control when state updates.
| Modifier | Trigger |
|---|---|
| update:input | On every keystroke |
| update:change | On blur / change |
| update:focusout | On focus out |
| (default) | input event for text, change for checkbox/radio |
<input type="text" data-bind="value:name, update:input" value="Raheel" />
<span data-bind="text:name"></span>
class binding
Conditionally toggles CSS classes based on state. Uses stateKey => className syntax.
<div data-bind="class: [ isHidden => hidden, isPink => pink ]">
content
</div>
attr binding
Reactively sets HTML attributes from state. Uses stateKey => attrName syntax.
<a data-bind="attr: [ linkUrl => href, linkTitle => title ]">Click</a>
html binding
Sets innerHTML of an element from state. Useful for rendering rich text from a textarea.
<textarea data-bind="value:content, update:blur"></textarea>
<div data-bind="html:content"></div>
visible binding
Shows or hides an element by toggling display:none based on a boolean state key.
<input type="checkbox" data-bind="value:isOpen" />
<div data-bind="visible:isOpen">
Shown when checkbox is checked
</div>
foreach binding
Renders a list from an array in state. Server-rendered rows with data-key are hydrated in place. New items are cloned from a <template> or the first existing child.
<tbody data-bind="foreach: [ data: users, as: user ]">
<tr>
<td data-bind="text:firstName">Raheel</td>
<td data-bind="text:age">40</td>
<td><button data-bind="click:removeItem">Remove</button></td>
</tr>
</tbody>
<!-- Add items programmatically -->
<script>
jsRibbon.component('Users', ($state) => ({
add: () => $state.users.push({ firstName: 'New', age: 25 })
}));
</script>
Arrays are proxied — calling push(), splice(), shift(), etc. automatically re-renders only the changed rows.
Event Bindings
Any DOM event can be bound using eventName:handlerName syntax. Handlers receive the event and a parsed data-* dataset object.
<button
data-bind="click:handleClick"
data-id="42"
data-user='{"name":"Raheel"}'
>Click</button>
<script>
jsRibbon.component('ClickCheck', ($state) => ({
handleClick: (event, data) => {
console.log(data.id); // 42 (auto-parsed)
console.log(data.user); // {name:"Raheel"} (parsed JSON)
}
}));
</script>
Supported events: click, dblclick, mouseenter, mouseleave, keydown, keyup, input, change, focus, blur, and more.
Checkbox & Radio
Single checkboxes bind to a boolean. Multiple checkboxes with the same key bind to an array. Radio buttons bind to the selected value string.
<!-- Single checkbox -->
<input type="checkbox" data-bind="value:accepted" />
<!-- Checkbox group (binds to array) -->
<input type="checkbox" value="HTML" data-bind="value:skills" />
<input type="checkbox" value="CSS" data-bind="value:skills" />
<!-- Select All toggle -->
<input type="checkbox" data-bind="toggle:skills" />
<!-- Radio buttons -->
<input type="radio" name="gender" value="male" data-bind="value:gender" checked />
<input type="radio" name="gender" value="female" data-bind="value:gender" />
jsRibbon API
factory receives ($state, el) and should return a context object with event handlers.true (default), the MutationObserver automatically registers new components as they enter the DOM.true, throws an error if the same component name is found with inconsistent markup. Default: false (soft warn).Forms & HTMX
jsRibbon intentionally does not ship a bespoke AJAX form helper. For form enhancement, use HTMX alongside jsRibbon. HTMX handles declarative XHR and content swapping; jsRibbon's MutationObserver automatically hydrates any new markup that HTMX injects.
<!-- HTMX posts the form, jsRibbon auto-hydrates the response -->
<form hx-post="/submit" hx-swap="innerHTML" hx-target="#result">
<input name="email" type="email" />
<button>Submit</button>
</form>
<div id="result"></div>