How to Read and Change Grid Data Using apex.model in Oracle APEX

A tested guide to apex.model in Oracle APEX, from reading and changing records to tracking changes, validity, notifications, and tree models.

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

TaskFunctions and methods
Create, find, and free modelsapex.model.create, get, release, destroy, list
Work across all modelsapex.model.anyChanges, anyErrors, addChangesToSaveRequest, save, multipleFetch
Read recordsgetRecord, getRecordId, getValue, getRecordValue, recordAt, indexOf, forEach, forEachInPage
Count recordsgetTotalRecords, getServerTotalRecords, getDataOverflow
Change recordssetValue, setRecordValue, insertNewRecord, copyRecords, moveRecords, deleteRecords
Check permissionsallowEdit, allowDelete, allowAdd, allowDrag, check, dragOperations
Track and save changesisChanged, getChanges, canRevertRecord, revertRecords, clearChanges, save
Validity and metadatagetRecordMetadata, setValidity, hasErrors, getErrors, setDisabledState, setHiddenState, isDisabled, metadataChanged
Selection kept in the modelsetSelectionState, getSelectedRecords, getSelectedCount, getSelectionState, clearSelection
React to changessubscribe, unSubscribe
Load and reshape datasetData, clearData, fetch, fetchAll, fetchRecords, transform, updateVisibility
Options and fieldsgetOption, setOption, modelId, getFieldKey, getFieldMetadata, isIdentityField
Tree modelsroot, 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:

OptionDescription
shapetable (the default), tree, or record.
fieldsRequired. An object of field names and their metadata, described below.
identityFieldThe field, or array of fields, that identifies a record. Required for editable table and tree models.
editableWhether records can be changed. The default is false.
recordIsArrayRecords are arrays, and each field's metadata has an index.
paginationTypenone (all data is present), one (one page at a time), or progressive (pages are added).
pageSizeThe number of records per fetch. The default is 100.
hasTotalRecordsThe server reports the total record count.
regionId, ajaxIdentifierThe region and plug-in Ajax identifier used to fetch and save. A model without a region is local.
pageItemsToSubmit, regionData, fetchData, saveData, requestOptionsPage items and extra data sent with requests.
trackChanges, onlyMarkForDeleteTrack changes (default true), and keep deleted records, marked, until saved (default true).
checkA function for extra permission checks.
genIdPrefixThe prefix of IDs for new records. The default is t.
parentModel, parentRecordIdThe master model and record of a detail model.
childrenField, parentIdentityFieldFor trees: the field holding a node's children, and the field holding its parent's ID.
metaFieldThe field that holds record metadata sent by the server.
visibilityFilter, visibilityFilterContextA function that decides which records are visible.
callServerA function used instead of apex.server.plugin for requests.

Each field takes metadata of its own:

PropertyDescription
dataTypeThe field's data type.
defaultValueThe value given to new records.
readonlyThe field cannot be changed.
volatileThe server generates the field, so it is not sent back.
virtualA field with no data, for views.
noCopyNot copied when a record is copied.
calcValue, dependsOnA function that calculates the field from the fields listed in dependsOn.
aggregatesAggregate functions such as COUNT, SUM, AVG, MIN, MAX, and MEDIAN.
controlBreakIndexThe field's position in a control break.
parentFieldFor detail models: the master field whose value new records receive.
indexThe 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: 6

After 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-urgent

An 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: 0

To 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 propertyDescription
viewIdA unique ID, returned by subscribe. A DOM ID is a good choice.
onChangefunction(changeType, change), called for each notification.
progressView, progressOptionsAn element to show a spinner over while the model fetches or saves, and options for it.

The notifications a model sends:

NotificationSent when
setA field value changed. change has record, recordId, field, and oldValue.
insert, copy, move, deleteRecords were inserted, copied, moved, or deleted.
revertChanges were reverted.
metaChangeMetadata changed, such as validity, highlight, or disabled.
refreshThe data was replaced or cleared, so views fetch again.
addDataRecords were added by a fetch.
refreshRecordsRecords were refreshed by a save or fetchRecords.
clearChangesThe changes were cleared, after a save.
instanceRenameA detail model's master record got a new ID, after a save.
destroyThe 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 observed

Output:

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 false

transform

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
    Jackets

For 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
An interactive grid row for the Boulder store with the floor area cell marked in red as an error
The floor area cell of a flagship store marked with an error by setValidity.

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.

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