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.
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:
| Method | DOM Event | When it fires |
|---|---|---|
setOnClick(handler) | onclick | Click |
setOnDoubleClick(handler) | ondblclick | Double click |
setOnChange(handler) | onchange | Value changed + field lost focus |
setOnFocus(handler) / setOnBlur(handler) | onfocus / onblur | Focus 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);
});
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).
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 inresolver()(buttons, rows, links).getEvents().setOnClick()— Inline handlers for dynamically created items (dropdown items, table rows).getEvents().setOnChange()— Value change handlers on inputs (not insideTable<T>rows).getEvents().setEvent("oninput", ...)— Real-time input handlers, and all input handlers insideTable<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
| Method | Behavior |
|---|---|
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() : boolean | Runs 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) : string | Gets/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
| Method | Behavior |
|---|---|
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[]) : boolean | Marks 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);
}
| Method | Restriction |
|---|---|
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() 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