Behind every interactive grid, cards region, and tree in Oracle APEX sits a model: a JavaScript object that holds the region's records. The region's view draws the records and writes edits back to the model, and the model tracks every change and saves them all to the server in one request. Once you understand the model, you can read and change grid data without touching the grid's HTML at all.
This guide covers apex.model and the model interface with tested examples and their real output: creating and finding models, reading and changing records, permissions and calculated fields, tracking and saving changes, validity and metadata, notifications, transforming data for charts, and tree models. Two recipes at the end validate a rule across two grid columns and make cells read-only in some rows.
Quick Reference
| Task | Functions and methods |
|---|---|
| Create, find, and free models | apex.model.create, get, release, destroy, list |
| Work across all models | apex.model.anyChanges, anyErrors, addChangesToSaveRequest, save, multipleFetch |
| Read records | getRecord, getRecordId, getValue, getRecordValue, recordAt, indexOf, forEach, forEachInPage |
| Count records | getTotalRecords, getServerTotalRecords, getDataOverflow |
| Change records | setValue, setRecordValue, insertNewRecord, copyRecords, moveRecords, deleteRecords |
| Check permissions | allowEdit, allowDelete, allowAdd, allowDrag, check, dragOperations |
| Track and save changes | isChanged, getChanges, canRevertRecord, revertRecords, clearChanges, save |
| Validity and metadata | getRecordMetadata, setValidity, hasErrors, getErrors, setDisabledState, setHiddenState, isDisabled, metadataChanged |
| Selection kept in the model | setSelectionState, getSelectedRecords, getSelectedCount, getSelectionState, clearSelection |
| React to changes | subscribe, unSubscribe |
| Load and reshape data | setData, clearData, fetch, fetchAll, fetchRecords, transform, updateVisibility |
| Options and fields | getOption, setOption, modelId, getFieldKey, getFieldMetadata, isIdentityField |
| Tree models | root, child, childCount, hasChildren, parent, walkTree |
Key Concepts
A model has a shape. table, a list of records, is by far the most common; tree holds nodes with children; and record holds a single record. A record is an object whose properties are its fields, or an array for models created with recordIsArray. Region models use arrays, which is why the examples read values with getValue rather than record.NAME.
Each record has an ID, the value of its identity field, and metadata recording whether it was inserted, updated, or deleted, any errors, and more. A region's getModel method, or its grid view's model property, hands you its model, as shown in the guide to selecting rows and paging through reports and cards.
How to Run These Examples
The examples ran in Oracle APEX 26.1. Most use a local model of five orders that the first example creates, and each of those examples started from a freshly created copy of it. To follow along in the console, run the apex.model.create example first, and run it again before each example that uses the orbitOrders model, since several of them change the data. The examples marked with the stores page use a real interactive grid with the static ID stores.
Open a page, press F12, paste the code into the Console tab, and run it. Several examples use await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.
Managing Models with apex.model
apex.model.create
Creates a model with an ID, options, and optionally its initial data, and returns it. Creating a model again with the same ID replaces the old one. Regions create their own models; you create models for your own data or plug-ins.
apex.model.create(pModelId, pOptions, [pData], [pTotal], [pMoreData], [pDataOverflow]) → model
A model ID is a name, or for a detail model an array of [name, instance] where the instance is the ID of its master record. The main options:
| Option | Description |
|---|---|
| shape | table (the default), tree, or record. |
| fields | Required. An object of field names and their metadata, described below. |
| identityField | The field, or array of fields, that identifies a record. Required for editable table and tree models. |
| editable | Whether records can be changed. The default is false. |
| recordIsArray | Records are arrays, and each field's metadata has an index. |
| paginationType | none (all data is present), one (one page at a time), or progressive (pages are added). |
| pageSize | The number of records per fetch. The default is 100. |
| hasTotalRecords | The server reports the total record count. |
| regionId, ajaxIdentifier | The region and plug-in Ajax identifier used to fetch and save. A model without a region is local. |
| pageItemsToSubmit, regionData, fetchData, saveData, requestOptions | Page items and extra data sent with requests. |
| trackChanges, onlyMarkForDelete | Track changes (default true), and keep deleted records, marked, until saved (default true). |
| check | A function for extra permission checks. |
| genIdPrefix | The prefix of IDs for new records. The default is t. |
| parentModel, parentRecordId | The master model and record of a detail model. |
| childrenField, parentIdentityField | For trees: the field holding a node's children, and the field holding its parent's ID. |
| metaField | The field that holds record metadata sent by the server. |
| visibilityFilter, visibilityFilterContext | A function that decides which records are visible. |
| callServer | A function used instead of apex.server.plugin for requests. |
Each field takes metadata of its own:
| Property | Description |
|---|---|
| dataType | The field's data type. |
| defaultValue | The value given to new records. |
| readonly | The field cannot be changed. |
| volatile | The server generates the field, so it is not sent back. |
| virtual | A field with no data, for views. |
| noCopy | Not copied when a record is copied. |
| calcValue, dependsOn | A function that calculates the field from the fields listed in dependsOn. |
| aggregates | Aggregate functions such as COUNT, SUM, AVG, MIN, MAX, and MEDIAN. |
| controlBreakIndex | The field's position in a control break. |
| parentField | For detail models: the master field whose value new records receive. |
| index | The field's array index, for recordIsArray models. |
apex.model.create("orbitOrders", {
shape: "table",
editable: true,
identityField: "ORDER_ID",
paginationType: "none",
fields: {
ORDER_ID: { dataType: "NUMBER", readonly: true },
ORDER_NUMBER: {},
CUSTOMER: {},
STATUS: { defaultValue: "NEW" },
TOTAL: { dataType: "NUMBER" }
}
}, [
{ ORDER_ID: "2282", ORDER_NUMBER: "ORD-12283", CUSTOMER: "Wildflower Travel Co.", STATUS: "SHIPPED", TOTAL: "463.21" },
{ ORDER_ID: "2280", ORDER_NUMBER: "ORD-12280", CUSTOMER: "Linda Kim", STATUS: "CANCELLED", TOTAL: "69.98" },
{ ORDER_ID: "2279", ORDER_NUMBER: "ORD-12279", CUSTOMER: "Matthew Jones", STATUS: "APPROVED", TOTAL: "1329.95" },
{ ORDER_ID: "2278", ORDER_NUMBER: "ORD-12278", CUSTOMER: "Linda Lopez", STATUS: "APPROVED", TOTAL: "1030.69" },
{ ORDER_ID: "2277", ORDER_NUMBER: "ORD-12277", CUSTOMER: "Prairie Travel Co.", STATUS: "APPROVED", TOTAL: "11495.93" }
]);
const orders = apex.model.get("orbitOrders");
console.log("model:", orders.modelId(), "- records:", orders.getTotalRecords(), "- shape:", orders.getOption("shape"));
console.log("list:", apex.model.list(true).filter((id) => /orbit/.test(id)));Output:
model: orbitOrders - records: 5 - shape: table list: ["orbitOrders"]
apex.model.get, release, and destroy
Models are reference counted. get returns a model and adds a reference, create adds one too, and release gives one back; a model with no references can be destroyed to free memory. destroy removes a model regardless of its references, so use it only when you are sure nothing else uses it. Views such as the interactive grid get and release their models; in your own code, release whatever you get.
apex.model.get(pModelId) → model apex.model.release(pModelId) apex.model.destroy(pModelId)
const a = apex.model.get("orbitOrders"); // reference count 2 (create + get)
const b = apex.model.get("orbitOrders"); // 3
console.log("same model:", a === b);
apex.model.release("orbitOrders");
apex.model.release("orbitOrders");
console.log("after 2 releases:", apex.model.list(true).includes("orbitOrders"));
apex.model.destroy("orbitOrders"); // regardless of the count
console.log("after destroy:", apex.model.list(true).includes("orbitOrders"), apex.model.get("orbitOrders") ?? null);
console.log("cached detail models:", apex.model.getMaxCachedModels());Output:
same model: true after 2 releases: true after destroy: false null cached detail models: 10
apex.model.list
Returns the IDs of the region models, and local models too when pIncludeLocal is true. pModelId limits the list to one model name and its detail instances, and pIncludeRelated adds their detail models.
apex.model.list([pIncludeLocal], [pModelId], [pIncludeRelated]) → ModelId[]
console.log("region models:", apex.model.list());
console.log("with local models:", apex.model.list(true));Output:
region models: ["stores_grid"] with local models: ["stores_grid", "orbitOrders"]
The interactive grid with the static ID stores has the model stores_grid, its static ID with _grid added. That naming rule is how you reach a grid's model directly with apex.model.get.
apex.model.anyChanges, anyErrors, and addChangesToSaveRequest
anyChanges and anyErrors tell you whether any model, or the models pModelId selects, has changes or errors; apex.page.isChanged uses anyChanges to decide whether to warn about unsaved changes. addChangesToSaveRequest adds the changes to a request object without sending it, for your own call to the server.
apex.model.anyChanges([pIncludeLocal], [pModelId], [pIncludeRelated]) → boolean apex.model.anyErrors([pIncludeLocal], [pModelId], [pIncludeRelated]) → boolean apex.model.addChangesToSaveRequest(pRequestData, [pModelId], [pIncludeRelated])
const model = apex.model.get("stores_grid"); // the interactive grid's model
console.log("changes:", apex.model.anyChanges(), "- errors:", apex.model.anyErrors());
model.setValue(model.recordAt(0), "STORE_NAME", "Orbit Denver Union Station (renovated)");
console.log("changes:", apex.model.anyChanges(), "- for this model:", apex.model.anyChanges(false, "stores_grid"));
const request = {};
apex.model.addChangesToSaveRequest(request);
const region = request.regions[0];
console.log("region properties:", Object.keys(region));
const row = region.saveData.models[0].values[0]; // the grid's records are arrays
console.log("changed row:", row.slice(0, 3), "- operation:", row.at(-1).op);Output:
changes: false - errors: false changes: true - for this model: true region properties: [ "reportId", "view", "ajaxColumns", "allowedOperations", "id", "ajaxIdentifier", "saveData" ] changed row: ["1", "Orbit Denver Union Station (renovated)", "Denver"] - operation: u
The request carries each changed row with its operation, u for update, i for insert, or d for delete, plus checksums that the server verifies so a tampered row is rejected.
apex.model.save and multipleFetch
save saves every model with changes, or those pModelId selects, in one request and returns its promise, or null when nothing changed. multipleFetch fetches the data of several models in one request. getMaxCachedModels and setMaxCachedModels control how many unused detail models are kept in memory, 10 by default.
apex.model.save([pRequestData], [pOptions], [pModelId], [pIncludeRelated], [pCallServer]) → Promise | null apex.model.multipleFetch([pRequestData], [pOptions], pModelIds, [pCallServer]) → Promise | null apex.model.getMaxCachedModels() → number apex.model.setMaxCachedModels(n)
const ids = apex.model.list();
ids.forEach((id) => apex.model.get(id).clearData(false));
await apex.model.multipleFetch(null, null, ids); // one request for all of them
console.log(ids.map((id) => `${id}: ${apex.model.get(id).getTotalRecords(true)} records`));
apex.model.setMaxCachedModels(20);
console.log("max cached:", apex.model.getMaxCachedModels());Output:
["stores_grid: 13 records"] max cached: 20
The model.save example further down uses apex.model.save as well.
Reading Records
getRecord, getRecordId, getValue, getRecordValue, recordAt, and indexOf
getRecord returns the record with a given ID, provided it has been fetched into the model, and getRecordId returns a record's ID. getValue reads a field of a record, and getRecordValue reads a field of the record with a given ID. recordAt returns the record at an index, and indexOf a record's index.
model.getRecord([pRecordId]) → Record | null model.getRecordId(pRecord) → string model.getValue([pRecord], pFieldName) → * model.getRecordValue(pRecordId, pFieldName) → * model.recordAt(index) → Record model.indexOf(pRecord) → number
setValue and setRecordValue
Change a field, by record or by record ID. They throw an error when the record cannot be edited or the field is read-only. The change is tracked and triggers the set notification. Pass strings: the server expects them.
model.setValue([pRecord], pFieldName, pValue) → string | null model.setRecordValue(pRecordId, pFieldName, pValue)
const orders = apex.model.get("orbitOrders");
const record = orders.getRecord("2279");
console.log(orders.getRecordId(record), orders.getValue(record, "CUSTOMER"), orders.getValue(record, "TOTAL"));
console.log("index:", orders.indexOf(record), "- recordAt(0):", orders.getValue(orders.recordAt(0), "ORDER_NUMBER"));
console.log("getRecordValue:", orders.getRecordValue("2282", "STATUS"));
orders.setValue(record, "STATUS", "SHIPPED");
orders.setRecordValue("2278", "STATUS", "SHIPPED");
console.log("after set:", orders.getValue(record, "STATUS"), orders.getRecordValue("2278", "STATUS"));
try { orders.setValue(record, "ORDER_ID", "9999"); } catch (e) { console.log("readonly field:", e.message); }Output:
2279 Matthew Jones 1329.95 index: 2 - recordAt(0): ORD-12283 getRecordValue: SHIPPED after set: SHIPPED SHIPPED readonly field: Set value not allowed for field
For a model with the record shape, leave the record out, as in model.getValue("NAME").
forEach and forEachInPage
forEach calls a function for every record already in the model, without fetching more, passing the record, its index, and its ID. forEachInPage does the same for a range of records and fetches them from the server if they are not there yet.
model.forEach(pCallback, [pThisArg]) model.forEachInPage(pOffset, pCount, pCallback, [pThisArg])
getTotalRecords, getServerTotalRecords, and getDataOverflow
getTotalRecords returns the number of records, or -1 when it is unknown; with pCurrentTotal, it returns how many are in the model right now. getServerTotalRecords returns the count on the server, adjusted for inserts and deletes, or -1. getDataOverflow tells whether the server has more records than it is willing to send.
model.getTotalRecords([pCurrentTotal]) → number model.getServerTotalRecords() → number model.getDataOverflow() → boolean
const orders = apex.model.get("orbitOrders");
let sum = 0;
orders.forEach((record, index, id) => { sum += Number(orders.getValue(record, "TOTAL")); });
console.log("records:", orders.getTotalRecords(), "- total:", sum.toFixed(2));
orders.forEachInPage(1, 2, (record, index, id) => console.log("page:", index, id, orders.getValue(record, "CUSTOMER")));
console.log("server total:", orders.getServerTotalRecords(), "- overflow:", orders.getDataOverflow());Output:
records: 5 - total: 14389.76 page: 1 2280 Linda Kim page: 2 2279 Matthew Jones server total: -1 - overflow: false
The local model has no server, so its server total is unknown. Looping over a grid's records like this is covered with more examples in looping through interactive grid records.
Changing Records
insertNewRecord, copyRecords, moveRecords, and deleteRecords
insertNewRecord inserts a record, either a new one with the default values or one you pass, after a given record or at the beginning, and returns its ID; new IDs are the genIdPrefix followed by a number. copyRecords inserts copies and moveRecords moves records, after a given record or, in trees, under a parent. deleteRecords deletes records and returns how many; with onlyMarkForDelete, they stay in the model, marked as deleted, until saved. Each one checks the permissions described next.
model.insertNewRecord([pParentRecord], [pAfterRecord], [pNewRecord]) → string model.copyRecords(pRecords, [pParentRecord], [pAfterRecord]) → string[] model.moveRecords(pRecords, [pParentRecord], [pAfterRecord]) → string[] model.deleteRecords(pRecords) → number
const orders = apex.model.get("orbitOrders");
const id = orders.insertNewRecord(null, orders.getRecord("2282")); // after the first record
console.log("new id:", id, "- STATUS:", orders.getRecordValue(id, "STATUS"), "- at index", orders.indexOf(orders.getRecord(id)));
orders.setRecordValue(id, "CUSTOMER", "Summit Outfitters");
const copies = orders.copyRecords([orders.getRecord("2277")], null, null); // at the beginning
console.log("copy:", copies, orders.getRecordValue(copies[0], "ORDER_NUMBER"));
orders.moveRecords([orders.getRecord("2280")], null, orders.recordAt(orders.getTotalRecords() - 1));
console.log("order:", (() => { const ids = []; orders.forEach((r, i, rid) => ids.push(rid)); return ids.join(" "); })());
console.log("deleted:", orders.deleteRecords([orders.getRecord("2279")]), "- still there:", !!orders.getRecord("2279"),
"- deleted flag:", orders.getRecordMetadata("2279").deleted);Output:
new id: t1000 - STATUS: NEW - at index 1 copy: ["t1001"] ORD-12277 order: t1001 2282 t1000 2279 2278 2277 2280 deleted: 1 - still there: true - deleted flag: true
The new record got the default status NEW and an ID of t1000, and the deleted record is still in the model, flagged, until the next save. Inserting rows into a grid from code is covered step by step in inserting records into an interactive grid using JavaScript.
allowEdit, allowDelete, allowAdd, allowDrag, check, and dragOperations
Tell whether a record can be edited, deleted, or dragged, and whether records can be added. The model must be editable to begin with; the record's allowedOperations metadata from the server and the check option can restrict it further. check is the general form, taking an operation name, and dragOperations returns which drag operations, move or copy, are allowed.
model.allowEdit(pRecord) → boolean model.allowDelete(pRecord) → boolean model.allowAdd([pParentRecord], [pAddAction], [pRecordsToAdd]) → boolean model.allowDrag(pRecord) → boolean model.check(pOperation, [pRecord], [pAddAction], [pRecordsToAdd]) → boolean model.dragOperations(pRecords) → object
apex.model.create("orbitGuarded", {
editable: true, identityField: "ORDER_ID", paginationType: "none",
fields: { ORDER_ID: {}, STATUS: {} },
// shipped and cancelled orders cannot be edited or deleted
check: (result, operation, record) => {
if (record && ["canEdit", "canDelete"].includes(operation)) {
return result && !["SHIPPED", "CANCELLED"].includes(record.STATUS);
}
return result;
}
}, [{ ORDER_ID: "2282", STATUS: "SHIPPED" }, { ORDER_ID: "2279", STATUS: "APPROVED" }]);
const m = apex.model.get("orbitGuarded");
for (const id of ["2282", "2279"]) {
const r = m.getRecord(id);
console.log(id, "edit:", m.allowEdit(r), "- delete:", m.allowDelete(r), "- drag:", m.allowDrag(r));
}
console.log("add:", m.allowAdd(), "- check canAdd:", m.check("canAdd"));
console.log("drag operations:", m.dragOperations([m.getRecord("2279")]));Output:
2282 edit: false - delete: false - drag: true
2279 edit: true - delete: true - drag: true
add: true - check canAdd: true
drag operations: {"normal":"move", "ctrl":"copy"}The check function here blocks edits and deletes for shipped and cancelled orders, so the shipped order 2282 reports false for both. On the server side, the same effect for an interactive grid comes from the Allowed Row Operations Column, as shown in making interactive grid rows editable on a condition.
A Calculated Field
A field with calcValue and dependsOn is recalculated whenever a field it depends on changes, such as a line total from quantity and unit price:
apex.model.create("orbitLines", {
editable: true, identityField: "LINE_ID", paginationType: "none",
fields: {
LINE_ID: {}, PRODUCT: {}, QUANTITY: {}, UNIT_PRICE: {},
LINE_TOTAL: {
dependsOn: ["QUANTITY", "UNIT_PRICE"],
calcValue: (argsArray, model, record) =>
(Number(model.getValue(record, "QUANTITY")) * Number(model.getValue(record, "UNIT_PRICE"))).toFixed(2)
}
}
}, [{ LINE_ID: "1", PRODUCT: "Trailblazer 2-Person Tent", QUANTITY: "1", UNIT_PRICE: "349.99", LINE_TOTAL: "349.99" }]);
const lines = apex.model.get("orbitLines");
lines.setRecordValue("1", "QUANTITY", "3");
console.log("LINE_TOTAL:", lines.getRecordValue("1", "LINE_TOTAL"));Output:
LINE_TOTAL: 1049.97
Tracking and Saving Changes
isChanged, getChanges, canRevertRecord, revertRecords, and clearChanges
isChanged tells whether the model has changes, and getChanges returns the metadata of every changed record, flagged as inserted, updated (with the original record), or deleted. revertRecords undoes the changes of records where canRevertRecord allows it. clearChanges forgets all changes without undoing them, so records keep their current values; this is what a successful save does.
model.isChanged() → boolean model.getChanges() → RecordMetadata[] model.canRevertRecord(pRecord) → boolean model.revertRecords(pRecords) → number model.clearChanges()
const orders = apex.model.get("orbitOrders");
console.log("changed:", orders.isChanged());
orders.setRecordValue("2279", "STATUS", "SHIPPED");
orders.deleteRecords([orders.getRecord("2280")]);
orders.insertNewRecord();
for (const meta of orders.getChanges()) {
console.log(orders.getRecordId(meta.record), { inserted: !!meta.inserted, updated: !!meta.updated, deleted: !!meta.deleted },
meta.original ? "was " + orders.getValue(meta.original, "STATUS") : "");
}
console.log("can revert 2279:", orders.canRevertRecord(orders.getRecord("2279")));
console.log("reverted:", orders.revertRecords([orders.getRecord("2279"), orders.getRecord("2280")]),
"- 2279 is", orders.getRecordValue("2279", "STATUS"));
orders.clearChanges();
console.log("after clearChanges:", orders.isChanged(), "- records:", orders.getTotalRecords());Output:
changed: false
2279 {"inserted":false, "updated":true, "deleted":false} was APPROVED
2280 {"inserted":false, "updated":false, "deleted":true}
t1000 {"inserted":true, "updated":false, "deleted":false}
can revert 2279: true
reverted: 2 - 2279 is APPROVED
after clearChanges: false - records: 6After clearChanges the inserted record stayed and the reverted delete was undone, leaving six records. clearChanges on its own does not write anything to the database, so never use it as a substitute for save.
model.save
Saves the model's changes to the server through its region's Ajax identifier and returns a promise. On success, the changes are cleared and the records are updated with whatever the server returned. Local models cannot save.
model.save([pCallback]) → Promise
const model = apex.model.get("stores_grid"); // the interactive grid's model
const id = model.getRecordId(model.recordAt(0));
const original = model.getRecordValue(id, "STORE_NAME");
model.setRecordValue(id, "STORE_NAME", original + " (renovated)");
await model.save(); // the grid's DML process saves the row
console.log("saved:", !model.isChanged(), "-", model.getRecordValue(id, "STORE_NAME"));
model.setRecordValue(id, "STORE_NAME", original); // put it back
await apex.model.save(); // saves all changed models
console.log("restored:", model.getRecordValue(id, "STORE_NAME"), "- changes:", apex.model.anyChanges());Output:
saved: true - Orbit Denver Union Station (renovated) restored: Orbit Denver Union Station - changes: false
This does exactly what the grid's Save button does. On the server, the grid's Interactive Grid - Automatic Row Processing (DML) page process saved the changed row. The whole grid is covered in the complete interactive grid guide.
Validity and Metadata
getRecordMetadata
Returns a record's metadata: its index and recSequence (both new in 26.1); the change flags inserted, updated, deleted, and original; error, warning, and message; disabled, hidden, highlight, and sel; allowedOperations; aggregate properties such as agg and grandTotal; and fields, the metadata of each field, with changed, error, warning, message, disabled, highlight, and url. Do not change it directly except as metadataChanged describes below.
model.getRecordMetadata([pRecordId]) → RecordMetadata
setValidity, hasErrors, and getErrors
setValidity marks a record or a field as error, warning, or valid, with a message that views display. hasErrors and getErrors report the records with errors.
model.setValidity(pValidity, pRecordId, [pFieldName], [pMessage]) model.hasErrors() → boolean model.getErrors() → RecordMetadata[]
setDisabledState, setHiddenState, isDisabled, and metadataChanged
Set a record's disabled and hidden metadata and tell whether it is disabled. Views decide what these mean: a disabled row cannot be selected, and a hidden one is not shown. After changing other metadata yourself, call metadataChanged so views update.
model.setDisabledState(pRecordId, pDisabled) model.setHiddenState(pRecordId, pHidden) model.isDisabled(pRecord, [pRecordMeta]) → boolean model.metadataChanged(pRecordId, [pFieldName], [pPropertyName])
const orders = apex.model.get("orbitOrders");
orders.setValidity("error", "2279", "TOTAL", "The total must not exceed the credit limit.");
orders.setValidity("warning", "2278", null, "Customer on hold.");
console.log("hasErrors:", orders.hasErrors(), "- errors:", orders.getErrors().map((m) => orders.getRecordId(m.record)));
const meta = orders.getRecordMetadata("2279");
console.log("record error:", !!meta.error, "- field:", meta.fields.TOTAL);
orders.setValidity("valid", "2279", "TOTAL");
console.log("after valid:", orders.hasErrors());
orders.setDisabledState("2280", true);
orders.setHiddenState("2277", true);
console.log("disabled:", orders.isDisabled(orders.getRecord("2280")), "- hidden:", orders.getRecordMetadata("2277").hidden);
const m2 = orders.getRecordMetadata("2282");
m2.highlight = "orbit-urgent"; // metadata changed by your code...
orders.metadataChanged("2282", null, "highlight"); // ...must be announced
console.log("highlight:", orders.getRecordMetadata("2282").highlight);Output:
hasErrors: true - errors: ["2279"]
record error: false - field: {"error":true, "message":"The total must not exceed the credit limit."}
after valid: false
disabled: true - hidden: true
highlight: orbit-urgentAn error set on a field lives in that field's metadata, which is why the record-level error flag is false while the TOTAL field carries the error and message. A warning does not count as an error, so only 2279 appears in getErrors.
setSelectionState, getSelectedRecords, getSelectedCount, getSelectionState, and clearSelection
Views that keep their selection in the model, such as a grid with persistSelection, use these methods. setSelectionState selects or unselects a record, with the action set, toggle, range, anchor, or all.
model.setSelectionState(pRecordId, pSelected, [pAction]) model.getSelectedRecords() → Record[] model.getSelectedCount() → number model.getSelectionState() → object model.clearSelection()
const orders = apex.model.get("orbitOrders");
orders.setSelectionState("2279", true);
orders.setSelectionState("2277", true);
console.log("count:", orders.getSelectedCount(), "- records:", orders.getSelectedRecords().map((r) => orders.getRecordId(r)));
console.log("state:", orders.getSelectionState());
orders.setSelectionState("2279", true, "toggle");
orders.clearSelection();
console.log("after clearSelection:", orders.getSelectedCount());Output:
count: 2 - records: ["2279", "2277"]
state: {"selectAll":false, "incomplete":false, "rangeAnchor":"2277"}
after clearSelection: 0To select rows the user can see, use the region's own methods instead, because they update the view too. Reading a grid's selected rows is covered in getting the selected rows of an interactive grid.
Notifications
subscribe and unSubscribe
Register an observer that the model notifies of every change, and remove it again by its view ID. Views use this to redraw, and you can use it to react to edits.
model.subscribe(pObserver) → string model.unSubscribe(pViewId)
| Observer property | Description |
|---|---|
| viewId | A unique ID, returned by subscribe. A DOM ID is a good choice. |
| onChange | function(changeType, change), called for each notification. |
| progressView, progressOptions | An element to show a spinner over while the model fetches or saves, and options for it. |
The notifications a model sends:
| Notification | Sent when |
|---|---|
| set | A field value changed. change has record, recordId, field, and oldValue. |
| insert, copy, move, delete | Records were inserted, copied, moved, or deleted. |
| revert | Changes were reverted. |
| metaChange | Metadata changed, such as validity, highlight, or disabled. |
| refresh | The data was replaced or cleared, so views fetch again. |
| addData | Records were added by a fetch. |
| refreshRecords | Records were refreshed by a save or fetchRecords. |
| clearChanges | The changes were cleared, after a save. |
| instanceRename | A detail model's master record got a new ID, after a save. |
| destroy | The model was destroyed. |
const orders = apex.model.get("orbitOrders");
const viewId = orders.subscribe({
viewId: "orbit_log",
onChange: (changeType, change) => console.log("notification:", changeType,
change.recordId ?? change.records?.map((r) => orders.getRecordId(r)) ?? "", change.field ?? "")
});
orders.setRecordValue("2279", "STATUS", "SHIPPED");
orders.insertNewRecord();
orders.deleteRecords([orders.getRecord("2280")]);
orders.revertRecords([orders.getRecord("2279")]);
orders.setValidity("error", "2278", "TOTAL", "Too high");
orders.clearChanges();
orders.unSubscribe(viewId);
orders.setRecordValue("2282", "STATUS", "RETURNED"); // no longer observedOutput:
notification: set 2279 STATUS notification: insert t1000 notification: delete ["2280"] notification: revert ["2279"] notification: metaChange 2278 TOTAL notification: clearChanges
The last change was not logged because the observer had been removed. Subscribing is the reliable way to react to edits in a grid, because it catches changes from the user, from code, and from other views alike.
Loading and Reshaping Data
setData and clearData
setData gives the model records at an offset, with the total and whether there is more, for a model that does not fetch its own. clearData removes all records and, unless pNotify is false, sends refresh so views fetch again.
model.setData(pData, [pOffset], [pTotal], [pMoreData]) model.clearData([pNotify]) → boolean
const orders = apex.model.get("orbitOrders");
orders.subscribe({ onChange: (type) => console.log("notification:", type) });
orders.clearData();
console.log("after clearData:", orders.getTotalRecords());
orders.setData([{ ORDER_ID: "2276", ORDER_NUMBER: "ORD-12276", CUSTOMER: "Riverbend Guides", STATUS: "APPROVED", TOTAL: "9051.16" }]);
console.log("after setData:", orders.getTotalRecords(), orders.getRecordValue("2276", "CUSTOMER"));Output:
notification: refresh after clearData: 0 notification: addData notification: refresh after setData: 1 Riverbend Guides
fetch, fetchAll, fetchRecords, hasControlBreaks, and getControlBreakId
Get data from the server: fetch loads a page from an offset, fetchAll loads every page and calls the callback after each, and fetchRecords loads fresh copies of specific records. Usually the model fetches by itself when a view asks for records. hasControlBreaks and getControlBreakId tell whether the data has control breaks and which break a record belongs to.
model.fetch([pOffset], [pCallback], [pNoProgress]) → Promise model.fetchAll(pCallback, [pNoProgress]) model.fetchRecords(pRecords, [pCallback]) → Promise model.hasControlBreaks() → boolean model.getControlBreakId(pRecord) → string | null
const model = apex.model.get(apex.model.list()[0]); // the interactive grid's model
console.log("records:", model.getTotalRecords(), "- server total:", model.getServerTotalRecords());
model.clearData(false);
await model.fetch(0);
console.log("fetched:", model.getTotalRecords(true));
let calls = 0;
await new Promise((resolve) => model.fetchAll((status) => { calls++; if (status.done) resolve(); }));
console.log("fetchAll:", calls, "call(s), records:", model.getTotalRecords());
const first = model.recordAt(0);
await model.fetchRecords([first]);
console.log("fetchRecords: refreshed", model.getRecordId(first));
console.log("control breaks:", model.hasControlBreaks(), "- getControlBreakId:", model.getControlBreakId(first));Output:
records: 13 - server total: 13 fetched: 13 fetchAll: 1 call(s), records: 13 fetchRecords: refreshed 1 control breaks: false - getControlBreakId: null
fetchAll is what you need before summing or exporting every row of a paged grid, because forEach only sees the records already loaded.
getOption, setOption, modelId, getFieldKey, getFieldMetadata, and isIdentityField
getOption reads an option, and setOption changes one of those that may change, such as genIdPrefix, pageItemsToSubmit, fetchData, saveData, regionData, and pageSize. modelId returns the model's ID, getFieldKey the property name or array index of a field, getFieldMetadata a field's metadata, and isIdentityField whether a field is the identity.
model.getOption(pName) → * model.setOption(pName, pValue) model.modelId() → ModelId model.getFieldKey(pFieldName) → string | number model.getFieldMetadata(pFieldName) → FieldMeta model.isIdentityField(pFieldName) → boolean
const orders = apex.model.get("orbitOrders");
console.log("modelId:", orders.modelId(), "- editable:", orders.getOption("editable"),
"- pageSize:", orders.getOption("pageSize"), "- genIdPrefix:", orders.getOption("genIdPrefix"));
orders.setOption("genIdPrefix", "new");
console.log("new record id:", orders.insertNewRecord());
console.log("field key:", orders.getFieldKey("CUSTOMER"), "- metadata:", orders.getFieldMetadata("STATUS"));
console.log("identity:", orders.isIdentityField("ORDER_ID"), orders.isIdentityField("STATUS"));Output:
modelId: orbitOrders - editable: true - pageSize: 100 - genIdPrefix: t
new record id: new1000
field key: CUSTOMER - metadata: {"defaultValue":"NEW"}
identity: true falsetransform
Builds another data structure from the records, such as the data for a chart, following a template of rules. Each rule creates an array at a path in the output, with one item per record: a field name, an object whose properties are field names or functions, or a function. filter, uniqueIndexField, and sort shape each array.
model.transform(pOptions, [pContext]) → object
const orders = apex.model.get("orbitOrders");
const chart = orders.transform({
template: [{
path: "items",
filter: (model, record) => model.getValue(record, "STATUS") === "APPROVED",
item: { label: "CUSTOMER", value: (model, record) => Number(model.getValue(record, "TOTAL")) }
}, {
path: "statuses",
uniqueIndexField: "STATUS",
item: "STATUS"
}]
});
console.log(JSON.stringify(chart, null, 1));Output:
{
"items": [
{
"label": "Matthew Jones",
"value": 1329.95
},
{
"label": "Linda Lopez",
"value": 1030.69
},
{
"label": "Prairie Travel Co.",
"value": 11495.93
}
],
"statuses": [
"SHIPPED",
"CANCELLED",
"APPROVED"
]
}One pass produced two structures: chart points for the approved orders only, and a list of distinct statuses. That makes it easy to keep a small chart in sync with a grid without another server call.
updateVisibility
Calls the model's visibilityFilter for every record and sets their hidden metadata, for filtering in the browser. The argument becomes the new filter context.
model.updateVisibility([pVisibilityContext])
apex.model.create("orbitFiltered", {
identityField: "ORDER_ID", paginationType: "none",
fields: { ORDER_ID: {}, STATUS: {} },
visibilityFilter: (model, record, context) => !context.status || model.getValue(record, "STATUS") === context.status,
visibilityFilterContext: {}
}, [{ ORDER_ID: "2282", STATUS: "SHIPPED" }, { ORDER_ID: "2280", STATUS: "CANCELLED" }, { ORDER_ID: "2279", STATUS: "APPROVED" }]);
const m = apex.model.get("orbitFiltered");
const visible = () => { const ids = []; m.forEach((r, i, id) => { if (!m.getRecordMetadata(id).hidden) ids.push(id); }); return ids.join(" "); };
console.log("all:", visible());
m.updateVisibility({ status: "APPROVED" });
console.log("APPROVED:", visible());Output:
all: 2282 2280 2279 APPROVED: 2279
Tree Models
root, child, childCount, hasChildren, parent, and walkTree
A tree model has a root node whose childrenField holds its children. root returns the root, child returns a child by index, and childCount and hasChildren report how many children a node has, or null when they are not loaded yet. parent returns a node's parent. walkTree visits a node and its descendants depth-first, calling the visitor's node, beginChildren, endChildren, and postNode functions; node can return true to skip a branch.
model.root() → Node model.child(pNode, pIndex) → Node model.childCount(pNode) → number | null model.hasChildren(pNode) → boolean | null model.parent(pNode) → Node model.walkTree(pNode, pVisitor, [pParentNode])
apex.model.create("orbitCategories", {
shape: "tree", identityField: "id", childrenField: "children", parentIdentityField: "parentId",
fields: { id: {}, name: {}, parentId: {}, children: {} }
}, { id: "0", name: "All Products", children: [
{ id: "1", name: "Camping", parentId: "0", children: [
{ id: "11", name: "Tents", parentId: "1", children: [] },
{ id: "12", name: "Sleeping Bags", parentId: "1", children: [] }] },
{ id: "2", name: "Clothing", parentId: "0", children: [
{ id: "21", name: "Jackets", parentId: "2", children: [] }] }] });
const tree = apex.model.get("orbitCategories");
const root = tree.root();
console.log("root:", tree.getValue(root, "name"), "- children:", tree.childCount(root), "- hasChildren:", tree.hasChildren(root));
const camping = tree.child(root, 0);
console.log("child 0:", tree.getValue(camping, "name"), "- parent:", tree.getValue(tree.parent(camping), "name"));
let depth = 0;
tree.walkTree(root, {
node: (node) => console.log(" ".repeat(depth) + tree.getValue(node, "name")),
beginChildren: () => { depth++; },
endChildren: () => { depth--; }
});Output:
root: All Products - children: 2 - hasChildren: true
child 0: Camping - parent: All Products
All Products
Camping
Tents
Sleeping Bags
Clothing
JacketsFor trees that load lazily, fetchChildNodes(pNode, [pCallback]) loads a node's children from the server.
Recipes
Validate a Rule Across Two Columns
A single column's validation, such as required, a maximum length, or a range, is set declaratively. A rule across columns can run in the browser through the model's notifications: when Flagship or Floor Area changes, the code checks the row and marks the cell with setValidity. The grid shows the error on the cell with its message, and will not save while the model has errors.
Put the code in the page's Execute when Page Loads attribute. The part marked Try it makes a small store a flagship and then enlarges it.
// === Page › Execute when Page Loads ===
// A rule across two columns: a flagship store needs at least 8,000 sq ft.
const stores = apex.region("stores").call("getViews", "grid").model;
function checkFloorArea(record) {
const id = stores.getRecordId(record);
const area = apex.locale.toNumber(stores.getValue(record, "FLOOR_AREA_SQFT") || "0");
if (stores.getValue(record, "FLAGSHIP").v && area < 8000) {
stores.setValidity("error", id, "FLOOR_AREA_SQFT",
"A flagship store needs at least 8,000 sq ft.");
} else {
stores.setValidity("valid", id, "FLOOR_AREA_SQFT");
}
}
stores.subscribe({
onChange: (type, change) => {
if (type === "set" && ["FLAGSHIP", "FLOOR_AREA_SQFT"].includes(change.field)) {
checkFloorArea(change.record);
}
}
});
// === Try it: make Boulder (6,400 sq ft) a flagship, then enlarge it ===
const boulder = stores.getRecord("2");
stores.setValue(boulder, "FLAGSHIP", { v: true, d: "On" });
console.log("errors:", stores.hasErrors(), "-",
stores.getRecordMetadata("2").fields.FLOOR_AREA_SQFT.message);
stores.setValue(boulder, "FLOOR_AREA_SQFT", "8500");
console.log("errors after 8,500 sq ft:", stores.hasErrors());
stores.setValue(boulder, "FLOOR_AREA_SQFT", "6400"); // (for the picture)Output:
errors: true - A flagship store needs at least 8,000 sq ft. errors after 8,500 sq ft: false

Notice that FLAGSHIP is a switch column whose value is an object with v for the value and d for the display text, which is why the code reads .v and sets { v: true, d: "On" }. As always, a server-side validation must check the same rule, for example a PL/SQL Function Body validation that runs for each changed row, as covered in the guide to validations in Oracle APEX.
Make Cells Read-Only in Some Rows
The name of a flagship store must not change, while other stores' names may. Declaratively, a column's Read Only condition can be evaluated For Each Row on the server. In the browser, the record's field metadata does it: setting fields.STORE_NAME.disabled makes that one cell read-only, and metadataChanged redraws it.
Put the code in the page's Execute when Page Loads attribute.
// === Page › Execute when Page Loads ===
// The names of flagship stores cannot be edited in the grid.
const grid = apex.region("stores").call("getViews", "grid");
function lockFlagshipNames() {
grid.model.forEach((record, index, id) => {
const meta = grid.model.getRecordMetadata(id);
meta.fields = meta.fields || {};
meta.fields.STORE_NAME = meta.fields.STORE_NAME || {};
meta.fields.STORE_NAME.disabled = !!grid.model.getValue(record, "FLAGSHIP").v;
grid.model.metadataChanged(id, "STORE_NAME"); // the grid redraws the cell
});
}
lockFlagshipNames();
grid.model.subscribe({ // again for new data and changes
onChange: (type, change) => {
if (["refresh", "addData"].includes(type) || (type === "set" && change.field === "FLAGSHIP")) {
lockFlagshipNames();
}
}
});
// === Try it: turn on edit mode and go to the name of Denver (a flagship), then of Boulder ===
apex.region("stores").call("getActions").set("edit", true);
for (const id of ["1", "2"]) {
grid.view$.grid("gotoCell", id, "STORE_NAME");
await new Promise((resolve) => setTimeout(resolve, 300));
console.log(grid.model.getValue(grid.model.getRecord(id), "STORE_NAME"), "→",
document.activeElement.tagName === "INPUT" ? "editable" : "read-only");
}Output:
Orbit Denver Union Station → read-only Orbit Boulder Pearl Street → editable
Metadata belongs to the records the model currently holds. Records fetched later, from the next page or a refresh, do not have it, which is why the code runs again on the refresh and addData notifications, and whenever Flagship changes.
Conclusion
A model holds the records of a region or of your own code, shaped as a table, a tree, or a single record. apex.model.create, get, release, and list manage models, and anyChanges, save, and multipleFetch work across all of them. A model reads records with getRecord, getValue, and forEach; changes them with setValue, insertNewRecord, copyRecords, moveRecords, and deleteRecords, within the limits of allowEdit and its siblings; tracks the changes for getChanges, revertRecords, and save; keeps validity and other metadata for its views; notifies subscribers of every change; and reshapes its data with transform. Tree models add root, child, parent, and walkTree. Reach an interactive grid's model through its view or with apex.model.get and the static ID plus _grid, and you can read, change, validate, and save grid data entirely from code.
