Understanding builder.bind(defs) - A Beginner's Guide
Quick Start (30 seconds)
Need to automatically sync state with DOM elements? Use builder.bind():
// HTML:
// <div id="counter"></div>
// <div id="message"></div>
// Create a reactive builder with DOM bindings
const app = reactive({ count: 0, message: 'Hello' })
.bind({
'#counter': 'count', // Bind #counter to count
'#message': 'message' // Bind #message to message
})
.build();
// Elements automatically show initial values:
// #counter: "0"
// #message: "Hello"
// Update state - DOM updates automatically
app.count = 5; // #counter shows "5"
app.message = 'Hi'; // #message shows "Hi"That's it! builder.bind() creates automatic DOM bindings and returns the builder for chaining!
What is builder.bind()?
builder.bind() is a builder method that creates automatic DOM bindings between your reactive state and HTML elements. When state changes, the bound elements update automatically.
A DOM binding:
- Connects a CSS selector to a state property
- Updates the element's
textContentautomatically - Can bind to property values or computed functions
- Can bind multiple properties to the same element
- Works with any valid CSS selector
Think of it as automatic UI synchronization - you declare once how state maps to DOM, and updates happen automatically forever.
Syntax
// Add DOM bindings to a builder
builder.bind(definitions)
// Full example
reactive({ count: 0 })
.bind({
'#counter': 'count' // selector: propertyName or function
})
.build()Parameters:
definitions- An object where:- Keys are CSS selectors (e.g.,
#id,.class,[attr]) - Values can be:
- Property names (string)
- Functions that return values
- Objects mapping element properties to state
- Keys are CSS selectors (e.g.,
Returns:
- The builder (for method chaining)
Important:
- DOM elements must exist when
.bind()is called - Bindings create effects automatically
- The builder is returned, so you can chain more methods
- Uses
ReactiveUtils.set()internally with bindings
Why Does This Exist?
The Problem with Manual DOM Updates
Let's say you want to display reactive state in the DOM:
// Create reactive state
const app = state({ count: 0 });
// Manually update DOM with an effect
effect(() => {
document.getElementById('counter').textContent = app.count;
});
// This works, but...
app.count = 5; // DOM updates, but you wrote boilerplate codeFor multiple bindings, this becomes repetitive:
What's the Real Issue?
Manual DOM Binding:
┌─────────────────┐
│ Create state │
│ count: 0 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Write effect │ ← Boilerplate
│ querySelector │ ← Repetitive
│ set textContent │ ← Manual
└────────┬────────┘
│
▼
┌─────────────────┐
│ Another binding │ ← Repeat for
│ Write effect │ ← each element
│ querySelector │ ← tedious!
│ set textContent │
└─────────────────┘
Lots of code!
Easy to forget!Problems: ❌ Repetitive effect() calls for each binding ❌ Manual DOM queries scattered everywhere ❌ Easy to forget to bind some elements ❌ Hard to see what's connected to what ❌ More code = more bugs
The Solution with builder.bind()
When you use builder.bind(), you declare bindings upfront:
// Create builder with DOM bindings
const app = reactive({ count: 0, message: 'Hello' })
.bind({
'#counter': 'count',
'#message': 'message'
})
.build();
// DOM automatically shows values:
// #counter: "0"
// #message: "Hello"
// Update state - DOM updates automatically
app.count = 5;
app.message = 'Hi';
// #counter: "5"
// #message: "Hi"What Just Happened?
Automatic Binding Pattern:
┌──────────────────────┐
│ reactive() │
│ count: 0 │
│ message: 'Hello' │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ .bind() │
│ #counter: count │ ← Declare once
│ #message: message │ ← Clear mapping
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Auto-creates effects │ ← Automatic
│ Queries DOM │ ← Automatic
│ Sets up sync │ ← Automatic
└──────────┬───────────┘
│
▼
Everything connected!
Clean, declarative!With builder.bind():
- Declare all bindings in one place
- Clear mapping of state → DOM
- Automatic effect creation
- Automatic DOM queries
- Less code, fewer bugs
- Easy to maintain
Benefits: ✅ Declarative binding syntax ✅ Automatic DOM synchronization ✅ Less boilerplate code ✅ Clear state → UI mapping ✅ Chainable with other builder methods ✅ Easy to see all connections
Mental Model
Think of builder.bind() like a control panel with labeled lights:
Manual Wiring (Electrician):
┌─────────────────────┐
│ Power Source │
│ (State) │
└─────────────────────┘
│
│ You must wire each light:
│
├→ Wire to Light 1
│ - Run cable
│ - Connect terminals
│ - Test connection
│
├→ Wire to Light 2
│ - Run cable
│ - Connect terminals
│ - Test connection
│
└→ Wire to Light 3
Manual work!
Error-prone!
Pre-Wired Control Panel (Plug & Play):
┌─────────────────────────┐
│ Control Panel │
│ (Builder with .bind()) │
│ │
│ Power → [Light 1] │ ← Labeled
│ (count) → [Light 2] │ ← Pre-wired
│ (message)→ [Light 3] │ ← Automatic
└─────────────────────────┘
│
▼
Everything connected!
Just plug in state!Key Insight: Just like a pre-wired control panel where each switch is already connected to its light, builder.bind() pre-wires your state to DOM elements. You declare the connections, and everything works automatically!
How Does It Work?
The Magic: Declarative DOM Bindings
When you call builder.bind(), here's what happens behind the scenes:
// What you write:
const app = reactive({ count: 0 })
.bind({
'#counter': 'count'
})
.build();
// What actually happens (simplified):
// 1. Builder receives binding definitions
builder.bind({
'#counter': 'count'
});
// 2. For each binding:
const element = document.querySelector('#counter');
// 3. Create an effect that updates the element
effect(() => {
element.textContent = state.count;
});
// 4. Effect runs immediately (sets initial value)
// 5. Effect re-runs whenever count changes
// 6. Cleanup stored for destroy()
// 7. Return builder for chaining
return builder;In other words: builder.bind():
- Takes your binding definitions
- Queries the DOM for each selector
- Creates effects that update each element
- Effects run immediately (initial values)
- Effects re-run when dependencies change
- Stores cleanup functions
- Returns the builder for chaining
Under the Hood
.bind({ '#counter': 'count' })
│
▼
┌───────────────────────┐
│ Parse Definitions │
│ #counter → count │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Query DOM │
│ document.querySelector│
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Create Effect │
│ el.textContent = val │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Effect Runs │
│ Sets initial value │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Store Cleanup │
│ (for destroy) │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Return Builder │
│ (for chaining) │
└───────────────────────┘What happens when state changes:
1️⃣ You write to state: app.count = 5 2️⃣ Reactive system detects change 3️⃣ Binding effect re-runs 4️⃣ Effect updates DOM element: element.textContent = 5 5️⃣ DOM synchronized automatically
Basic Usage
Simple Property Binding
The simplest way to use builder.bind():
// HTML: <div id="counter"></div>
const builder = reactive({ count: 0 });
builder.bind({
'#counter': 'count'
});
const counter = builder.build();
// DOM shows: 0
counter.count = 5;
// DOM shows: 5Multiple Property Bindings
Bind multiple elements in one call:
// HTML:
// <div id="username"></div>
// <div id="email"></div>
// <div id="age"></div>
const user = reactive({ name: 'John', email: 'john@example.com', age: 25 })
.bind({
'#username': 'name',
'#email': 'email',
'#age': 'age'
})
.build();
// DOM shows:
// #username: "John"
// #email: "john@example.com"
// #age: "25"
user.name = 'Jane';
// #username: "Jane"Function Binding (Computed Display)
Bind to a function for computed values:
// HTML: <div id="fullName"></div>
const user = reactive({ firstName: 'John', lastName: 'Doe' })
.bind({
'#fullName': function() {
return this.state.firstName + ' ' + this.state.lastName;
}
})
.build();
// DOM shows: "John Doe"
user.firstName = 'Jane';
// DOM shows: "Jane Doe"Binding Patterns
Pattern 1: ID Selectors
// HTML: <div id="message"></div>
reactive({ text: 'Hello World' })
.bind({
'#message': 'text'
})
.build();Pattern 2: Class Selectors (Multiple Elements)
// HTML:
// <div class="counter"></div>
// <div class="counter"></div>
reactive({ count: 0 })
.bind({
'.counter': 'count' // Binds to ALL .counter elements
})
.build();Pattern 3: Attribute Selectors
// HTML: <div data-display="count"></div>
reactive({ count: 0 })
.bind({
'[data-display="count"]': 'count'
})
.build();Pattern 4: Multiple Properties on Same Element
// HTML: <div id="display"></div>
reactive({ count: 0, name: 'Counter' })
.bind({
'#display': {
textContent: 'count',
title: 'name'
}
})
.build();
// <div id="display" title="Counter">0</div>Pattern 5: Computed Functions
reactive({ count: 0 })
.bind({
'#doubled': function() {
return this.state.count * 2;
},
'#status': function() {
return this.state.count > 5 ? 'High' : 'Low';
}
})
.build();Advanced Binding Examples
Example 1: Conditional Display
// HTML:
// <div id="count"></div>
// <div id="status"></div>
const counter = reactive({ value: 0 })
.bind({
'#count': 'value',
'#status': function() {
if (this.state.value === 0) return 'Zero';
if (this.state.value > 0) return 'Positive';
return 'Negative';
}
})
.build();
counter.value = 5;
// #count: "5"
// #status: "Positive"Example 2: Formatted Values
// HTML:
// <div id="price"></div>
// <div id="date"></div>
const app = reactive({ price: 29.99, timestamp: Date.now() })
.bind({
'#price': function() {
return '$' + this.state.price.toFixed(2);
},
'#date': function() {
return new Date(this.state.timestamp).toLocaleDateString();
}
})
.build();
// #price: "$29.99"
// #date: "1/2/2026"Example 3: List Display
// HTML: <div id="items"></div>
const app = reactive({ items: ['Apple', 'Banana', 'Cherry'] })
.bind({
'#items': function() {
return this.state.items.join(', ');
}
})
.build();
// #items: "Apple, Banana, Cherry"
app.items.push('Date');
// #items: "Apple, Banana, Cherry, Date"Example 4: Multiple Element Properties
// HTML: <div id="box"></div>
const app = reactive({ width: 100, color: 'blue' })
.bind({
'#box': {
textContent: function() {
return this.state.width + 'px';
},
style: function() {
return {
width: this.state.width + 'px',
height: this.state.width + 'px',
backgroundColor: this.state.color
};
}
}
})
.build();
app.width = 200;
app.color = 'red';
// Box resizes and changes colorExample 5: Computed from Computed
const app = reactive({ count: 5 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.bind({
'#count': 'count',
'#doubled': 'doubled',
'#quadrupled': function() {
return this.state.doubled * 2;
}
})
.build();
// #count: "5"
// #doubled: "10"
// #quadrupled: "20"Chaining with Other Methods
Combining with Computed
const app = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.bind({
'#count': 'count',
'#doubled': 'doubled'
})
.build();
app.count = 5;
// #count: "5"
// #doubled: "10"Combining with Actions
// HTML:
// <div id="count"></div>
// <button id="inc">+</button>
const counter = reactive({ count: 0 })
.bind({
'#count': 'count'
})
.action('increment', (state) => {
state.count++;
})
.build();
document.getElementById('inc').onclick = () => {
counter.increment();
};
// Clicking button updates DOM automaticallyFull Chain Example
// HTML:
// <div id="count"></div>
// <div id="doubled"></div>
// <div id="status"></div>
const app = reactive({ count: 0 })
.computed({
doubled() {
return this.state.count * 2;
}
})
.bind({
'#count': 'count',
'#doubled': 'doubled',
'#status': function() {
return this.state.count > 5 ? 'High' : 'Low';
}
})
.watch({
count(newVal) {
if (newVal > 10) {
console.warn('Count very high!');
}
}
})
.action('increment', (state) => state.count++)
.action('reset', (state) => state.count = 0)
.build();
app.increment();
// DOM updates automaticallyCommon Patterns
Pattern: Form Display Sync
// HTML:
// <input id="nameInput" type="text">
// <div id="nameDisplay"></div>
const form = reactive({ name: '' })
.bind({
'#nameDisplay': 'name'
})
.build();
document.getElementById('nameInput').addEventListener('input', (e) => {
form.name = e.target.value;
});
// As user types, display updates automaticallyPattern: Dashboard with Live Data
// HTML:
// <div id="temp"></div>
// <div id="humidity"></div>
// <div id="status"></div>
const dashboard = reactive({ temp: 20, humidity: 45 })
.bind({
'#temp': function() {
return this.state.temp + '°C';
},
'#humidity': function() {
return this.state.humidity + '%';
},
'#status': function() {
if (this.state.temp > 25 && this.state.humidity > 60) {
return 'Hot & Humid';
}
return 'Comfortable';
}
})
.build();
// Update from API
setInterval(() => {
dashboard.temp = Math.round(Math.random() * 30);
dashboard.humidity = Math.round(Math.random() * 100);
}, 5000);
// Dashboard updates automaticallyPattern: Shopping Cart Display
// HTML:
// <div id="itemCount"></div>
// <div id="total"></div>
const cart = reactive({ items: [], itemPrice: 10 })
.bind({
'#itemCount': function() {
return this.state.items.length + ' items';
},
'#total': function() {
return '$' + (this.state.items.length * this.state.itemPrice).toFixed(2);
}
})
.build();
cart.items.push({ id: 1, name: 'Product 1' });
// #itemCount: "1 items"
// #total: "$10.00"Pattern: Status Indicators
// HTML:
// <div id="indicator"></div>
const app = reactive({ isOnline: true, errorCount: 0 })
.bind({
'#indicator': {
textContent: function() {
if (!this.state.isOnline) return 'Offline';
if (this.state.errorCount > 0) return 'Errors';
return 'Online';
},
className: function() {
if (!this.state.isOnline) return 'status-offline';
if (this.state.errorCount > 0) return 'status-error';
return 'status-online';
}
}
})
.build();
app.isOnline = false;
// <div id="indicator" class="status-offline">Offline</div>Common Pitfalls
Pitfall #1: Element Not Found
❌ Wrong:
// Element doesn't exist yet
reactive({ count: 0 })
.bind({
'#counter': 'count' // Fails silently if element not found
})
.build();
// Later in HTML:
// <div id="counter"></div>The binding fails if the element doesn't exist when .bind() is called.
✅ Correct:
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
reactive({ count: 0 })
.bind({
'#counter': 'count'
})
.build();
});
// Or place script at end of bodyPitfall #2: Wrong Selector Syntax
❌ Wrong:
// HTML: <div id="counter"></div>
reactive({ count: 0 })
.bind({
'counter': 'count' // Missing # for ID
})
.build();Without #, it looks for a <counter> tag, not an ID.
✅ Correct:
reactive({ count: 0 })
.bind({
'#counter': 'count' // Correct # for ID
})
.build();Pitfall #3: Binding to Non-Existent Property
❌ Wrong:
reactive({ count: 0 })
.bind({
'#display': 'number' // 'number' doesn't exist
})
.build();This displays undefined because the property doesn't exist.
✅ Correct:
reactive({ count: 0 })
.bind({
'#display': 'count' // Use existing property
})
.build();Pitfall #4: Arrow Functions Losing this
❌ Wrong:
reactive({ count: 0 })
.bind({
'#doubled': () => this.state.count * 2 // 'this' is wrong!
})
.build();✅ Correct:
reactive({ count: 0 })
.bind({
'#doubled': function() {
return this.state.count * 2; // Regular function works!
}
})
.build();Pitfall #5: Forgetting Multiple Bindings Apply to All Matches
⚠️ Unexpected Behavior:
// HTML:
// <div class="display"></div>
// <div class="display"></div>
// <div class="display"></div>
reactive({ count: 0 })
.bind({
'.display': 'count' // Binds to ALL three elements
})
.build();Class selectors bind to all matching elements, not just the first.
✅ This is actually correct! Just be aware that all matching elements will show the same value.
Summary
What is builder.bind()?
builder.bind() is a builder method that creates automatic DOM bindings between reactive state and HTML elements. Changes to state automatically update the bound elements.
Why use builder.bind()?
- Automatic DOM synchronization
- Declarative binding syntax
- Less boilerplate code
- Clear state → UI mapping
- Chainable with other builder methods
Key Points to Remember:
1️⃣ Elements must exist - DOM must be ready when binding 2️⃣ Use correct selectors - #id, .class, [attr], etc. 3️⃣ Property or function - Bind to property names or functions 4️⃣ Regular functions - Not arrow functions (for this binding) 5️⃣ Returns builder - Chain with other methods
Mental Model: Think of builder.bind() as a pre-wired control panel - you declare which state connects to which elements, and the system automatically keeps everything synchronized!
Quick Reference:
// SIMPLE BINDING
reactive({ count: 0 })
.bind({ '#counter': 'count' })
.build();
// MULTIPLE BINDINGS
reactive({ name: 'John', age: 25 })
.bind({
'#name': 'name',
'#age': 'age'
})
.build();
// FUNCTION BINDING
reactive({ firstName: 'John', lastName: 'Doe' })
.bind({
'#fullName': function() {
return this.state.firstName + ' ' + this.state.lastName;
}
})
.build();
// MULTIPLE PROPERTIES ON ELEMENT
reactive({ count: 0 })
.bind({
'#display': {
textContent: 'count',
title: function() {
return 'Count: ' + this.state.count;
}
}
})
.build();
// CHAIN WITH OTHER METHODS
reactive({ count: 0 })
.computed({ doubled() { return this.state.count * 2; } })
.bind({
'#count': 'count',
'#doubled': 'doubled'
})
.action('inc', (state) => state.count++)
.build();Remember: builder.bind() gives you declarative DOM bindings that automatically keep your UI synchronized with your reactive state. Declare once, and it works forever!