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
| Control | Import | Purpose |
|---|---|---|
Button | @io-oidis-usercontrols | Clickable button |
TextField | @io-oidis-usercontrols | Text input field |
Label | @io-oidis-usercontrols | Display text / headings |
DropDownList | @io-oidis-usercontrols | Self-contained single-select dropdown |
DropDownMultiselect | @io-oidis-usercontrols | Multi-select dropdown with checkboxes |
DropDownListItem | @io-oidis-usercontrols | Individual dropdown option |
DropDownSelectItem | @io-oidis-usercontrols | Checkbox dropdown option (multiselect) |
CheckBox | @io-oidis-usercontrols | Boolean checkbox |
Table<T> | @io-oidis-usercontrols | Data table with pagination |
Dialog | @io-oidis-usercontrols | Modal dialog |
Toast | @io-oidis-usercontrols | Notification message |
Switch | @io-oidis-usercontrols | Toggle switch with label |
ProgressBar | @io-oidis-usercontrols | Progress indicator (0-100%) |
Image | @io-oidis-usercontrols | Image with fallback support |
DropDownSearch | @io-oidis-usercontrols | Searchable single-select dropdown |
InlineInputGroup | @io-oidis-usercontrols | Inline text field with button |
InlineItem | @io-oidis-usercontrols | Removable tag/pill |
Accordion | @io-oidis-usercontrols | Collapsible sections |
AccordionItem | @io-oidis-usercontrols | Individual accordion section |
Range | @io-oidis-usercontrols | HTML5 range slider |
Pagination | @io-oidis-usercontrols | Page navigation (see Pagination) |
Autocomplete | @io-oidis-usercontrols | Typeahead search (see Autocomplete) |
ResponsiveTable<T> | @io-oidis-usercontrols | Responsive data table (see ResponsiveTable) |
DatePicker | @io-oidis-usercontrols | Single date or from/to range calendar (see Date & Time Pickers) |
DateTimePicker | @io-oidis-usercontrols | Combined calendar + time-wheel resolving a ZonedDateTime (see Date & Time Pickers) |
TimePicker | @io-oidis-usercontrols | Hours/minutes/seconds wheel picker (see Date & Time Pickers) |
PaginationItem | @io-oidis-usercontrols | Internal pagination button |
TableBody | @io-oidis-usercontrols | Internal 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 |
|---|---|---|
true | true | Visible and clickable |
true | false | Visible but grayed out (disabled) |
false | true | Hidden |
false | false | Hidden |
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);
disabled in HTML templatesThe 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 & → &, < → <, > → > directly in the bound <input>. Visible effect: a value like "Hero & Outreach s.r.o." renders as "Hero & 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.
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>`);
DropDownList
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:
| Method | Purpose |
|---|---|
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).
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.
DropDownMultiselect
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:
| Method | Purpose |
|---|---|
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"
};
}
}
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.
Dropdown Empty State Handling
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:
- Always call
Clear()first — before checking for empty data, not inside a conditional - Set descriptive text — use
Text()(self-contained) orContent()(manual) with a meaningful label like "Žádné skupiny" or "Vyberte" - Disable when empty —
Enabled(false)prevents the user from opening an empty menu - Re-enable when populated — always call
Enabled(true)in the success path - Handle API errors — in the catch block, set error text and disable the dropdown
- Cascade dependent dropdowns — if dropdown B depends on dropdown A's selection and A is empty, also disable B
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:
| Type | Usage |
|---|---|
ToastType.SUCCESS | Successful operations (save, delete, update) |
ToastType.ERROR | Failed operations (catch blocks) |
ToastType.WARNING | Non-critical issues |
ToastType.INFO | Informational 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:
| Method | Purpose |
|---|---|
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:
| Method | Purpose |
|---|---|
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:
| Method | Purpose |
|---|---|
Source($value?) | Get/set image source URL |
Fallback($value?) | Get/set fallback image URL |
Alt($value?) | Get/set alt text |
DropDownSearch
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:
| Method | Purpose |
|---|---|
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:
| Method | Purpose |
|---|---|
Value($value?) | Get/set the text field value |
field | Direct 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:
| Method | Purpose |
|---|---|
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:
| Method | Purpose |
|---|---|
AddItem($header, $content, $expanded?) | Create collapsible section, returns AccordionItem |
AccordionItem properties:
| Property | Purpose |
|---|---|
text | Header Button component |
content | Body 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:
| Method | Purpose |
|---|---|
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 reassignmentnew T()in the constructor — Aftersuper(), in declaration order- Types from
io-oidis-usercontrols— Import the control classes
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