Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Plugins - Lona Docs Log in

Plugins

Plugins are the UI layer for row types. A plugin decides how a row creates cells, binds data, exposes row options, and optionally participates in layout rows such as calendars and timelines.

What A Plugin Owns

  • cell DOM via makeCell() and bindCell()
  • optional async preloading via loadData
  • optional legends and row options
  • optional static and fixed children
  • optional layout participation through contentProviders

Minimal Plugin

import { LonaIcons, NumberIcon } from "@tento-ui/ui/icons";
import { NumberInput } from "@tento-ui/components/input/number-input";
import {
  type BindCellContext,
  type RenderPlugin,
} from "@tento-lona/sheets-ui";
import type { SheetDelegate } from "@tento-lona/sheets-ui";
import { SheetRow } from "@tento-lona";

export class NumberSheetPlugin implements RenderPlugin<NumberInput> {
  readonly id = "number";
  readonly icon: HTMLTemplateElement = LonaIcons.$template(NumberIcon);

  readonly loadData = null;
  readonly legendFactory = null;
  readonly makeRowOptions = null;

  optionsInfo(_row: SheetRow.Joined.Client): Option<string> {
    return "Store numeric values.";
  }

  makeCell(): NumberInput {
    return NumberInput.make();
  }

  bindCell(
    $cell: NumberInput,
    { row, column, isCurrent }: BindCellContext,
  ): void {
    const day = column.discrete()[0];
    const handle = row.cell(day);

    handle.onData((cell) => {
      $cell.bind(
        typeof cell?.value === "number" ? cell.value : null,
        { placeholder: isCurrent ? "Enter a value" : undefined },
      );
    });
  }

  onResize(): void {}
  onBindRow(_handle: SheetDelegate, _row: SheetRow.Joined.Client): void {}
  onViewportColumnsChanged(): void {}
  onRenderedColumnsWillChange(): void {}
}

The important point is that bindCell() receives the joined row and the resolved column. Most plugins read from row.cell(...), a row variant, or a store keyed by row.id.

Registering A Plugin

Register the plugin instance with the manager. The plugin's id is the row type key used at render time.

pluginManager.plugin(new NumberSheetPlugin());
pluginManager.plugin(new ColumnTaskPlugin(makeRowOptions, taskItemDelegate));

When the sheet renders a row with type === "tasks", it calls pluginManager.getPlugin("tasks") and delegates to that plugin.

BindCellContext

bindCell() and onResize() receive the same context object:

FieldMeaning
sheetRead-only sheet view model
rowJoined row for the current cell
columnResolved sheet column
widthPxPixel width allocated to the cell
inThePastWhether the column is before the current date
isCurrentWhether this is the current column
handleSheetDelegate for popovers, fixed children, and rebinding
nowCurrent date in the column timezone

Two patterns show up constantly:

const day = column.discrete()[0];
const cellHandle = row.cell(day);
const variant = row.variant as TasksVariant;
const index = variant.tasksIndex;

Async Data Loading

loadData is optional. Use it when the plugin needs async work to complete before binding visible cells.

readonly loadData = async (_sheet, row, column) => {
  await this.store.ensureLoaded(row.id, column);
};

The third argument is a NaiveDate.Partial, not a viewport range. For more complex layouts, child providers often do their own loading instead.

Positioning APIs

Every plugin receives a SheetDelegate through context.handle:

HandleUse case
handle.popoverScroll-aware popovers anchored to a row and column
handle.fixedChildScroll-aware overlays and spans
handle.staticChildRow-local elements positioned by raw pixel offset
handle.tooltipHover tooltips
handle.rebindRequest row, column, or structure rebinds

For example:

handle.popover.reveal(
  "task-details",
  $details,
  { x: column.withSubindex(0), y: row.id },
  { offsetY: 48, context: "task-details" },
);

Static And Fixed Children

Use staticChildren() for row-local UI such as banners or headers:

staticChildren(row: SheetRow.Joined.Client): StaticChildSpec[] {
  return [{
    id: "data-title",
    $element: DataTitleHeader.make().bind(row.label ?? "Untitled"),
    fillWidth: true,
  }];
}

Use fixedChildrenForRow() for items positioned against dates or columns:

async fixedChildrenForRow(
  row: SheetRow.Joined.Client,
  context: FixedChildRowContext,
): Promise<SheetDelegate.FixedChildSpec[]> {
  return [{
    id: "task-span",
    x: { type: "date-range", startDate, endDate },
    inset: { left: 0, top: 0 },
    widthBehavior: "fill",
    makeElement: () => TaskSpan.make(),
  }];
}

Participating In Layout Rows

If a child row type should render inside a calendar or timeline, add contentProviders to the plugin:

contentProviders: ContentProviders = {
  calendar: this.calendarProvider,
  timeline: {
    subcolumnFromRow: (row, parentRow) => this.makeTimelineProvider(row),
  },
};

See Layout Plugins & Content Providers, Subcolumn Plugins, and Parent Plugins for those contracts.

See Also