Skip to main content

Events and Validation

Interaction logic in Oidis controllers is built from three pieces: EventsBinding.SingleClick for buttons, the component events manager (getEvents()) for every other event, and the TextField validation API together with ValidationBinding for form validation feedback.

EventsBinding​

Package: io-oidis-gui

import { EventsBinding } from "@io-oidis-gui/Io/Oidis/Gui/Bindings/EventsBinding.js";

EventsBinding has a single method:

EventsBinding.SingleClick($component, async ($eventArgs? : EventArgs) : Promise<boolean | void> => { ... }, $timeout?);

SingleClick protects against repeated clicks: the component is disabled when clicked and enabled again ~200 ms after the handler finishes. When the handler returns true, the component stays disabled until $timeout (default 3500 ms) expires. Calling stopPropagation() on the native event inside the handler throws — use getEvents().setOnClick() when you need propagation control.

There is no EventsBinding.Change, DoubleClick, Blur or Focus

Only SingleClick exists. Every other event is registered through getEvents() — see Alternative: getEvents().

Mandatory Error Handling​

Every async event handler must include a try/catch block. This is a strict convention:

EventsBinding.SingleClick($instance.saveButton, async () : Promise<void> => {
try {
if (!this.validateForm($instance)) {
return;
}
await this.client.SaveRecord(this.currentRecord);
this.ShowMessage("Saved successfully", ToastType.SUCCESS);
await this.FetchData();
} catch (ex) {
LogIt.Error("Save failed", ex);
this.ShowMessage("Failed to save", ToastType.ERROR);
}
});

Alternative: getEvents()​

Components expose their events through getEvents(), which returns an ElementEventsManager. It provides typed shortcut methods (setOnClick, setOnDoubleClick, setOnChange, setOnFocus, setOnBlur, setOnMouseOver, …) and low-level access via setEvent().

// Click — dropdown items and custom controls
ddItem.getEvents().setOnClick(() : void => {
$instance.selector.Content(ddItem.Text());
$instance.selector.Data(ddItem.Data());
});

// Change — fires after the field loses focus (onchange)
$instance.quantityInput.getEvents().setOnChange(() : void => {
const value : number = StringUtils.ToDouble($instance.quantityInput.Value());
this.recalculate(value);
});

Frequently used shortcut methods:

MethodDOM EventWhen it fires
setOnClick(handler)onclickClick
setOnDoubleClick(handler)ondblclickDouble click
setOnChange(handler)onchangeValue changed + field lost focus
setOnFocus(handler) / setOnBlur(handler)onfocus / onblurFocus gained / lost

Low-level setEvent() for other DOM events:

For events without a shortcut method, use setEvent() directly with the DOM event name (or an EventType constant from io-oidis-gui Enums/Events/EventType.ts, e.g. EventType.ON_KEY_UP):

// Input — fires on every keystroke (real-time updates)
$instance.percentageInput.getEvents().setEvent("oninput", () : void => {
this.updateRemaining($instance);
});
onchange vs oninput
  • setOnChange() (= onchange) — Fires when the user leaves the field. Best for: saving values, triggering API calls, running simulations.
  • setEvent("oninput", ...) (= oninput) — Fires on every keystroke. Best for: real-time UI feedback (remaining percentage, character count, live preview).
Inside Table<T> rows — always use setEvent()

setOnChange() registers handlers via the global EventsManager (globalEvents.setEvent()), which may not dispatch events correctly for dynamically created elements inside Table<T> rows. Always use setEvent("oninput", ...) or setEvent("onchange", ...) for inputs inside table rows — these register directly on the element's local event list and work reliably with dynamic DOM.

When to use which API:

  • EventsBinding.SingleClick() — Standard click handlers in resolver() (buttons, rows, links)
  • .getEvents().setOnClick() — Inline handlers for dynamically created items (dropdown items, table rows)
  • .getEvents().setOnChange() — Value change handlers on inputs (not inside Table<T> rows)
  • .getEvents().setEvent("oninput", ...) — Real-time input handlers, and all input handlers inside Table<T> rows

Where to Bind Events​

Events are always bound in the resolver() lifecycle method:

public async resolver($instance : MyPanel) : Promise<void> {
EventsBinding.SingleClick($instance.addButton, async () : Promise<void> => { ... });
EventsBinding.SingleClick($instance.deleteButton, async () : Promise<void> => { ... });
$instance.filterInput.getEvents().setOnChange(() : void => { ... });
}

Exception: Table row events are bound in beforeLoad() as part of the 3-callback BindData pattern (third callback).

Dynamically Created Components — Bind Before AddChild​

For components built imperatively (rows added on demand outside Table<T>), bind events before AddChild. Reversing the order leaves the DOM listener detached and the click silently no-ops.

const row : ContactRow = new ContactRow();
EventsBinding.SingleClick(row.deleteButton, handler);
$instance.contactsContainer.AddChild(row);

Form Validation​

Validation lives in two places, both in io-oidis-usercontrols:

  • TextField (Bootstrap) — per-field validation API: required flag, custom validator, valid/invalid marks and the inline feedback message.
  • ValidationBinding — static helpers for marks on any component, required-field checks on a group of fields, and input restrictions.
import { ValidationBinding } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bindings/ValidationBinding.js";

TextField Validation API​

MethodBehavior
Required($value? : boolean)Gets/sets the required flag; Validate() fails for an empty value
SetValidator(($value : string) => boolean)Custom format check, run by Validate() only for a non-empty value
Validate() : booleanRuns the required and custom checks and sets the is-valid / is-invalid mark
MarkValid() / MarkInvalid()Sets the mark without running any check
ResetValidation()Removes the marks and clears the feedback message
InvalidFeedback($message? : string) : stringGets/sets the text of the .invalid-feedback element

InvalidFeedback() looks for the next sibling of the input with the invalid-feedback class first, then for any .invalid-feedback element in the parent container. The panel layout must contain that element — without it the call silently does nothing:

<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" data-oidis-bind="${this.nameInput}">
<div class="invalid-feedback"></div>
</div>

Required flags and validators are configured once, in the panel constructor or in beforeLoad():

this.nameInput.Required(true);
this.emailInput.Required(true);
this.emailInput.SetValidator(($value : string) : boolean => new EmailValidator().validate($value));

ValidationBinding Helpers​

MethodBehavior
IsValidInput($component, $value)Sets is-valid (true) or is-invalid (false) on any component
IsValidDropDown($button, $value)Same for a dropdown button
RestoreValidMark($component)Removes the validation classes (does not clear the feedback text)
RequiredFields($fields : TextField[]) : booleanMarks every field valid/invalid by emptiness; returns false when any is empty

Complete Validation Method​

private validateForm($instance : MyPanel) : boolean {
let validated : boolean = true;

// Clear previous state
[$instance.nameInput, $instance.emailInput, $instance.percentageInput].forEach(($field : TextField) : void => {
$field.ResetValidation();
});

// Required and format checks
if (!$instance.nameInput.Validate()) {
$instance.nameInput.InvalidFeedback("Title is required");
validated = false;
}
if (!$instance.emailInput.Validate()) {
$instance.emailInput.InvalidFeedback("Enter a valid e-mail");
validated = false;
}

// Business rule checked in the controller
const percentage : number = StringUtils.ToInteger($instance.percentageInput.Value());
if (percentage < 0 || percentage > 100) {
$instance.percentageInput.MarkInvalid();
$instance.percentageInput.InvalidFeedback("Value must be between 0 and 100");
validated = false;
}

if (!validated) {
$instance.nameInput.InstanceOwner().scrollIntoView({behavior: "smooth", block: "center"});
}
return validated;
}

For a quick required-only check of a group of fields, ValidationBinding.RequiredFields([$instance.nameInput, $instance.emailInput]) marks all of them at once and returns the combined result.

Input Restrictions​

Set in beforeLoad() to restrict what users can type:

public async beforeLoad($instance : MyPanel) : Promise<void> {
ValidationBinding.setOnlyZIPFormat($instance.zipInput);
ValidationBinding.setOnlyPhoneFormat($instance.phoneInput);
ValidationBinding.setOnlyDoubleAllowed($instance.priceInput);
}
MethodRestriction
setOnlyNumbersAllowed($field)Digits only (inputmode="number")
setOnlyDoubleAllowed($field)Decimal number
setOnlyZIPFormat($field)Digits, at most 5 characters
setOnlyPhoneFormat($field)Digits and +, at most 12 digits (inputmode="tel")
setOnlyLowercaseAllowed($field, $onChange?)Displays lowercase (CSS text-transform only — the value is not changed); $onChange reports Caps Lock / Shift state
setOnlyQuerySafeAllowed($field)HTML-escapes &, <, > while typing

These restrictions prevent invalid characters at input time, before any validation logic runs. They are a UX aid only — the backend must validate the data again.

setOnlyQuerySafeAllowed replaces oninput

setOnlyQuerySafeAllowed() assigns the native oninput property of the input element, so it overrides any other handler assigned the same way.

Combining Events and Validation​

Here's a typical pattern showing both working together:

public async resolver($instance : SettingsPanel) : Promise<void> {

EventsBinding.SingleClick($instance.saveButton, async () : Promise<void> => {
try {
if (!this.validateForm($instance)) {
return;
}
this.currentRecord.Name = $instance.nameInput.Value();
this.currentRecord.Email = $instance.emailInput.Value();
await this.client.SaveSettings(this.currentRecord);

this.ShowMessage("Uloženo", ToastType.SUCCESS);
await this.FetchData();
} catch (ex) {
LogIt.Error("Save failed", ex);
this.ShowMessage("Nepodařilo se uložit", ToastType.ERROR);
}
});

// Clear validation on input (optional UX improvement)
$instance.nameInput.getEvents().setEvent("oninput", () : void => {
$instance.nameInput.ResetValidation();
});
}

See also: Panels and Controllers | Tables | Dialogs | Events System | Validators