Dialogs
Dialogs in Oidis are Dialog components (a Bootstrap modal or offcanvas) opened and closed with Visible(). The
DialogBinding class adds keyboard handling — Esc clicks the close button, Enter clicks the submit button.
Package: io-oidis-usercontrols
import { DialogBinding } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bindings/DialogBinding.js";
import { Dialog } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/Dialog.js";
The Dialog Component
Dialog.Visible(true) shows the modal and Visible(false) hides it. The bound element must be a Bootstrap .modal (or
.offcanvas) element — the component creates the bootstrap.Modal / bootstrap.Offcanvas instance on the first call:
<div class="modal fade" role="dialog" tabindex="-1" data-oidis-bind="${this.deleteDialog}">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
...
</div>
</div>
</div>
The Bootstrap modal is driven only once the component is loaded, and it opens or closes asynchronously (~100 ms later). Visible()
without an argument returns the state tracked from the Bootstrap shown / hidden events, so it is also correct after the
user closes the dialog with the backdrop or a data-bs-dismiss="modal" button.
DialogBinding
Setup in resolver()
public async resolver($instance : MyPanel) : Promise<void> {
new DialogBinding().Bind({
close : $instance.cancelButton,
holder: $instance.confirmDialog,
submit: $instance.confirmButton
});
}
Bind options (IDialogUserControls):
| Option | Type | Description |
|---|---|---|
holder | Dialog | The dialog to manage |
close | Button | Button[] (optional) | Esc clicks the first button. A single Button also gets a click handler that hides the dialog |
submit | Button | Button[] | Enter clicks the first button while the dialog is visible. The dialog stays open — you control when to close |
close gets no click handlerOnly a single Button passed as close is wired to hide the dialog. When you pass an array, hide the dialog yourself —
with data-bs-dismiss="modal" on the buttons or with your own setOnClick() handlers:
new DialogBinding().Bind({
close : [$instance.closeKill, $instance.cancelKill],
holder: $instance.killDialog,
submit: $instance.killConfirm
});
[$instance.closeKill, $instance.cancelKill].forEach(($button : Button) : void => {
$button.getEvents().setOnClick(() : void => {
$instance.killDialog.Visible(false);
});
});
The Enter handler is registered on the window. While the dialog is visible, pressing Enter anywhere — including inside a
<textarea> — clicks the submit button.
holder.Visible() is trueThe window key handler checks holder.Visible() before it clicks anything. Dialog resets that state from its
hidden.bs.modal (hidden.bs.offcanvas) listener, so once the user closes the dialog through a data-bs-dismiss="modal"
button or the backdrop, Enter and Esc no longer click the buttons of the hidden dialog — no extra close handler is needed
for that.
The shown / hidden listeners are attached when the first Visible(true) or Visible(false) call on the loaded component
creates the Bootstrap instance. A dialog whose Visible() was never called with an argument — e.g. one opened only through
data-bs-toggle="modal" markup — keeps reporting false, and DialogBinding ignores its keys. Open bound dialogs with
Visible(true).
Opening the Dialog
Open a dialog by setting it visible. Optionally pass data:
EventsBinding.SingleClick($item.deleteButton, async () : Promise<void> => {
$instance.confirmDialog.Data($item.Data());
$instance.confirmDialog.Visible(true);
});
Handling Confirmation
The submit button's handler executes the action and closes the dialog:
EventsBinding.SingleClick($instance.confirmButton, async () : Promise<void> => {
try {
const record : IProduct = $instance.confirmDialog.Data();
$instance.confirmDialog.Visible(false);
await this.client.DeleteRecord(record.Id);
this.ShowMessage("Record deleted", ToastType.SUCCESS);
await $instance.table.ShowData();
} catch (ex) {
LogIt.Error("Delete failed", ex);
this.ShowMessage("Failed to delete", ToastType.ERROR);
}
});
Complete Example: Delete Confirmation
Panel
export class ProductPanel extends BasePanel {
public readonly table : ProductsTable;
public readonly deleteDialog : Dialog;
public readonly deleteName : Label;
public readonly deleteCancelButton : Button;
public readonly deleteConfirmButton : Button;
constructor() {
super();
this.table = new ProductsTable();
this.deleteDialog = new Dialog();
this.deleteName = new Label();
this.deleteCancelButton = new Button();
this.deleteConfirmButton = new Button();
}
protected innerHtml() : string {
return `
<div data-oidis-bind="${this.table}"></div>
<div class="modal fade" role="dialog" tabindex="-1" data-oidis-bind="${this.deleteDialog}">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<p>Are you sure you want to delete <strong data-oidis-bind="${this.deleteName}"></strong>?</p>
</div>
<div class="modal-footer">
<button class="btn btn-light" type="button" data-oidis-bind="${this.deleteCancelButton}">Cancel</button>
<button class="btn btn-danger" type="button" data-oidis-bind="${this.deleteConfirmButton}">Delete</button>
</div>
</div>
</div>
</div>`;
}
}
Controller
public async resolver($instance : ProductPanel) : Promise<void> {
new DialogBinding().Bind({
close : $instance.deleteCancelButton,
holder: $instance.deleteDialog,
submit: $instance.deleteConfirmButton
});
EventsBinding.SingleClick($instance.deleteConfirmButton, async () : Promise<void> => {
try {
const record : IProduct = $instance.deleteDialog.Data();
$instance.deleteDialog.Visible(false);
await this.client.DeleteProduct(record.Id);
this.ShowMessage("Product deleted", ToastType.SUCCESS);
await $instance.table.ShowData();
} catch (ex) {
LogIt.Error("Delete failed", ex);
this.ShowMessage("Failed to delete product", ToastType.ERROR);
}
});
}
public async beforeLoad($instance : ProductPanel) : Promise<void> {
$instance.table.BindData(
async ($offset : number, $limit : number) : Promise<IModelListResult> => { ... },
async ($data : IProduct, $item : ProductsTableItem) : Promise<void> => { ... },
async ($item : ProductsTableItem) : Promise<void> => {
EventsBinding.SingleClick($item.deleteButton, async () : Promise<void> => {
$instance.deleteDialog.Data($item.Data());
$instance.deleteName.Text(escapeHtml($item.Data().Name));
$instance.deleteDialog.Visible(true);
});
}
);
}
Label.Text() writes HTML — escape user data (see GUI Pitfalls).
Edit Form Dialog
For forms, a reusable Dialog subclass keeps the fields, filling and reading in one place and leaves only the save action
to the controller:
export class ProductDialog extends Dialog {
public readonly title : Label;
public readonly nameField : TextField;
public readonly priceField : TextField;
public readonly save : Button;
public readonly cancel : Button;
private editedId : string;
constructor() {
super();
this.title = new Label();
this.nameField = new TextField();
this.priceField = new TextField();
this.save = new Button();
this.cancel = new Button();
this.editedId = "";
this.nameField.Required(true);
}
public Open($record : IProduct) : void {
this.editedId = ObjectValidator.IsEmptyOrNull($record) ? "" : $record.Id;
this.title.Text(ObjectValidator.IsEmptyOrNull($record) ? "New product" : "Edit product");
this.nameField.Value(ObjectValidator.IsEmptyOrNull($record) ? "" : $record.Name);
this.priceField.Value(ObjectValidator.IsEmptyOrNull($record) ? "" : String($record.Price));
this.nameField.ResetValidation();
this.Visible(true);
}
public Validate() : boolean {
return this.nameField.Validate();
}
public getInput() : IProduct {
return {Id: this.editedId, Name: this.nameField.Value(), Price: StringUtils.ToDouble(this.priceField.Value())};
}
protected innerCode() : string {
this.cancel.getEvents().setOnClick(() : void => {
this.Visible(false);
});
return super.innerCode();
}
protected innerHtml() : string {
return `
<div class="modal fade" role="dialog" tabindex="-1">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title" data-oidis-bind="${this.title}">New product</h5></div>
<div class="modal-body">
<form onsubmit="return false;">
<input class="form-control" type="text" data-oidis-bind="${this.nameField}">
<div class="invalid-feedback">Name is required</div>
<input class="form-control mt-3" type="text" data-oidis-bind="${this.priceField}">
</form>
</div>
<div class="modal-footer">
<button class="btn btn-light" type="button" data-oidis-bind="${this.cancel}">Cancel</button>
<button class="btn btn-primary" type="button" data-oidis-bind="${this.save}">Save</button>
</div>
</div>
</div>
</div>`;
}
}
When a Dialog subclass has its own innerHtml(), the component uses the first child element (the .modal) as its
instance owner. In the panel it is bound to a plain placeholder: <div data-oidis-bind="${this.dialog}"></div>.
// Controller
EventsBinding.SingleClick($instance.addButton, async () : Promise<void> => {
$instance.dialog.Open(null);
});
EventsBinding.SingleClick($instance.dialog.save, async () : Promise<void> => {
if (!$instance.dialog.Validate()) {
return;
}
try {
await this.client.SaveProduct($instance.dialog.getInput());
$instance.dialog.Visible(false);
this.ShowMessage("Product saved", ToastType.SUCCESS);
await $instance.table.ShowData();
} catch (ex) {
LogIt.Error("Save failed", ex);
this.ShowMessage("Failed to save product", ToastType.ERROR);
}
});
Data Passing via .Data()
The .Data() method on dialog components is the standard way to pass context between the trigger and the handler:
// Store data when opening
$instance.dialog.Data(record);
// Retrieve data in the handler
const record : IProduct = $instance.dialog.Data();
This avoids the need for separate state variables when the dialog always operates on a single record.
Dialogs and Page Changes
Dialog keeps track of every open dialog (opened through Visible(true) or reported open by Bootstrap's shown event).
Dialog.HideAll() hides them all, and BasePageLayoutController.onSuccess calls it at the start of every page change,
before beforeTabLoad and processRequestPath.
Without it, a dialog that is still open when the page changes — its action navigated away, or a refused call
(HTTP 401) redirected to the "not authorized" page — loses its element with the old content while the Bootstrap backdrop
stays on <body>, and the new page cannot be clicked.
- A dialog opened in
beforeTabLoadorprocessRequestPathof the new page stays open; only the previous page's dialogs are closed. - A layout override of
onSuccessmust callsuper.onSuccess()before opening its own dialogs. - Call
Dialog.HideAll()yourself when content is replaced outside the page lifecycle.
Key Rules
- DialogBinding in
resolver()— Always set up dialog bindings in the resolver lifecycle method - Bind a
.modalelement —Dialogdrives Bootstrap's modal; a plain<div>is not styled or positioned as a dialog - Close handlers for arrays — Only a single
closebutton is wired automatically - Use
.Data()for context — Store the relevant record on the dialog component - Always try/catch — Dialog handlers follow the same error handling rules as all event handlers
- Refresh after action — Call
ShowData()on the table to reflect changes
See also: Panels and Controllers | Events and Validation | User Controls | GUI Pitfalls