Custom Data-Bind Attributes
Create custom data-bind attributes when built-in attributes lack specific DOM mutations. Define handler classes with init , update , and deinit methods, register them using engine.registerBindingAttribute , and evaluate the performance trade-offs of executing bindings in JavaScript instead of the native fast path.
The built-in data-bind-* attributes handle standard UI updates like toggling classes or conditionally rendering elements. Custom attributes allow
you to define specialized view-layer transformations or stateful behaviors that fall outside this standard set. A custom attribute requires a
JavaScript class with up to three lifecycle methods. The engine calls these methods during model synchronization and DOM attachment.
The Handler Class Structure
Section titled “The Handler Class Structure”A custom attribute handler is a plain JavaScript class.
class MyCustomHandler { init(element, value) { // Setup internal state, cache references, or attach listeners. }
update(element, value) { // React to model changes and update the DOM. }
deinit(element) { // Clean up timers, listeners, or external references. }}The init and update methods receive the target DOM element and the evaluated value of the binding expression. The deinit method receives
only the element. You only need to implement the methods required for your specific logic. The engine verifies method existence before execution.
Registering a Custom Attribute
Section titled “Registering a Custom Attribute”Call engine.registerBindingAttribute(attributeName, HandlerClass) to expose the handler to the engine. Execute this
registration inside engine.whenReady to ensure the binding system is fully initialized.
engine.whenReady.then(() => { engine.registerBindingAttribute("coh-my-attribute", MyCustomHandler);});<div data-bind-coh-my-attribute="{{model.someProperty}}"></div>The engine instantiates a separate handler for each DOM element carrying the attribute. Adopt a consistent prefix (like coh- or ui-) for custom
attributes to prevent naming collisions with built-in handlers.
Lifecycle Timing
Section titled “Lifecycle Timing”initexecutes on the firstsynchronizeModels()call after registration.updateexecutes immediately afterinitwith identical arguments, and subsequently on everysynchronizeModels()call.deinitexecutes when the engine removes the element from the DOM. Elements insidedata-bind-ifordata-bind-forblocks triggerdeinitupon removal and re-triggerinitandupdateon new handler instances upon reappearance.
Stateless Handlers: Number Formatting
Section titled “Stateless Handlers: Number Formatting”You can format numbers with thousand separators using a stateless custom attribute. This intercepts the raw number and formats it right before it hits the DOM.
import Block from '@components/Layout/Block/Block';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const StatsPanel = () => ( <Block class="stats-panel"> <TextBlock attr:data-bind-coh-format-number="{{StatsModel.kills}}" /> </Block>);<div class="stats-panel"> <span class="stat__label">Gold</span> <span class="stat__value" data-bind-coh-formatted-number="{{PlayerModel.gold}}"></span></div>class FormattedNumber { update(element, value) { const num = Number(value);
// Fallback for non-numeric values if (isNaN(num)) { element.textContent = value; return; }
// Split integer and decimal parts, then insert commas into the integer const parts = num.toString().split("."); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); element.textContent = parts.join("."); }}
engine.whenReady.then(() => { engine.registerBindingAttribute("coh-formatted-number", FormattedNumber);
engine.createJSModel("PlayerModel", { gold: 1584230 }); engine.synchronizeModels();});Stateful Handlers: Typewriter Effect
Section titled “Stateful Handlers: Typewriter Effect”Some attributes must manage internal state over time independent of the model synchronization loop. A typewriter effect requires an independent
interval timer to append text character by character. Use init to start the process and deinit to clean it up.
<div class="dialogue"> <p class="dialogue__text" data-bind-coh-typewriter="{{DialogueModel.line}}"></p></div>class TypewriterText { constructor() { this.fullText = ""; this.charIndex = 0; this.intervalId = null; }
_startReveal(element) { this.charIndex = 0; element.textContent = "";
this.intervalId = setInterval(() => { if (this.charIndex < this.fullText.length) { element.textContent += this.fullText[this.charIndex]; this.charIndex++; } else { clearInterval(this.intervalId); this.intervalId = null; } }, 40); }
init(element, value) { this.fullText = String(value); this._startReveal(element); }
update(element, value) { const newText = String(value);
// Only restart the timer if the text actually changed if (newText !== this.fullText) { if (this.intervalId) clearInterval(this.intervalId); this.fullText = newText; this._startReveal(element); } }
deinit(element) { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } }}
engine.whenReady.then(() => { engine.registerBindingAttribute("coh-typewriter", TypewriterText);});Architectural Evaluation
Section titled “Architectural Evaluation”Custom attributes expand the binding system but bypass the native evaluation pipeline.
Built-in attributes evaluate entirely within the SDK’s C++ fast path. They require zero JavaScript execution time , making them optimal for high-frequency updates.
Custom attributes always execute within the JavaScript environment. Invoking the update method incurs standard JavaScript execution
overhead . This cost scales linearly with the number of bound elements and the complexity of the internal DOM manipulation.
Use custom attributes exclusively for complex DOM mutations, time-driven internal state, or text formatting that cannot be reasonably precomputed. Do not replace basic native bindings (like toggling a class) with custom JavaScript equivalents.
© 2026 Coherent Labs. All rights reserved.