This guide helps you understand how to use Doh's core data binding tools: Doh.observe
and Doh.mimic
. These functions are fundamental to creating reactive applications in Doh, enabling you to automatically synchronize data between different parts of your application and react to changes in real-time. They provide powerful mechanisms for property watching and establishing one-way or two-way data flows.
This guide covers:
Doh.observe
for one-way property watching and reacting to changes.Doh.mimic
.The Doh.observe
function allows you to monitor changes to a specific property on an object. It establishes a one-way data flow, executing callbacks whenever the observed property's value is set. This is ideal for scenarios where one part of your application needs to react to changes initiated elsewhere.
const removeObserver = Doh.observe(object, propName, onChangeCallback, onEveryCallback);
object
- The object containing the property to watch.propName
- The name of the property to observe.onChangeCallback
- (Optional) Function called only when the property value actually changes from its previous value.onEveryCallback
- (Optional) Function called on every set operation on the property, regardless of whether the value changes.Callbacks receive these parameters:
object
- The object being observed.propName
- The property name being observed.newValue
- The new value being assigned to the property.oldValue
- The previous value of the property (only provided to onChangeCallback
).Returns a function. Calling this function will remove the observer and stop the callbacks from firing for future changes.
const user = { name: "John", updates: 0 };
// Observe changes to the 'name' property
const removeNameObserver = Doh.observe(user, "name",
(obj, prop, newVal, oldVal) => {
console.log(`User name changed from ${oldVal} to ${newVal}`);
}
);
// Observe every update to the 'updates' property
const removeUpdateObserver = Doh.observe(user, "updates", null, // No onChange needed here
(obj, prop, newVal) => {
console.log(`User updates counter set to ${newVal}. Total updates: ${newVal}`);
}
);
user.name = "Jane"; // Logs: "User name changed from John to Jane"
user.name = "Jane"; // Does *not* log, as the value didn't change.
user.updates++; // Logs: "User updates counter set to 1. Total updates: 1"
user.updates++; // Logs: "User updates counter set to 2. Total updates: 2"
// Later, to stop observing:
removeNameObserver();
removeUpdateObserver();
The Doh.mimic
function creates a two-way binding between a property on one object and a property on another object. It ensures that if either property changes, the other property is automatically updated to match. This is perfect for keeping related data, like a model and a view, perfectly synchronized.
Internally, Doh.mimic
uses Doh.observe
on both properties to achieve this mutual synchronization.
const removeMimic = Doh.mimic(myObject, myProp, theirObject, theirProp, onChangeCallback);
myObject
- The first object in the binding pair.myProp
- The property name on the myObject
to synchronize.theirObject
- The second object in the binding pair.theirProp
- The property name on the theirObject
to synchronize.onChangeCallback
- (Optional) A callback function that is executed only when myObject[myProp]
changes (either directly or because theirObject[theirProp]
changed) and its value is different from the previous value.Upon calling Doh.mimic
, the system first checks if the initial values of myObject[myProp]
and theirObject[theirProp]
are different. If they are, myObject[myProp]
is immediately set to the value of theirObject[theirProp]
before the two-way observation begins. This ensures the properties start in a synchronized state without causing an initial echo loop.
The onChangeCallback
receives:
myObject
- The first object (myObject
).myProp
- The property name on the first object (myProp
).theirObject
- The second object (theirObject
).theirProp
- The property name on the second object (theirProp
).newValue
- The new, synchronized value.oldValue
- The previous value of myObject[myProp]
.Returns a function. Calling this function removes the underlying observers, effectively breaking the two-way binding.
const model = { value: 5 };
const view = { displayValue: 0 }; // Starts different
console.log(`Before mimic: model=${model.value}, view=${view.displayValue}`); // model=5, view=0
const removeMimic = Doh.mimic(
model, "value", // Source object and property
view, "displayValue", // Target object and property
(myThing, myProp, theirThing, theirProp, newVal, oldVal) => {
// This callback runs when model.value changes
console.log(`Callback: Model property '${myProp}' changed from ${oldVal} to ${newVal}. View property '${theirProp}' is synchronized.`);
}
);
console.log(`After mimic: model=${model.value}, view=${view.displayValue}`); // model=0, view=0 (Initial sync: model took view's value)
// Change the model's property
model.value = 10;
// Output: Callback: Model property 'value' changed from 0 to 10. View property 'displayValue' is synchronized.
console.log(`After model change: model=${model.value}, view=${view.displayValue}`); // model=10, view=10 (View updated)
// Change the view's property
view.displayValue = 15;
// Output: Callback: Model property 'value' changed from 10 to 15. View property 'displayValue' is synchronized. (Model update triggered the callback)
console.log(`After view change: model=${model.value}, view=${view.displayValue}`); // model=15, view=15 (Model updated)
// Later, to remove the two-way binding:
removeMimic();
// Changes are no longer synchronized
model.value = 20;
console.log(`After removing mimic: model=${model.value}, view=${view.displayValue}`); // model=20, view=15
Both observe
and mimic
utilize a stack-based approach for managing callbacks. This design enables the creation of powerful "networks" of observations, where data changes can propagate through multiple interconnected objects.
You can chain observers and mimics together to create complex data flow networks. A change initiated at one point can trigger a cascade of updates through connected objects. This architecture encourages building stable "observation anchors" while maintaining flexibility in how different parts of your application react to data changes.
// Create a network: data <-> ui -> analytics
const data = { value: 0 };
const ui = { display: 0 };
const analytics = { lastValue: 0 };
const logger = { message: '' };
// 1. Two-way binding between data model and UI display
Doh.mimic(data, "value", ui, "display");
// 2. One-way observation: Analytics watches the UI display
Doh.observe(ui, "display", (obj, prop, newVal) => {
analytics.lastValue = newVal;
console.log(`Analytics: UI updated to ${newVal}`);
});
// 3. One-way observation: Logger also watches the UI display
Doh.observe(ui, "display", (obj, prop, newVal) => {
logger.message = `UI display is now ${newVal}`;
console.log(`Logger: ${logger.message}`);
});
// Trigger a change in the data model
data.value = 50;
// Console output will show:
// -> Analytics: UI updated to 50
// -> Logger: UI display is now 50
console.log(ui.display); // 50
console.log(analytics.lastValue); // 50
console.log(logger.message); // "UI display is now 50"
// Trigger a change from the UI side
ui.display = 100;
// Console output will show:
// -> Analytics: UI updated to 100
// -> Logger: UI display is now 100
console.log(data.value); // 100 (updated via mimic)
console.log(analytics.lastValue); // 100
console.log(logger.message); // "UI display is now 100"
Because observers and mimics are managed internally using callback stacks, the system reliably propagates changes regardless of where you attach your observers within a network.
A key feature of Doh's data binding is its seamless integration with HTML DOM elements through special proxy objects. When you work with Doh's HTML objects (created via New('ElementType')
or similar), their attr
and css
properties are proxies specifically designed for observation.
This allows you to directly observe
or mimic
HTML attributes and CSS style properties:
// Create Doh HTML elements
const button = New('Button', { text: 'Click Me' });
const panel = New('Panel', { style: 'height: 100px; background-color: lightblue;' });
// Observe the 'disabled' attribute of the button
Doh.observe(button.attr, "disabled", (obj, prop, newVal) => {
console.log(`Button is now ${newVal ? "disabled" : "enabled"}`);
});
// Observe the 'height' CSS property of the panel
Doh.observe(panel.css, "height", (obj, prop, newVal, oldVal) => {
console.log(`Panel height changed from ${oldVal} to ${newVal}`);
// Potentially trigger layout adjustments
// updateLayout();
});
// --- Trigger Changes Programmatically ---
// Disable the button - triggers the first observer
button.attr.disabled = true; // Logs: Button is now disabled
// Change the panel's height - triggers the second observer
panel.css.height = '150px'; // Logs: Panel height changed from 100px to 150px
// Enable the button again
button.attr.disabled = false; // Logs: Button is now enabled
// Note: If the browser or user interaction changes these attributes/styles
// (and Doh reflects these changes back to the proxies), the observers
// would trigger accordingly, creating a powerful link between your
// JavaScript logic and the live DOM state.
This capability creates a robust bridge, allowing your data model and JavaScript logic to react to changes in the visible UI, and vice-versa.
The observe
and mimic
systems are optimized for performance and have been successfully used in demanding production environments, including applications requiring smooth 60+ fps animations. They provide an efficient alternative to manual data synchronization methods, imposing minimal overhead.
Reactive systems can sometimes lead to infinite loops or performance issues if not carefully managed. Doh incorporates several safety mechanisms to mitigate these risks in complex observation networks:
setTimeout(0)
) between processing queued updates, preventing the system from hogging the event loop.onChangeCallback
in observe
and the internal logic of mimic
only propagate updates when a value actually changes, preventing unnecessary callback executions and potential cycles.These built-in protections allow developers to build sophisticated reactive systems with greater confidence, reducing the likelihood of encountering common pitfalls associated with reactive programming.
Under the hood, observe
and mimic
leverage JavaScript's property descriptors, defining custom getters and setters for the observed properties.
observe
: Creates a setter that manages a queue of onChangeCallback
and onEveryCallback
functions. It processes this queue asynchronously, invoking callbacks appropriately based on value changes.mimic
: Essentially sets up two observe
calls – one for each property pointing at the other – to create the bidirectional synchronization. It includes the logic for initial state synchronization.The implementation ensures that if a property already has native getters or setters, Doh preserves this original behavior by incorporating them into the observation mechanism, ensuring compatibility while adding reactive capabilities. The use of callback stacks allows multiple observers to coexist on the same property without conflict.