Skip to main content

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:

  1. The Panel's constructor creates every component instance (new TextField(), …)
  2. The Panel's innerHtml() is called, producing an HTML string
  3. data-oidis-bind="${this.nameInput}" evaluates to the instance's unique binding identifier
  4. The framework scans the rendered DOM for these identifiers
  5. Each data-oidis-bind element is linked to the corresponding component instance, which becomes a live, typed reference to the DOM element
Binding does not create instances

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:

ComponentPackagePurpose
TextFieldio-oidis-usercontrolsText input field
Buttonio-oidis-usercontrolsClickable button
Labelio-oidis-usercontrolsDisplay text
DropDownio-oidis-usercontrolsDropdown selector
CheckBoxio-oidis-usercontrolsCheckbox input
Table<T>io-oidis-usercontrolsData table
Dialogio-oidis-usercontrolsModal dialog
Toastio-oidis-usercontrolsNotification toast
GuiCommonsio-oidis-guiPlain layout container
Custom PanelsprojectSub-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 a TextField to an existing <input>, the component uses that element directly. This lets you set type, class, placeholder and 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 via innerHtml().
  • <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:

AspectquerySelectordata-oidis-bind
Type safetyReturns Element | nullReturns typed component (TextField, Button, etc.)
Null riskCan return null if selector is wrongBinding is guaranteed at render time
RefactoringString-based — no compile-time checksProperty-based — TypeScript catches renames
API accessRaw DOM API onlyFull GuiCommons API (Value, Text, Visible, etc.)
Framework integrationManualAutomatic 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​

  1. Always use readonly for component properties — prevents accidental reassignment
  2. Instantiate in the constructor — this.x = new T(); after super(); the binding system never creates instances
  3. Use declare for model-backed properties — Prevents TypeScript initialization conflicts
  4. One Panel = one HTML template — Keep innerHtml() focused on structure, not logic
  5. Descriptive names — nameInput not input1, saveButton not btn
  6. 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