Skip to main content

User Controls

Oidis provides a set of built-in UI controls in the io-oidis-usercontrols package. All controls share the GuiCommons API and work with data-oidis-bind.

Available Controls​

ControlImportPurpose
Button@io-oidis-usercontrolsClickable button
TextField@io-oidis-usercontrolsText input field
Label@io-oidis-usercontrolsDisplay text / headings
DropDownList@io-oidis-usercontrolsSelf-contained single-select dropdown
DropDownMultiselect@io-oidis-usercontrolsMulti-select dropdown with checkboxes
DropDownListItem@io-oidis-usercontrolsIndividual dropdown option
DropDownSelectItem@io-oidis-usercontrolsCheckbox dropdown option (multiselect)
CheckBox@io-oidis-usercontrolsBoolean checkbox
Table<T>@io-oidis-usercontrolsData table with pagination
Dialog@io-oidis-usercontrolsModal dialog
Toast@io-oidis-usercontrolsNotification message
Switch@io-oidis-usercontrolsToggle switch with label
ProgressBar@io-oidis-usercontrolsProgress indicator (0-100%)
Image@io-oidis-usercontrolsImage with fallback support
DropDownSearch@io-oidis-usercontrolsSearchable single-select dropdown
InlineInputGroup@io-oidis-usercontrolsInline text field with button
InlineItem@io-oidis-usercontrolsRemovable tag/pill
Accordion@io-oidis-usercontrolsCollapsible sections
AccordionItem@io-oidis-usercontrolsIndividual accordion section
Range@io-oidis-usercontrolsHTML5 range slider
Pagination@io-oidis-usercontrolsPage navigation (see Pagination)
Autocomplete@io-oidis-usercontrolsTypeahead search (see Autocomplete)
ResponsiveTable<T>@io-oidis-usercontrolsResponsive data table (see ResponsiveTable)
DatePicker@io-oidis-usercontrolsSingle date or from/to range calendar (see Date & Time Pickers)
DateTimePicker@io-oidis-usercontrolsCombined calendar + time-wheel resolving a ZonedDateTime (see Date & Time Pickers)
TimePicker@io-oidis-usercontrolsHours/minutes/seconds wheel picker (see Date & Time Pickers)
PaginationItem@io-oidis-usercontrolsInternal pagination button
TableBody@io-oidis-usercontrolsInternal table body container

Common API (GuiCommons)​

Every control supports these methods:

component.Value($v?)      // Get/set value (inputs)
component.Text($v?) // Get/set display text (labels, cells)
component.Content($v?) // Get/set HTML or dropdown display
component.Data($v?) // Get/set associated data object
component.Visible($v?) // Show/hide
component.Enabled($v?) // Enable/disable
component.Active($v?) // Active state (tabs, menu items)
component.InstanceOwner() // Get raw HTMLElement
component.getEvents() // Get ElementEventsManager
component.Clear() // Remove children
component.AddChild($el) // Append child component

Button​

Clickable action trigger. Bind clicks via EventsBinding.SingleClick().

// Panel
public readonly saveButton : Button;
public readonly cancelButton : Button;

// Constructor
this.saveButton = new Button();
this.cancelButton = new Button();

// Controller — resolver()
EventsBinding.SingleClick($instance.saveButton, async () : Promise<void> => {
await this.handleSave();
});

// Enable/disable
$instance.saveButton.Enabled(false); // Disable during save
$instance.saveButton.Enabled(true); // Re-enable after

Visible vs. Enabled​

Button has two independent states — both must be set for a fully interactive button:

Visible()Enabled()Result
truetrueVisible and clickable
truefalseVisible but grayed out (disabled)
falsetrueHidden
falsefalseHidden

A common mistake is setting only Visible(true) without Enabled(true) — the button appears but stays grayed out. Always set both when toggling button state:

// In resolver() — conditional visibility AND interactivity
$instance.adminButton.Visible(isAdmin);
$instance.adminButton.Enabled(isAdmin);
Never use hardcoded disabled in HTML templates

The native HTML disabled attribute takes precedence over .Enabled() — once set in the template, .Enabled(true) in the controller cannot override it.

<!-- ❌ Wrong — controller cannot re-enable this button -->
<button data-oidis-bind="${this.saveButton}" disabled>Save</button>

<!-- ✅ Correct — let the controller manage state exclusively -->
<button data-oidis-bind="${this.saveButton}">Save</button>

If a button should start disabled, set it in the controller (beforeLoad() or resolver()), not in HTML:

$instance.saveButton.Enabled(false);  // Start disabled, enable later

TextField​

Text input field. Use .Value() to get/set. Works with all <input> types — text, number, email, password, etc.

// Panel
public readonly nameInput : TextField;
public readonly emailInput : TextField;
public readonly priceInput : TextField;

// Constructor
this.nameInput = new TextField();
this.emailInput = new TextField();
this.priceInput = new TextField();

// Controller — read value
const name : string = $instance.nameInput.Value();

// Controller — set value
$instance.nameInput.Value("Default Name");

// Controller — clear
$instance.nameInput.Value("");

// Input restrictions (beforeLoad)
ValidationBinding.setOnlyNumbersAllowed($instance.zipInput);

HTML Entity Encoding (HtmlTagsEnabled)​

By default, TextField sanitizes its value via ValidationBinding.setOnlyQuerySafeAllowed() on first render — this converts & → &amp;, < → &lt;, > → &gt; directly in the bound <input>. Visible effect: a value like "Hero & Outreach s.r.o." renders as "Hero &amp; Outreach s.r.o." in the input.

To preserve raw characters, enable HTML tags on the field (this skips the sanitization binding):

// Panel constructor — apply before first render
this.clientInput = new TextField();
this.clientInput.HtmlTagsEnabled(true); // raw & < > are preserved

Apply to fields holding user-entered free text (names, company names, notes, tags) where the literal character must round-trip. Numeric / date inputs don't need it.

Numeric Inputs​

<input type="number"> binds to TextField — not to GuiCommons. The .Value() method returns a string, so convert with StringUtils.ToDouble() or StringUtils.ToInteger():

// Panel
public readonly quantityInput : TextField;

// Constructor
this.quantityInput = new TextField();

// innerHtml()
<input type="number" class="form-control" min="0" max="100" step="0.1"
data-oidis-bind="${this.quantityInput}" />

// Controller — set numeric value (convert to string)
$instance.quantityInput.Value("25.5");

// Controller — read numeric value (convert from string)
const quantity : number = StringUtils.ToDouble($instance.quantityInput.Value());

Label​

Display text. .Text() stores the value and renders it through .Content(), which writes innerHTML — both methods render HTML.

Escape externally sourced text

Label.Text() → GuiCommons.Content() → ElementManager.setInnerHtml() → element.innerHTML. Escape every value that comes from users, the database or an external API before passing it to Text() or Content(); only static markup you build yourself may be passed as-is. See GUI Pitfalls — Label.Text and Content Render HTML.

// Panel
public readonly statusLabel : Label;
public readonly countLabel : Label;

// Constructor
this.statusLabel = new Label();
this.countLabel = new Label();

// Controller — set text
$instance.statusLabel.Text("Active");
$instance.countLabel.Text(`${items.length} records`);

// Controller — set HTML
$instance.statusLabel.Content(`<strong>Active</strong>`);

Self-contained single-select dropdown — generates its own toggle button and menu. No separate selector/menu elements needed in the panel.

Import: @io-oidis-usercontrols/.../DropDownList.js (also exports DropDownListItem)

// Panel
public readonly categoryDropdown : DropDownList;

// Constructor
this.categoryDropdown = new DropDownList();
<!-- innerHtml() — the component generates the full dropdown markup -->
<div style="min-width: 200px;" data-oidis-bind="${this.categoryDropdown}"></div>
// Controller — populate in FetchData()
private buildCategoryMenu($instance : MyPanel) : void {
$instance.categoryDropdown.Clear();

for (const category of this.categories) {
const item : DropDownListItem = $instance.categoryDropdown.AddItem(category.Name);
item.Data(category);
item.getEvents().setOnClick(() : void => {
this.selectedCategoryId = category.Id;
$instance.categoryDropdown.Text(category.Name); // Update selector label
});
}

// Pre-select first item
if (this.categories.length > 0) {
this.selectedCategoryId = this.categories[0].Id;
$instance.categoryDropdown.Text(this.categories[0].Name);
}
}

API:

MethodPurpose
AddItem($text)Create and add a DropDownListItem, returns it
Clear()Remove all items
Text($value?)Get/set the selector button label
Enabled($value?)Enable/disable the entire dropdown
setSelectorStyle($value)Set button CSS classes (e.g. "btn-outline-primary dropdown-toggle")

Default styling: btn-primary btn-sm dropdown-toggle with pill shape (border-radius: 25px). Override via setSelectorStyle() or by subclassing (see below).

Subclassing for project-specific styling

The default DropDownList styling (small pill-shaped primary button) is designed for Hub GUI. Solution projects should create a subclass that overrides selectorCss in the constructor and optionally innerHtml():

export class MyDropDownList extends DropDownList {
constructor() {
super();
this.selectorCss = " btn-outline-primary dropdown-toggle";
}

protected innerHtml() : string {
return `
<div class="dropdown float-start w-100 h-100">
<button class="btn ${this.selectorCss} w-100 h-100" aria-expanded="false"
data-bs-toggle="dropdown" type="button"
data-oidis-bind="${this.selector}">${this.Text()}</button>
<div data-oidis-bind="${this.menu}" class="dropdown-menu"></div>
</div>`;
}
}

selectorCss is protected and used in innerHtml() — it must be set in the constructor before the component renders. Calling setSelectorStyle() after rendering has no visual effect.

Multi-select dropdown with checkboxes, select all / unselect all buttons, and optional search. Extends DropDownSearch which extends DropDownList.

Import: @io-oidis-usercontrols/.../DropDownMultiselect.js (also exports DropDownSelectItem)

// Panel
public readonly tagDropdown : DropDownMultiselect;

// Constructor
this.tagDropdown = new DropDownMultiselect();
<!-- innerHtml() — self-contained, generates full markup with checkboxes -->
<div style="min-width: 300px;" data-oidis-bind="${this.tagDropdown}"></div>
// Controller — populate in FetchData()
private buildTagMenu($instance : MyPanel) : void {
$instance.tagDropdown.Clear();
$instance.tagDropdown.SearchEnabled(false); // Hide search bar

for (const tag of this.tags) {
const item : DropDownSelectItem = $instance.tagDropdown.AddItem(tag.Label);
item.Data(tag);
}
}

// Controller — read selected items
const selectedTags : Tag[] = <Tag[]>$instance.tagDropdown.Data();

API:

MethodPurpose
AddItem($text)Create a DropDownSelectItem with checkbox, returns it
Clear()Remove all items
Data()Returns array of .Data() from all selected items
SelectAll($value)Select or deselect all items
Select($item, $value)Select/deselect a specific item
IsAllSelected()Check if all items are selected
SearchEnabled($value?)Show/hide the search bar

Localization: The component has a protected localization property with {emptySelection, fullSelection, selectAll, unselectAll}. Default values are English. Since localization is used in innerHtml(), it must be set in the constructor before rendering — subclass to customize:

export class MyDropDownMultiselect extends DropDownMultiselect {
constructor() {
super();
this.selectorCss = " btn-outline-primary dropdown-toggle";
this.localization = {
emptySelection: "Choose items",
fullSelection : "All selected",
selectAll : "Select all",
unselectAll : "Unselect all"
};
}
}
Protected properties require subclassing

Both selectorCss and localization are protected and embedded into HTML at construction time via innerHtml(). You cannot set them from the controller after the component is created — the HTML is already rendered with the default values. Always use a subclass with constructor overrides.

When a dropdown is populated from an API call, the result may be empty (no records) or the call may fail. Without proper handling, the dropdown selector can display a raw UUID or remain in an undefined state. Always handle the empty case explicitly.

Pattern for DropDownList (single-select):

private buildGroupMenu($instance : MyPanel) : void {
$instance.groupDropdown.Clear();

if (ObjectValidator.IsEmptyOrNull(this.groups) || this.groups.length === 0) {
$instance.groupDropdown.Text("Žádné skupiny");
$instance.groupDropdown.Enabled(false);
return;
}

$instance.groupDropdown.Enabled(true);

for (const group of this.groups) {
const item : DropDownListItem = $instance.groupDropdown.AddItem(group.Name);
item.Data(group);
item.getEvents().setOnClick(() : void => {
$instance.groupDropdown.Text(group.Name);
});
}

// Pre-select first item
$instance.groupDropdown.Text(this.groups[0].Name);
}

Pattern for DropDownMultiselect:

private buildReportMenu($instance : MyPanel) : void {
$instance.reportDropdown.Clear();

if (ObjectValidator.IsEmptyOrNull(this.reports) || this.reports.length === 0) {
$instance.reportDropdown.Enabled(false);
return;
}

$instance.reportDropdown.Enabled(true);

for (const report of this.reports) {
const item : DropDownSelectItem = $instance.reportDropdown.AddItem(report.Label);
item.Data(report);
}
}

Pattern for manual dropdown (Button + DropDownList menu):

// When using separate selector button + menu (e.g. in dialogs)
$instance.groupSelector.Content("Vyberte skupinu"); // Placeholder text
$instance.groupSelector.Data(null);
$instance.groupMenu.Clear();

try {
const groups : EANGroup[] = (await this.client.ListGroups()).data;
if (ObjectValidator.IsEmptyOrNull(groups) || groups.length === 0) {
$instance.groupSelector.Content("Žádné skupiny");
$instance.groupSelector.Enabled(false);
} else {
$instance.groupSelector.Enabled(true);
// ... populate items
}
} catch (ex) {
LogIt.Error("Failed to load groups", ex);
$instance.groupSelector.Content("Chyba načítání");
$instance.groupSelector.Enabled(false);
}

Rules:

  1. Always call Clear() first — before checking for empty data, not inside a conditional
  2. Set descriptive text — use Text() (self-contained) or Content() (manual) with a meaningful label like "Žádné skupiny" or "Vyberte"
  3. Disable when empty — Enabled(false) prevents the user from opening an empty menu
  4. Re-enable when populated — always call Enabled(true) in the success path
  5. Handle API errors — in the catch block, set error text and disable the dropdown
  6. Cascade dependent dropdowns — if dropdown B depends on dropdown A's selection and A is empty, also disable B
DropDownMultiselect has built-in empty text

DropDownMultiselect automatically shows its localization.emptySelection text (default "Select") when no items are selected. However, this does not cover the case when the dropdown has zero items — you still need to disable the component explicitly when there is no data to select from.

CheckBox​

Boolean toggle. Use .Value() which returns a boolean.

// Panel
public readonly isActiveCheckbox : CheckBox;

// Constructor
this.isActiveCheckbox = new CheckBox();

// Controller — read
const isActive : boolean = $instance.isActiveCheckbox.Value();

// Controller — set
$instance.isActiveCheckbox.Value(true);

Table<T>​

Data table with server-side pagination and typed row items.

// Panel
public readonly table : Table<ProductTableItem>;

// Constructor
this.table = new Table<ProductTableItem>();

// Controller — bind in beforeLoad()
$instance.table.BindData(fetchCallback, bindCallback, eventsCallback);

// Controller — refresh
await $instance.table.ShowData(true); // Force refresh
await $instance.table.ShowData(); // Cached

See Tables for the complete 3-callback pattern.

Dialog​

Modal dialog bound to a Bootstrap .modal element. DialogBinding adds Esc/Enter handling.

// Panel
public readonly confirmDialog : Dialog;

// Constructor
this.confirmDialog = new Dialog();

// Controller — setup in resolver()
new DialogBinding().Bind({
close : $instance.cancelBtn, // a single Button also gets a click handler that hides the dialog
holder: $instance.confirmDialog,
submit: $instance.confirmBtn
});

// Open
$instance.confirmDialog.Data(record);
$instance.confirmDialog.Visible(true);

// Close
$instance.confirmDialog.Visible(false);

See Dialogs for the complete dialog pattern.

Toast​

Notification message shown to the user. Typically accessed through the controller's ShowMessage() method:

// Using controller's built-in method (preferred)
this.ShowMessage("Record saved successfully", ToastType.SUCCESS);
this.ShowMessage("Failed to save", ToastType.ERROR);
this.ShowMessage("Connection restored", ToastType.INFO);
this.ShowMessage("Rate limit approaching", ToastType.WARNING);

Toast types:

TypeUsage
ToastType.SUCCESSSuccessful operations (save, delete, update)
ToastType.ERRORFailed operations (catch blocks)
ToastType.WARNINGNon-critical issues
ToastType.INFOInformational messages

Switch​

Toggle switch extending CheckBox with a text label. Use .Value() for boolean state and .Text() for the label.

// Panel
public readonly darkModeSwitch : Switch;

// Constructor
this.darkModeSwitch = new Switch();

// Controller — read
const isDarkMode : boolean = $instance.darkModeSwitch.Value();

// Controller — set
$instance.darkModeSwitch.Value(true);
$instance.darkModeSwitch.Text("Dark Mode");

API:

MethodPurpose
Value($value?)Get/set boolean state (inherited from CheckBox)
Text($value?)Get/set switch label text
Enabled($value?)Enable/disable the switch

ProgressBar​

Progress indicator displaying a percentage value (0-100).

// Panel
public readonly uploadProgress : ProgressBar;

// Constructor
this.uploadProgress = new ProgressBar();

// Controller — set value
$instance.uploadProgress.Value(75); // 75%

// Controller — read value
const progress : number = $instance.uploadProgress.Value();

API:

MethodPurpose
Value($value?)Get/set progress percentage (0-100)
Visible($value?)Show/hide the progress bar

Image​

Image component with fallback URL support. If the primary source fails to load, the fallback image is displayed automatically.

// Panel
public readonly avatar : Image;

// Constructor
this.avatar = new Image();

// Controller
$instance.avatar.Source(user.AvatarUrl);
$instance.avatar.Fallback("/assets/default-avatar.png");
$instance.avatar.Alt("User avatar");

API:

MethodPurpose
Source($value?)Get/set image source URL
Fallback($value?)Get/set fallback image URL
Alt($value?)Get/set alt text

Extends DropDownList with an integrated search field. Items are filtered when the user types 3+ characters. Includes search and clear filter buttons.

// Panel
public readonly countryDropdown : DropDownSearch;

// Constructor
this.countryDropdown = new DropDownSearch();

// Controller — populate
$instance.countryDropdown.Clear();
for (const country of countries) {
const item : DropDownListItem = $instance.countryDropdown.AddItem(country.Name);
item.Data(country);
item.getEvents().setOnClick(() : void => {
$instance.countryDropdown.Text(country.Name);
});
}

API:

MethodPurpose
AddItem($text)Create a searchable DropDownListItem, returns it
Clear()Remove all items and reset search
FillInParent($value?)Get/set whether dropdown menu fills parent width (default true)
Text($value?)Get/set selector button label (inherited)

InlineInputGroup​

Inline text field combined with a button. The component contains a field property (TextField) for direct access.

// Panel
public readonly tagInput : InlineInputGroup;

// Constructor
this.tagInput = new InlineInputGroup();

// Controller — read/write value
const tag : string = $instance.tagInput.Value();
$instance.tagInput.Value("");

// Controller — access internal field
$instance.tagInput.field.getEvents().setEvent(EventType.ON_KEY_UP, () : void => {
// Handle key events on the input
});

API:

MethodPurpose
Value($value?)Get/set the text field value
fieldDirect access to internal TextField

InlineItem​

Removable tag/pill component extending DropDownListItem. Used to display selected items that can be removed by clicking the "x" button.

// Controller — create inline items
const item : InlineItem = new InlineItem();
item.Text("JavaScript");
item.Data(tag);
item.Enabled(true);
item.getEvents().setOnClick(() : void => {
// Handle removal
});
container.AddChild(item);

API:

MethodPurpose
Text($value?)Get/set display text (inherited)
Data($value?)Get/set associated data (inherited)
Enabled($value?)Get/set enabled state of the remove button

Accordion​

Collapsible sections using Bootstrap Accordion. Each section has a header and expandable content area.

// Panel
public readonly faqAccordion : Accordion;

// Constructor
this.faqAccordion = new Accordion();

// Controller — add sections
const item : AccordionItem = $instance.faqAccordion.AddItem(
"What is Oidis?",
"Oidis is a full-stack TypeScript framework.",
true // expanded by default
);

$instance.faqAccordion.AddItem(
"How to install?",
"Run <code>oidis install</code> in the project root."
);

API:

MethodPurpose
AddItem($header, $content, $expanded?)Create collapsible section, returns AccordionItem

AccordionItem properties:

PropertyPurpose
textHeader Button component
contentBody GuiCommons container

Range​

HTML5 range slider with configurable min, max, step, and an optional descriptor component to display the current value.

// Panel
public readonly volumeSlider : Range;
public readonly volumeLabel : Label;

// Constructor
this.volumeSlider = new Range();
this.volumeLabel = new Label();

// Controller — configure
$instance.volumeSlider.Min(0);
$instance.volumeSlider.Max(100);
$instance.volumeSlider.Step(5);
$instance.volumeSlider.Value(50);

// Bind descriptor to show current value
$instance.volumeSlider.setDescriptorInstance($instance.volumeLabel);

API:

MethodPurpose
Value($value?)Get/set current slider value
Min($value?)Get/set minimum value
Max($value?)Get/set maximum value
Step($value?)Get/set step increment
setDescriptorInstance($value)Bind a GuiCommons component to display the current value

PaginationItem (Internal)​

Internal component used by Pagination to render individual page buttons. Not typically used directly — create pagination through Table or ResponsiveTable bindings.

TableBody (Internal)​

Internal component used by Table to wrap the table body content. Not typically used directly.

Declaration Rules​

All control declarations in panels follow the same pattern:

// ✅ Correct — readonly declaration, instance created in the constructor
public readonly nameInput : TextField;
public readonly table : Table<ItemType>;
public readonly dialog : Dialog;

constructor() {
super();
this.nameInput = new TextField();
this.table = new Table<ItemType>();
this.dialog = new Dialog();
}

// ❌ Incorrect — never instantiated, stays undefined
public readonly nameInput : TextField = undefined;

// ❌ Incorrect — missing readonly
public nameInput : TextField;

// ❌ Incorrect — field initializer instead of constructor assignment
public readonly nameInput : TextField = new TextField();
  • readonly — Prevents accidental reassignment
  • new T() in the constructor — After super(), in declaration order
  • Types from io-oidis-usercontrols — Import the control classes
The framework does not create controls

There is no decorator, reflection or build-time transform that instantiates panel members. A member declared as = undefined without a constructor assignment stays undefined: ${this.x} in innerHtml() renders the text undefined and GuiCommons.Register() logs <Class>.<member> was declared but instance is missing. Did you miss it in constructor?.


See also: Data Binding | Panels and Controllers | Events and Validation | Autocomplete | ResponsiveTable | Pagination | Date & Time Pickers