Skip to content
SiteEmail

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.

A custom attribute handler is a plain JavaScript class.

handler-skeleton.js
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.

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.

registration.js
engine.whenReady.then(() => {
engine.registerBindingAttribute("coh-my-attribute", MyCustomHandler);
});
usage.html
<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.

  • init executes on the first synchronizeModels() call after registration.
  • update executes immediately after init with identical arguments, and subsequently on every synchronizeModels() call.
  • deinit executes when the engine removes the element from the DOM. Elements inside data-bind-if or data-bind-for blocks trigger deinit upon removal and re-trigger init and update on new handler instances upon reappearance.

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.

StatsPanel.tsx
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>
);
formatted-number.js
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();
});

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.

dialogue-box.html
<div class="dialogue">
<p class="dialogue__text" data-bind-coh-typewriter="{{DialogueModel.line}}"></p>
</div>
typewriter-text.js
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);
});

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.