How to Use the Grid, Table, and Single Row Views of an Oracle APEX Interactive Grid

A tested guide to the grid, tableModelView, and recordView widgets behind the Oracle APEX interactive grid, with options, methods, and events.

An interactive grid in Oracle APEX draws its data with three view widgets. The grid widget is the familiar spreadsheet-like grid view. The tableModelView renders records with templates, and powers the cards region and the grid's icon and detail views. The recordView shows one record as a form, which is the grid's single row view. Going one level below the interactive grid to these widgets gives you control over columns, cell selection, fill and copy down, the active row while editing, and even lets you build selectable lists of your own.

This guide covers all three widgets with tested examples and their real output, and ends with two recipes: coloring rows by their values, and showing fewer columns on a phone.

Quick Reference

TaskWidget and methods
Read and change grid optionsgrid "option"
Change columnsgetColumns, hideColumn, showColumn, moveColumn, setColumnWidth, freezeColumn, unfreezeColumn
RedrawrefreshColumns, refresh, resize
Select rowsgetSelectedRecords, setSelectedRecords, getSelection, setSelection, selectAll
Select cells, fill, copy downgetSelectedRanges, setSelectedRanges, fillSelection, copyDownSelection
Move the current cellgetCurrentCell, setCurrentCell, gotoCell, getColumnForCell
Edit mode and the active rowsetEditMode, inEditMode, getActiveRecord, getActiveRecordId, getActiveCellFromColumnItem, setActiveRecordValue, finishEditing, lockActive, unlockActive
Page through rowsgetPageInfo, nextPage, gotoPage, and the other paging methods
Build a list from a modeltableModelView
Work with the single row viewrecordView: getRecord, getFields, fieldElement, gotoField, setEditMode, getActions, getToolbar

How to Run These Examples

The examples ran in Oracle APEX 26.1 on an editable interactive grid of stores with the static ID stores. The output under each example is exactly what the browser console printed.

To try one, open a page with an interactive grid, press F12, paste the code into the Console tab, and replace stores and the column names with your own. Several examples use await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.

How These Widgets Work

The three are jQuery UI widgets. You create one with $(element).grid(options), call a method with $(element).grid("method", args), and read or change an option with $(element).grid("option", name, value). Their events carry the widget name as a prefix: gridselectionchange, tablemodelviewselectionchange, recordviewrecordchange.

In an interactive grid, the grid widget is the view$ property of the grid view. Every grid example below starts from it:

Getting the grid widget:

const grid$ = apex.region("stores").call("getViews", "grid").view$;

The views, their models, and the grid-level methods above these widgets are covered in the guide to controlling an interactive grid with JavaScript.

Options Shared by the Grid and Table Model View

grid and tableModelView share a base widget, tableModelViewBase, with these common options:

OptionDescription
modelNameRequired. The ID of the model to show, which must already exist.
editableThe view allows editing, provided the model does too.
autoAddRecordAdd a record automatically when the model is empty.
hideDeletedRowsRemove deleted records at once instead of marking them.
paginationscroll (scroll paging, the default) or page buttons; virtual renders only the rows near the viewport; loadMore adds a Load More button; plus showPageLinks, maxLinks, showPageSelector, showRange, firstAndLastButtons, and hideSinglePage.
rowsPerPageRecords per page, when not scrolling.
persistSelectionKeep the selection in the model's metadata.
loadIncompleteSelectionWhat to do when the selection includes records not fetched yet.
footer, hideEmptyFooter, stickyFooter, stickyTopThe footer with status and paging, and sticky header and footer.
hasSize, fixedRowHeightThe container has a fixed height; all rows are the same height.
entityTitleSingular, entityTitlePluralWhat the records are called in status messages, as in "2 orders selected".
selectionStatusMessageKey, updateStatusThe text message key for the selection count, or a function that shows the status yourself.
noDataMessage, noDataIcon, showNullAsWhat to show when there is no data, and for null values.
highlightsHighlight rules, each with an ID, seq, row, color, background, and cssClass.
applyTemplateOptions, progressOptionsOptions for apex.util.applyTemplate and for the spinner.

The shared paging methods, getModel, getPageInfo, firstPage, previousPage, nextPage, lastPage, gotoPage, loadMore, and fetchAllData, work as they do for the cards region, described in the guide to selecting rows and paging through reports and cards. The shared editing methods are described with the grid below.

The Grid Widget

Grid Options

OptionDescription
columnsThe column definitions: an array holding one object of column names mapped to definitions.
columnGroupsHeading groups, with heading, headingAlignment, and parentGroupName.
rowHeaderThe row header: none, plain, sequence (row numbers), or label (from rowHeaderLabelColumn). rowHeaderCheckbox adds selection checkboxes, and rowHeaderWidth sets the width.
multiple, selectAllAllow several rows to be selected, and select all with Ctrl+A or the header checkbox.
selectCells, multipleCells, multipleRanges, selectCellsColumn, selectCellsRowCell selection instead of row selection: ranges, several ranges, and whole columns and rows. multipleRanges, selectCellsColumn, and selectCellsRow are new in 26.1.
allowSelectHiddenHidden rows, in collapsed control breaks, can be selected.
selectionStateItemA page item that receives the selected record IDs.
allowEditMode, allowInsert, allowDeleteKeyboard and mouse control: enter edit mode, Insert a row, Delete a row.
allowCopy, allowCut, allowPasteClipboard operations. allowCut and allowPaste are new in 26.1.
skipReadonlyCells, tabbableCellContentTab skips read-only cells in edit mode; content that is a tab stop in navigation mode.
columnSort, columnSortMultipleSorting from the headings. The grid only reports it through sortchange; the model and server do the sorting.
reorderColumns, resizeColumnsReordering and resizing from the headings.
collapsibleControlBreaksControl breaks can be collapsed.
aggregateLabels, aggregateTooltipsLabels and tooltips of aggregate rows.
contextMenu, contextMenuId, contextMenuActionA context menu.
tooltipA jQuery UI tooltip for cells.
constrainNavigationKeep the arrow keys from scrolling the page.

Each column definition has these properties:

PropertyDescription
heading, label, headingAlignment, alignmentThe heading (may contain markup), the plain label, and alignment.
seq, width, noStretch, frozen, hidden, canHideOrder, minimum width, stretching, freezing, and visibility.
elementIdThe column item that edits the column.
readonly, isRequired, virtual, noCopyEditing, and whether copy down and fill skip the column.
cellTemplate, escape, columnCssClasses, cellCssClassesColumn, headingCssClassesRendering: a template, escaping, and classes.
linkTargetColumn, linkText, linkAttributesLink columns.
groupName, useGroupForThe column group.
canSort, sortDirection, sortIndex, controlBreakDirection, controlBreakIndexSort and control break state, set by the server.
usedAsRowHeader, helpid, copyValueToClipboard, noHeaderActivateAccessibility, help, clipboard, and header activation.

In an interactive grid, set grid options with defaultGridViewOptions in the Initialization JavaScript Function, or change them at runtime as this example does:

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
const o = (name) => grid$.grid("option", name);
console.log({ editable: o("editable"), rowHeader: o("rowHeader"), rowHeaderCheckbox: o("rowHeaderCheckbox"),
              multiple: o("multiple"), selectCells: o("selectCells"), persistSelection: o("persistSelection"),
              columnSort: o("columnSort"), reorderColumns: o("reorderColumns"), showNullAs: o("showNullAs") });
console.log("pagination:", o("pagination"));
grid$.grid("option", "rowHeader", "sequence");            // row numbers instead of check boxes
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("row headers:", grid$.find("tbody th").slice(0, 5).map((i, th) => th.textContent.trim()).get().join(" "));

Output:

{
  "editable": true,
  "rowHeader": "plain",
  "rowHeaderCheckbox": true,
  "multiple": true,
  "selectCells": false,
  "persistSelection": true,
  "columnSort": true,
  "reorderColumns": true,
  "showNullAs": ""
}
pagination: {
  "scroll": true,
  "virtual": true,
  "loadMore": false,
  "showPageLinks": false,
  "maxLinks": 5,
  "showPageSelector": false,
  "showRange": true,
  "firstAndLastButtons": false,
  "hideSinglePage": false
}
row headers: 1 2 3 4 5

getColumns, hideColumn, showColumn, moveColumn, setColumnWidth, freezeColumn, and unfreezeColumn

getColumns returns the column definitions in their current order. The other methods change a column and render the grid again. moveColumnGroup moves a column group at a given level.

Syntax:

grid.getColumns() → object[]
grid.hideColumn(pColumn)
grid.showColumn(pColumn)
grid.moveColumn(pColumn, pNewPosition)
grid.moveColumnGroup(pLevel, pOriginalPosition, pNewPosition)
grid.setColumnWidth(pColumn, pWidth)
grid.freezeColumn(pColumn)
grid.unfreezeColumn(pColumn)

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
const show = () => grid$.grid("getColumns").filter((c) => !c.hidden).map((c) => c.property).join(" ");
console.log(grid$.grid("getColumns").filter((c) => !c.property.startsWith("APEX$")).slice(0, 3).map((c) => ({ name: c.property, heading: c.heading, width: c.width, seq: c.seq })));
grid$.grid("hideColumn", "COUNTRY");
grid$.grid("moveColumn", "CITY", 0);
grid$.grid("setColumnWidth", "STORE_NAME", 300);
grid$.grid("freezeColumn", "STORE_NAME");
console.log("visible:", show());
const nameColumn = grid$.grid("getColumns").find((c) => c.property === "STORE_NAME");
console.log("STORE_NAME:", { width: nameColumn.width, frozen: nameColumn.frozen });
grid$.grid("showColumn", "COUNTRY");
grid$.grid("unfreezeColumn", "STORE_NAME");

Output:

[
  {
    "name": "ID",
    "seq": 2
  },
  {
    "name": "STORE_NAME",
    "heading": "Store",
    "seq": 3
  },
  {
    "name": "CITY",
    "heading": "City",
    "seq": 4
  }
]
visible: CITY APEX$ROW_ACTION STORE_NAME STATE OPENED_ON FLOOR_AREA_SQFT FLAGSHIP
STORE_NAME: {"width":300, "frozen":true}

In an interactive grid, the column changes a user makes are saved with the report, but changes made with these methods are not, unless you save the report afterwards.

refreshColumns, refresh, and resize

After changing column definitions yourself, call refreshColumns and then refresh, which renders the grid from the model. resize adjusts the grid to a container that changed size. debugCellEdit(true) keeps a cell's editor in the cell after it loses focus, which helps item plug-in developers inspect its styles.

Syntax:

grid.refreshColumns()
grid.refresh([pFocus])
grid.resize()
grid.debugCellEdit(pValue)

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
const columns = grid$.grid("getColumns");
columns.find((c) => c.property === "CITY").heading = "Town";   // changed outside the grid
grid$.grid("refreshColumns");
grid$.grid("refresh");
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("heading:", grid$.find("thead th").filter((i, th) => th.textContent.trim() === "Town").length > 0);
grid$.grid("resize");
grid$.grid("fetchAllData");
grid$.grid("debugCellEdit", true);                       // item plug-in developers: keep editors in cells

Output:

heading: true

getSelectedRecords, setSelectedRecords, getSelection, setSelection, and selectAll

Read and set the selected rows, either as records or as row elements. setSelectedRecords returns how many rows it selected, and selectAll selects every row. The grid fires selectionchange.

Syntax:

grid.getSelectedRecords() → Record[]
grid.setSelectedRecords(pRecords, [pFocus], [pNoNotify]) → number
grid.getSelection() → jQuery[]
grid.setSelection(pRows, [pFocus], [pNoNotify])
grid.selectAll([pFocus], [pNoNotify])

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
const model = grid$.grid("getModel");
grid$.on("gridselectionchange", () => console.log("selectionchange:", grid$.grid("getSelectedRecords").length, "row(s)"));
grid$.grid("setSelectedRecords", [model.getRecord("2"), model.getRecord("7")]);
await new Promise((resolve) => setTimeout(resolve, 200));
console.log("rows:", grid$.grid("getSelection").map((row$) => row$.find("td").eq(1).text()));
grid$.grid("setSelection", grid$.find("tbody tr").slice(0, 1), false, true);   // the first row, no event
console.log("records:", grid$.grid("getSelectedRecords").map((r) => model.getValue(r, "CITY")));
grid$.grid("selectAll");
await new Promise((resolve) => setTimeout(resolve, 200));

Output:

selectionchange: 2 row(s)
rows: ["Orbit Chicago River North", "Orbit Boulder Pearl Street"]
records: ["Denver"]
selectionchange: 13 row(s)

The rows came back in grid order, Chicago before Boulder, even though the records were passed the other way round. The second selection passed pNoNotify, so it fired no event.

getSelectedRanges, setSelectedRanges, fillSelection, and copyDownSelection

With selectCells on, the selection is made of cell ranges, each written as {startRowIndex, startColIndex, endRowIndex, endColIndex} with zero-based indexes; getSelectedRanges adds the record and column keys. getSelectedRanges and setSelectedRanges are new in 26.1. fillSelection writes a value into the selected cells of some columns, and copyDownSelection copies the first row's values down; these are the Fill and Copy Down entries of the interactive grid. Both need edit mode, respect read-only cells, and update the model a moment after the call. getSelectedRange, for a single range, is deprecated.

Syntax:

grid.getSelectedRanges() → Range[]
grid.setSelectedRanges(pRanges, [pFocus], [pNoNotify])
grid.fillSelection(pFillValue, [pColumns], [pCallback]) → boolean
grid.copyDownSelection([pColumns], [pCallback]) → boolean

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
const model = grid$.grid("getModel");
grid$.grid("setEditMode", true);
grid$.grid("option", { selectCells: true });             // cell range selection
grid$.grid("setSelectedRanges", [{ startRowIndex: 0, startColIndex: 2, endRowIndex: 2, endColIndex: 3 }]);
console.log("ranges:", grid$.grid("getSelectedRanges"));
console.log("cells:", grid$.grid("getSelection").map((row$) => row$.map((i, td) => td.textContent).get().join(" | ")));
grid$.grid("fillSelection", "Colorado", ["STATE"]);        // fill STATE of the selected rows
await new Promise((resolve) => setTimeout(resolve, 300));   // the grid writes the model a moment later
console.log("STATE:", [0, 1, 2].map((i) => model.getValue(model.recordAt(i), "STATE")));
grid$.grid("copyDownSelection", ["CITY"]);                // copy the first row's CITY down
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("CITY:", [0, 1, 2].map((i) => model.getValue(model.recordAt(i), "CITY")));
model.revertRecords([0, 1, 2].map((i) => model.recordAt(i)));

Output:

ranges: [
  {
    "startRowIndex": 0,
    "startRowKey": "1",
    "startColIndex": 2,
    "startColKey": "CITY",
    "endRowIndex": 2,
    "endRowKey": "7",
    "endColIndex": 3,
    "endColKey": "STATE"
  }
]
cells: ["Denver | Colorado", "Seattle | Washington", "Chicago | Illinois"]
STATE: ["Colorado", "Colorado", "Colorado"]
CITY: ["Denver", "Denver", "Denver"]

The example waits after each call before reading the model, because the grid writes the values a moment later. It ends by reverting the three records, so nothing is saved.

getCurrentCell, setCurrentCell, gotoCell, and getColumnForCell

The current cell is the one that has, or last had, the focus. gotoCell makes the cell of a record and column current, setCurrentCell does the same for a cell element, and getColumnForCell returns the column definition of a cell. The grid fires currentcellchange.

Syntax:

grid.getCurrentCell() → jQuery
grid.setCurrentCell(pCell$, [pFocus])
grid.gotoCell(pRecordId, [pColumn])
grid.getColumnForCell(pCell$) → object

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
grid$.on("gridcurrentcellchange", () => console.log("currentcellchange"));
grid$.grid("gotoCell", "5", "CITY");
const cell$ = grid$.grid("getCurrentCell");
console.log("current cell:", cell$.text(), "- column:", grid$.grid("getColumnForCell", cell$).property);
grid$.grid("setCurrentCell", cell$.next(), true);
console.log("current cell:", grid$.grid("getCurrentCell").text());
await new Promise((resolve) => setTimeout(resolve, 200));

Output:

current cell: San Francisco - column: CITY
current cell: California
currentcellchange
currentcellchange

Both currentcellchange events arrived after the code finished, which is typical of the grid's events: they follow a moment after the call.

setEditMode and inEditMode

Switch between navigation mode and edit mode, where cells show their column items. The grid fires modechange.

Syntax:

grid.setEditMode(pEditMode, [pSelect])
grid.inEditMode() → boolean

getActiveRecord, getActiveRecordId, getActiveCellFromColumnItem, setActiveRecordValue, and finishEditing

In edit mode one row is active: its cells are edited through the column items, which dynamic actions and your code can read and set like page items. getActiveRecord and getActiveRecordId return that row's record and ID, and getActiveCellFromColumnItem returns the cell of a column item. A column item value set without a change event does not reach the model until setActiveRecordValue writes it. finishEditing returns a promise that resolves once the model has every edit, so call it before reading the model while editing.

Syntax:

grid.getActiveRecord() → Record
grid.getActiveRecordId() → string
grid.getActiveCellFromColumnItem(pItem) → jQuery | null
grid.setActiveRecordValue(pColumn)
grid.finishEditing() → Promise

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
grid$.on("gridmodechange", (event, data) => console.log("modechange:", data.editMode));
grid$.grid("gotoCell", "3", "CITY");
grid$.grid("setEditMode", true);
console.log("in edit mode:", grid$.grid("inEditMode"), "- active record:", grid$.grid("getActiveRecordId"));
const cityItem = grid$.grid("getColumns").find((c) => c.property === "CITY").elementId;
console.log("column item:", cityItem, "- cell:", grid$.grid("getActiveCellFromColumnItem", apex.item(cityItem).node).attr("class"));
apex.item(cityItem).setValue("Seattle (Capitol Hill)", null, true);   // no change event...
grid$.grid("setActiveRecordValue", "CITY");               // ...so write it to the model
await grid$.grid("finishEditing");
const model = grid$.grid("getModel");
console.log("model CITY:", model.getRecordValue("3", "CITY"));
model.revertRecords([model.getRecord("3")]);
grid$.grid("setEditMode", false);

Output:

modechange: true
in edit mode: true - active record: 3
column item: C77318059258661949 - cell: a-GV-cell u-tS is-active is-focused
model CITY: Seattle (Capitol Hill)
modechange: false

This is the piece that trips up most people who set grid cells from a dynamic action. Setting the column item with change suppressed, as the example does, leaves the model unchanged until setActiveRecordValue runs. The column item's name is generated, C77318059258661949 here, so read it from the column's elementId rather than hard-coding it.

lockActive and unlockActive

Keep the active row from changing while asynchronous work on it is still running, such as a server call that sets column items. The grid waits for unlockActive before the user moves to another row. The Set Value dynamic action with a server call does this for you. Every lock needs its own unlock.

Syntax:

grid.lockActive()
grid.unlockActive()

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
grid$.grid("gotoCell", "4", "CITY");
grid$.grid("setEditMode", true);
grid$.grid("lockActive");                                  // e.g. while a Set Value action calls the server
const done = new Promise((resolve) => setTimeout(() => {
    apex.item(grid$.grid("getColumns").find((c) => c.property === "STATE").elementId).setValue("Oregon");
    grid$.grid("unlockActive");
    resolve();
}, 500));
grid$.grid("gotoCell", "6", "CITY");                        // the user moves on: waits for the unlock
await done;
await grid$.grid("finishEditing");
console.log("record 4 STATE:", grid$.grid("getModel").getRecordValue("4", "STATE"), "- active:", grid$.grid("getActiveRecordId"));

Output:

record 4 STATE: Oregon - active: 6

The user moved to record 6 while record 4 was still locked, yet the late value, Oregon, still landed on record 4. Without the lock it could have been written to the wrong row.

Paging

The grid pages like the cards region, with scroll paging by default. The pagechange event reports each set of rows rendered.

Example:

const grid$ = apex.region("stores").call("getViews", "grid").view$;   // the grid widget of the Stores grid
grid$.on("gridpagechange", (event, data) => console.log("pagechange: offset", data.offset, "count", data.count));
console.log("page info:", grid$.grid("getPageInfo"));
grid$.grid("option", { pagination: { scroll: false, showRange: true }, rowsPerPage: 5 });
grid$.grid("refresh");
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("nextPage:", grid$.grid("nextPage"));
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("gotoPage(2):", grid$.grid("gotoPage", 2));
await new Promise((resolve) => setTimeout(resolve, 300));
const info = grid$.grid("getPageInfo");
console.log("first:", info.firstOffset, "last:", info.lastOffset, "total:", info.total);

Output:

page info: {
  "rowHeight": 32,
  "recordsPerRow": 1,
  "firstOffset": 1,
  "lastOffset": 13,
  "pageSize": 24,
  "pageOffset": 0,
  "total": 13,
  "scrollOffset": 0,
  "viewOffset": 0
}
pagechange: offset 0 count 13
pagechange: offset 0 count 5
pagechange: offset 0 count 5
pagechange: offset 5 count 5
nextPage: true
pagechange: offset 10 count 3
gotoPage(2): true
first: 11 last: 13 total: 13

Grid Events

EventFires whenData
selectionchangeThe selection changes.
currentcellchangeThe current cell changes.
modechangeEdit mode changes.editMode
pagechangeNew records are displayed.offset, count
activatecellA cell is activated with Enter or a double click, in a non-editable grid.cell$, row$
activatecolumnheaderA heading is activated; the interactive grid opens its column menu.header$, column
cancelcolumnheaderThat menu should close.
sortchangeThe user asks for a sort, which a handler must perform and then refresh.header$, column, direction, action
columnreorderThe user moved a column.header$, column, newPosition, oldPosition
columnresizeThe user resized a column.header$, column, width

The examples above already listen to selectionchange, currentcellchange, modechange, and pagechange.

The Table Model View

The tableModelView renders each record with a template, and wraps the whole list in markup before and after it. The cards region is a tableModelView, and you can create your own on any model:

OptionDescription
recordTemplateThe markup for one record, with substitutions for the model's columns and for &APEX$ROW_ID., &APEX$ROW_INDEX., &APEX$ROW_URL., &APEX$ROW_STATE_CLASSES., and &APEX$VALIDATION_MESSAGE., plus the placeholders #APEX$ROW_IDENTIFICATION# (the data-id and data-rownum attributes) and #APEX$SELECTOR# (a selection checkbox or radio button).
beforeTemplate, afterTemplateThe markup that opens and closes the list, such as an opening and closing ul tag.
headerTemplate, syncHeaderHScrollA header above the list, and horizontal scroll synchronization.
labelColumn, iconClassColumn, imageURLColumn, imageAttributes, linkTarget, linkTargetColumn, linkAttributes, accLabelColumnSimple rendering without a template: a label, an icon or image, and a link.
itemNavigationModenone, focus (a current item), or select (selection). itemSelector finds the item elements.
multiple, selectAll, selectAllId, selectionStateItemSelection options, as for the grid.
controlBreakTemplate, controlBreakBeforeTemplate, controlBreakAfterTemplate, controlBreakSelector, aggregateTemplateControl breaks and aggregates.
collectionClassesClasses of the list element.
allowCopy, clipboardValueCopying items to the clipboard.

Its methods are those of the cards region: getSelectedValues, setSelectedValues, getSelection, setSelection, getSelectedRecords, setSelectedRecords, selectAll, getCurrentItem, getCurrentItemValue, setCurrentItem, setCurrentItemValue, getRecords, refresh, resize, and focus. Its events are selectionchange, currentitemchange, and pagechange. With itemNavigationMode set to "select", the selection methods that return null on a native cards region work.

This example builds a selectable list on the orbitOrders model created in the guide to reading and changing grid data with apex.model; run that guide's apex.model.create example first.

Example:

// A tableModelView of your own: templates render the records of a model.
apex.jQuery(`<div id="orbit_order_list"></div>`).insertBefore("#catalog").tableModelView({
    modelName: "orbitOrders",
    beforeTemplate: `<ul class="orbit-orders">`,
    recordTemplate: `<li #APEX$ROW_IDENTIFICATION# class="orbit-order">#APEX$SELECTOR# <b>&ORDER_NUMBER.</b> &CUSTOMER. (&STATUS.)</li>`,
    afterTemplate: `</ul>`,
    itemNavigationMode: "select",
    itemSelector: ".orbit-order",
    multiple: true,
    footer: true,
    entityTitleSingular: "order", entityTitlePlural: "orders",
    pagination: { scroll: false, showRange: false }
});
const view$ = apex.jQuery("#orbit_order_list");
view$.on("tablemodelviewselectionchange", () => console.log("selectionchange:", view$.tableModelView("getSelectedValues")));
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("items:", view$.find(".orbit-order").length);
console.log("setSelectedValues:", view$.tableModelView("setSelectedValues", ["2279", "2277"]));
await new Promise((resolve) => setTimeout(resolve, 200));
view$.tableModelView("setCurrentItemValue", "2278", true);
console.log("current:", view$.tableModelView("getCurrentItemValue"));

Output:

items: 5
setSelectedValues: 2
selectionchange: ["2279", "2277"]
current: 2278
A custom list of five orders with checkboxes, two selected, and a footer reading 2 orders selected
A tableModelView of your own: templates render the orders, #APEX$SELECTOR# adds the checkboxes, and the footer uses the entity titles.

The footer says "2 orders selected" because of entityTitleSingular and entityTitlePlural. The older useIconList and getIconList options, which rendered through the iconList widget, are deprecated.

The Record View

The recordView shows one record of a model as a form of fields, with a toolbar for moving between records. The interactive grid uses it as its single row view, opened with the single-row-view action; the grid view's singleRowView$ property is the widget element.

OptionDescription
modelName, recordOffsetThe model, and the index of the record to show.
fields, fieldGroupsThe field definitions, like grid columns, and their group headings.
editable, alwaysEditEditing, and staying in edit mode permanently.
skipDeletedRecords, autoAddRecordSkip deleted records, and add one when the model is empty.
showExcludeHiddenFields, showExcludeNullValuesOffer to hide hidden fields and empty values.
labelAlignment, formCssClassesLabel alignment and form classes.
toolbar, actionsContextThe toolbar, or none, and an actions context to share.
noDataMessage, noDataIcon, showNullAs, idPrefix, hasSize, progressOptions, applyTemplateOptionsAs for the other views.

getRecord, getFields, and fieldElement

Return the record shown, the field definitions, and the element of a field with its label.

Syntax:

recordView.getRecord() → Record
recordView.getFields() → object[]
recordView.fieldElement(pFieldName) → jQuery

Example:

const stores = apex.region("stores");
stores.call("setSelectedRecords", ["7"]);
stores.call("getActions").invoke("single-row-view");      // the Single Row View of the grid
const grid = stores.call("getViews", "grid");
const rv$ = grid.singleRowView$;                            // a recordView widget
console.log("singleRowMode:", grid.singleRowMode);
const model = rv$.recordView("getModel");
console.log("record:", model.getRecordId(rv$.recordView("getRecord")), model.getValue(rv$.recordView("getRecord"), "STORE_NAME"));
console.log("fields:", rv$.recordView("getFields").slice(0, 4).map((f) => f.property));
console.log("CITY field:", rv$.recordView("fieldElement", "CITY").find("label").text());

Output:

singleRowMode: true
record: 7 Orbit Chicago River North
fields: ["APEX$ROW_ACTION", "ID", "STORE_NAME", "CITY"]
CITY field: City
The single row view of an Oracle APEX interactive grid showing the Chicago store as a form, row 3 of 13
The single row view shows one record as a form, with Previous and Next buttons in its own toolbar.

gotoField, setEditMode, inEditMode, getActions, and getToolbar

gotoField shows a record and focuses one of its fields. setEditMode and inEditMode work as in the grid. getActions returns the view's actions, next-record, previous-record, insert-record, delete-record, duplicate-record, refresh-record, revert-record, exclude-null-values, and exclude-hidden, and getToolbar returns its toolbar. The recordView also has getModel, getActiveRecord, getActiveRecordId, setActiveRecordValue, finishEditing, lockActive, unlockActive, refresh, refreshFields, resize, and focus, which work as for the grid. Its events are recordchange, when another record is shown, and modechange.

Syntax:

recordView.gotoField(pRecordId, [pField])
recordView.setEditMode(pEditMode)
recordView.inEditMode() → boolean
recordView.getActions() → actions
recordView.getToolbar() → jQuery

Example:

const stores = apex.region("stores");
stores.call("setSelectedRecords", ["7"]);
stores.call("getActions").invoke("single-row-view");
const rv$ = stores.call("getViews", "grid").singleRowView$;
rv$.on("recordviewrecordchange", () => console.log("recordchange:", rv$.recordView("getModel").getRecordId(rv$.recordView("getRecord"))));
rv$.on("recordviewmodechange", (event, data) => console.log("modechange:", data.editMode));
rv$.recordView("getActions").invoke("next-record");       // the Next button of its toolbar
await new Promise((resolve) => setTimeout(resolve, 300));
rv$.recordView("gotoField", "9", "CITY");
await new Promise((resolve) => setTimeout(resolve, 300));
rv$.recordView("setEditMode", true);
console.log("in edit mode:", rv$.recordView("inEditMode"), "- active:", rv$.recordView("getActiveRecordId"));
console.log("toolbar buttons:", rv$.recordView("getToolbar").find("button").map((i, b) => b.title || b.textContent.trim()).get().join(", "));
rv$.recordView("setEditMode", false);

Output:

recordchange: 12
recordchange: 12
recordchange: 9
modechange: true
in edit mode: true - active: 9
toolbar buttons: Grid View, Change, Settings, Previous, ⌥ Page Up, Next, ⌥ Page Down
modechange: false

Recipes

Color Rows by Their Values

The interactive grid's Highlight feature lets users color rows themselves. To color rows by a rule of your own, such as stores that opened in the last five years, give the grid a highlight rule with the highlights option, and set the rule's ID as the highlight metadata of the matching records. The grid adds the rule's class, hlr_new-store, to their rows.

Put the code in the page's Execute when Page Loads attribute.

Example:

// === Page › Execute when Page Loads ===
// Rows of stores that opened in the last five years get a light amber background.
const grid = apex.region("stores").call("getViews", "grid");
grid.view$.grid("option", "highlights", {
    "new-store": { row: true, background: "#fdf0cf", seq: 1 }       // the rule, as CSS
});
function markNewStores() {
    const since = apex.date.subtract(new Date(), 5, apex.date.UNIT.YEAR);
    grid.model.forEach((record, index, id) => {
        const opened = apex.date.parse(grid.model.getValue(record, "OPENED_ON"), "DD-MON-YYYY");
        const isNew = apex.date.isAfter(opened, since);
        grid.model.getRecordMetadata(id).highlight = isNew ? "new-store" : null;
        grid.model.metadataChanged(id);
    });
}
markNewStores();
grid.model.subscribe({
    onChange: (type) => { if (["refresh", "addData"].includes(type)) markNewStores(); }
});
// === Try it: which rows have the class of the rule ===
apex.region("stores").call("setSelectedRecords", [], false, true);   // (no selection in the picture)
console.log($("#stores tr.hlr_new-store td:nth-child(3)").map((i, cell) => cell.textContent).get());

Output:

[
  "Orbit Austin South Congress",
  "Orbit Minneapolis North Loop",
  "Orbit Asheville Downtown"
]
An interactive grid with three store rows highlighted in light amber
The three stores opened in the last five years, highlighted by a rule set from code.

The grid writes the rule's colors as a CSS rule itself, which works even under a strict Content Security Policy. A highlight the user defines from the Actions menu uses the same metadata and replaces yours on the rows it matches. For other ways to color rows, see highlighting values less than zero with jQuery.

Show Fewer Columns on a Phone

Seven columns do not fit on a phone, and the user ends up scrolling sideways through every row. A media query tells the page that the screen is narrow, and hideColumn leaves out the columns that matter least. The query's change event shows them again when the phone turns sideways or a window grows.

Put the code in the page's Execute when Page Loads attribute. The example ran in a window 390 pixels wide, the size of a phone.

Example:

// === Page › Execute when Page Loads ===
// On a narrow screen, the Stores grid shows only its most important columns.
const grid$ = apex.region("stores").call("getViews", "grid").view$;
const optional = ["STATE", "COUNTRY", "OPENED_ON"];
function fitColumns(narrow) {
    for (const column of optional) grid$.grid(narrow ? "hideColumn" : "showColumn", column);
}
const phone = window.matchMedia("(max-width: 640px)");
fitColumns(phone.matches);
phone.addEventListener("change", (event) => fitColumns(event.matches));   // rotating a tablet
// === Try it: open the page on a phone (390 pixels wide) ===
console.log("phone:", phone.matches, "- visible:", grid$.grid("getColumns")
    .filter((c) => !c.hidden && !c.property.startsWith("APEX$")).map((c) => c.property).join(" "));

Output:

phone: true - visible: STORE_NAME CITY FLOOR_AREA_SQFT FLAGSHIP
An interactive grid on a phone-sized screen showing only the Store, City, Floor Area, and Flagship columns
On a 390-pixel screen, the grid keeps only its four most useful columns.

Columns hidden this way are not saved in the user's report, and the user can still show them from the Actions menu. The same media query test is available as apex.theme.mq, covered in the guide to apex.page, apex.navigation, and apex.theme.

Conclusion

The grid, the tableModelView, and the recordView are the widgets that draw a model's records. The grid's options configure columns, row or cell selection, editing, and paging, and its methods hide, move, resize, and freeze columns, select rows and cell ranges, fill and copy down, move the current cell, and switch edit mode. While editing, setActiveRecordValue, finishEditing, and lockActive keep the active row and the model in step, which is the key to setting grid cells reliably from code. The tableModelView renders records through templates, and with #APEX$ROW_IDENTIFICATION# and #APEX$SELECTOR# it gives you selectable lists of your own. The recordView is the form behind the single row view, with its own actions and events.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE and software veteran with 25+ years of experience, passionate about AI and IT innovation.

guest

0 Comments
Oldest
Newest Most Voted
00