Data Binding
Oidis uses the data-oidis-bind HTML attribute to connect DOM elements to typed TypeScript component references. This is the bridge between your Panel's HTML template and the Controller's logic.
How It Works
In a Panel's innerHtml() method, you place data-oidis-bind attributes that reference the Panel's readonly properties:
export class ProductPanel extends BasePanel {
public readonly nameInput : TextField;
public readonly priceInput : TextField;
public readonly saveButton : Button;
public readonly table : Table<ProductTableItem>;
constructor() {
super();
this.nameInput = new TextField();
this.priceInput = new TextField();
this.saveButton = new Button();
this.table = new Table<ProductTableItem>();
}
public innerHtml() : string {
return `
<div class="ProductForm">
<div data-oidis-bind="${this.nameInput}"></div>
<div data-oidis-bind="${this.priceInput}"></div>
<div data-oidis-bind="${this.saveButton}"></div>
</div>
<div data-oidis-bind="${this.table}"></div>
`;
}
}
What happens at runtime:
- The Panel's constructor creates every component instance (
new TextField(), …) - The Panel's
innerHtml()is called, producing an HTML string data-oidis-bind="${this.nameInput}"evaluates to the instance's unique binding identifier- The framework scans the rendered DOM for these identifiers
- Each
data-oidis-bindelement is linked to the corresponding component instance, which becomes a live, typed reference to the DOM element
The framework only attaches existing instances to the DOM — nothing instantiates panel members for you. A member declared as public readonly x : T = undefined; stays undefined: ${this.x} renders the text undefined and GuiCommons.Register() logs <Class>.<member> was declared but instance is missing. Did you miss it in constructor?. Always declare public readonly x : T; and assign this.x = new T(); in the constructor after super().
After binding, the Controller can interact with components using the GuiCommons API:
// In Controller
$instance.nameInput.Value("Product A"); // Set input value
$instance.saveButton.Enabled(false); // Disable button
$instance.table.Visible(true); // Show table
Supported Component Types
data-oidis-bind works with all Oidis UI controls:
| Component | Package | Purpose |
|---|---|---|
TextField | io-oidis-usercontrols | Text input field |
Button | io-oidis-usercontrols | Clickable button |
Label | io-oidis-usercontrols | Display text |
DropDown | io-oidis-usercontrols | Dropdown selector |
CheckBox | io-oidis-usercontrols | Checkbox input |
Table<T> | io-oidis-usercontrols | Data table |
Dialog | io-oidis-usercontrols | Modal dialog |
Toast | io-oidis-usercontrols | Notification toast |
GuiCommons | io-oidis-gui | Plain layout container |
| Custom Panels | project | Sub-panel composition |
DOM Element Adoption vs. Generation
When the framework encounters a data-oidis-bind attribute, it decides whether to adopt the existing DOM element or generate new HTML from the component's innerHtml():
<input>elements are always adopted — if you bind aTextFieldto an existing<input>, the component uses that element directly. This lets you settype,class,placeholderand other attributes in your HTML template.- Empty
<div>triggers generation — if you bind to<div data-oidis-bind="${this.field}"></div>with no inner content, the component generates its own HTML viainnerHtml(). <div>with inner content is adopted — if the element already contains HTML, the component adopts it.
<!-- Adopted: TextField uses this exact <input>, preserving type="date" and classes -->
<input class="form-control" type="date" data-oidis-bind="${this.dateField}" />
<!-- Generated: Empty div → TextField renders its own <input> inside -->
<div data-oidis-bind="${this.textField}"></div>
<!-- Adopted: Composite controls like Autocomplete always generate into <div> -->
<div data-oidis-bind="${this.searchField}"></div>
This mechanism is useful when you need specific HTML attributes (like type="date", type="number", custom CSS classes, or placeholder) that the component's default innerHtml() doesn't provide.
Layout Containers
For container-level operations (show/hide, enable/disable a whole section), bind a plain GuiCommons instance (@io-oidis-gui/Io/Oidis/Gui/Bootstrap/Primitives/GuiCommons.js):
public readonly listContent : GuiCommons;
public readonly editContent : GuiCommons;
constructor() {
super();
this.listContent = new GuiCommons();
this.editContent = new GuiCommons();
}
<div data-oidis-bind="${this.listContent}" class="ListContent">
<!-- List view content -->
</div>
<div data-oidis-bind="${this.editContent}" class="EditContent d-none">
<!-- Edit form content -->
</div>
In the Controller:
$instance.listContent.Visible(false);
$instance.editContent.Visible(true);
Sub-Panel Composition
Panels can embed other Panels via data-oidis-bind:
public readonly detailsPanel : OrderDetailsPanel;
constructor() {
super();
this.detailsPanel = new OrderDetailsPanel();
}
public innerHtml() : string {
return `
<div data-oidis-bind="${this.table}"></div>
<div data-oidis-bind="${this.detailsPanel}"></div>
`;
}
The sub-panel is a full Panel instance with its own innerHtml(), but it's managed by the parent Controller — sub-panels do not have their own controllers.
Why Not querySelector?
The Oidis linter warns against querySelector usage. Here's why data-oidis-bind is preferred:
| Aspect | querySelector | data-oidis-bind |
|---|---|---|
| Type safety | Returns Element | null | Returns typed component (TextField, Button, etc.) |
| Null risk | Can return null if selector is wrong | Binding is guaranteed at render time |
| Refactoring | String-based — no compile-time checks | Property-based — TypeScript catches renames |
| API access | Raw DOM API only | Full GuiCommons API (Value, Text, Visible, etc.) |
| Framework integration | Manual | Automatic lifecycle management |
// ❌ querySelector — fragile, untyped
const input = document.querySelector("#nameInput") as HTMLInputElement;
if (input) input.value = "test";
// ✅ data-oidis-bind — typed, safe
$instance.nameInput.Value("test");
InstanceOwner() — Escape Hatch
When you need the raw DOM element (for scrolling, focus, or third-party library integration), use InstanceOwner():
// Scroll to a component
$instance.nameInput.InstanceOwner().scrollIntoView({
behavior: "smooth",
block: "center"
});
// Get raw element for measurements
const rect = $instance.table.InstanceOwner().getBoundingClientRect();
This is the controlled escape hatch — you get the DOM element through the framework rather than bypassing it.
Event Binding
data-oidis-bind components support event binding through EventsBinding:
// ✅ Correct — use EventsBinding
EventsBinding.SingleClick($instance.saveButton, async () : Promise<void> => {
await this.handleSave();
});
// ✅ Also valid — use getEvents() on the component
$instance.saveButton.getEvents().setOnClick(() => {
this.handleClick();
});
// ❌ Incorrect — don't use addEventListener directly
$instance.saveButton.InstanceOwner().addEventListener("click", () => { ... });
See Panels and Controllers for the full event binding reference.
Best Practices
- Always use
readonlyfor component properties — prevents accidental reassignment - Instantiate in the constructor —
this.x = new T();aftersuper(); the binding system never creates instances - Use
declarefor model-backed properties — Prevents TypeScript initialization conflicts - One Panel = one HTML template — Keep
innerHtml()focused on structure, not logic - Descriptive names —
nameInputnotinput1,saveButtonnotbtn - Bootstrap layout in HTML — Use Bootstrap 5 classes (
row,col-md-6,d-flex) in the template
See also: Panels and Controllers | Events and Validation | Tables