KanbanBoard
KanbanBoard is a workflow/status board that renders cards in columns and lets the user drag cards between columns.
A drop moves the card optimistically and then asks the consumer to persist the change — a rejected persist reverts
the card to its source column automatically.
Package: io-oidis-usercontrols (Bootstrap generation)
import { KanbanBoard } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/KanbanBoard.js";
Basic Usage
Declare a KanbanBoard in the panel, bind it in innerHtml(), and configure it in the controller:
// Panel
export class ProjectsPanel extends BasePanel {
public readonly board : KanbanBoard;
constructor() {
super();
this.board = new KanbanBoard();
}
public innerHtml() : string {
return `
<div data-oidis-bind="${this.board}"></div>
`;
}
}
// Controller — configure in beforeLoad()
public async beforeLoad($instance : ProjectsPanel) : Promise<void> {
$instance.board.setColumns([
{ key: "NEW", title: "New", badgeClass: "bg-secondary" },
{ key: "IN_PROGRESS", title: "In progress", badgeClass: "bg-primary" },
{ key: "DONE", title: "Done", badgeClass: "bg-success" },
{ key: "CONVERTED", title: "Converted", badgeClass: "bg-success", dropDisabled: true }
]);
$instance.board.OnCardMove(async ($cardId : string, $fromColumn : string, $toColumn : string) : Promise<boolean> => {
// the card is already shown in $toColumn; return false (or throw) to revert it back to $fromColumn
return await this.client.UpdateStatus($cardId, $toColumn);
});
$instance.board.OnCardClick(($cardId : string) : void => {
this.openDetail($cardId);
});
const records : IRecord[] = await this.client.ListRecords();
$instance.board.ShowData(records.map(($record : IRecord) : IKanbanCard => {
return {
id: $record.Id,
column: $record.Status,
contentHtml: `<div class="fw-bold">${this.escapeHtml($record.Name)}</div>`,
draggable: $record.Status !== "CONVERTED"
};
}));
}
contentHtml is injected into the card body as-is. The consumer is responsible for escaping its own data before
building the HTML string.
Interfaces
IKanbanColumn
| Property | Type | Purpose |
|---|---|---|
key | string | Unique column identifier, referenced by IKanbanCard.column |
title | string | Column header text |
badgeClass? | string | Bootstrap badge classes for the header counter, e.g. "bg-success" |
dropDisabled? | boolean | Cards cannot be dropped into this column |
count? | number | Overrides the displayed counter (e.g. server-side total); local card count otherwise |
IKanbanCard
| Property | Type | Purpose |
|---|---|---|
id | string | Unique card identifier, passed to the move and click callbacks |
column | string | Key of the column the card sits in |
contentHtml | string | Pre-rendered card body HTML — the consumer escapes its own data |
draggable? | boolean | Default true; false means the card cannot be dragged (e.g. terminal state) |
IKanbanBoardLocalization
| Property | Type | Default | Purpose |
|---|---|---|---|
emptyColumn | string | "No items" | Placeholder shown in a column without cards |
Optimistic Move and Revert
Dropping a card into another column updates the board state and the DOM immediately, then awaits the
OnCardMove() callback:
- callback resolves
true— the move is confirmed, the card stays in the target column, - callback resolves
falseor throws — the move is reverted: the card returns to its source column and the board re-renders.
Dropping a card back into its source column is a no-op — OnCardMove() is not called.
$instance.board.OnCardMove(async ($cardId : string, $fromColumn : string, $toColumn : string) : Promise<boolean> => {
const result : IUpdateResult = await this.client.UpdateStatus($cardId, $toColumn);
return result.success;
});
getCards() returns the current card state including optimistic moves, so it always reflects what the user sees.
Terminal States
Two flags cover states that must not participate in dragging — for example a derived CONVERTED stage that is
computed on the server and cannot be entered or left manually:
IKanbanColumn.dropDisabled— the column never accepts drops and never highlights as a drop target,IKanbanCard.draggable: false— the card cannot be picked up (it remains clickable).
Readonly(true) disables all dragging on the whole board while keeping cards clickable.
Responsive Layout
- Desktop (
lgand up): columns sit side by side in one horizontal flex row that scrolls inside the board — the page itself never scrolls horizontally. - Below
lg: columns stack vertically full-width and drag is inactive; cards remain clickable, soOnCardClick()stays the navigation path.
Drag affordances: the dragged card gets an is-dragging class and a hovered valid drop column gets is-drop-target;
dropDisabled columns never highlight.
The board uses native HTML5 DnD, which touch devices do not support. On touch (and below the lg breakpoint in
general) the board falls back to click-through — moves are done from the card detail instead.
Column Footer
setColumnFooter() renders a muted footer under a column, e.g. a "+12 more" hint when the column shows only a
subset of a larger server-side total (combine with IKanbanColumn.count):
$instance.board.setColumnFooter("NEW", "+12 more");
$instance.board.setColumnFooter("NEW", ""); // clears the footer
Localization
$instance.board.Localization({
emptyColumn: "Žádné položky"
});
API Reference
| Method | Signature | Purpose |
|---|---|---|
setColumns | ($columns : IKanbanColumn[]) : void | Define/replace columns, re-renders |
ShowData | ($cards : IKanbanCard[]) : void | Replace all cards, groups by column, re-renders |
getCards | () : IKanbanCard[] | Current card state incl. optimistic moves |
Readonly | ($value? : boolean) : boolean | Get/set read-only state; true disables all dragging |
OnCardMove | ($callback : ($cardId, $fromColumn, $toColumn) => Promise<boolean>) : void | Persist callback; false/throw reverts the move |
OnCardClick | ($callback : ($cardId : string) => void) : void | Card click handler |
Localization | ($value? : IKanbanBoardLocalization) : IKanbanBoardLocalization | Get/set UI labels |
setColumnFooter | ($columnKey : string, $html : string) : void | Muted footer under a column; "" clears |
Consumers
Reference integrations live in io-oidis-labs: the Projects board (project workflow states) and Lead Radar
(lead pipeline with a derived, drop-disabled CONVERTED stage).
See also: ResponsiveTable | User Controls