How to Build HTML, Apply Templates, and Debounce Events Using apex.util in Oracle APEX

A tested guide to apex.util in Oracle APEX, from client-side templates and named templates to escaping, debounce, spinners, and htmlBuilder.

apex.util is the toolbox that the rest of the Oracle APEX JavaScript API is built from, and most of it is just as useful in your own code. It has a template engine that understands the same substitution syntax as the server, escaping functions for HTML, attributes, and CSS selectors, small helpers for arrays and objects, debounce and throttle for noisy events, and spinners that do not flicker. Alongside it sit apex.widget.waitPopup and htmlBuilder, a safe way to assemble markup.

This guide covers all of it with tested examples and real output, including the named templates that are new in APEX 26.1, and ends with two recipes: showing server data through a template, and drawing simple bar charts without a chart region.

Quick Reference

TaskFunction
Render an APEX-style template in the browserapex.util.applyTemplate
Define and apply reusable named templatesdefineTemplates, applyNamedTemplate, getTemplateDef, listTemplates
Escape values safelyescapeHTML, escapeHTMLAttr, stripHTML, escapeCSS
Work with arrays, objects, and ISO datestoArray, arrayEqual, getNestedObject, getDateFromISO8601String
Tame frequent eventsdebounce, throttle
Show progressshowSpinner, delayLinger, apex.widget.waitPopup
Measure scrollbarsgetScrollbarSize
Build markup safelyhtmlBuilder

How to Run These Examples

The examples ran in Oracle APEX 26.1 on a test application with a page of sample items, a product catalog in cards, a customers report, a stores interactive grid, and a home page with a list of recent orders. The output under each example is exactly what the browser console printed.

Most examples work on any page: press F12, paste the code into the Console tab, and swap in your own item names and region IDs. Several use await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.

Client-Side Templates

apex.util.applyTemplate

Processes an APEX-style template in the browser, with the same syntax that template components and region templates use on the server, but filled with the values the browser has. It returns the resulting markup, with any script elements removed.

apex.util.applyTemplate(pTemplate, [pOptions]) → string

A template can contain three kinds of tokens, processed in this order:

  • Directives in braces. {if NAME/}, {elseif NAME/}, {else/}, and {endif/} test a value for being empty or one of the falseValues; ?NAME tests only for empty and =NAME only for true or false. {case NAME/}, {when value/}, {otherwise/}, and {endcase/} compare a value. {loop "SEP" NAME/} ... {endloop/} repeats for each value of a list, with &APEX$ITEM. and &APEX$I. for the value and its position, and {loop MODEL_ID/} repeats for each record of a model. {with/} ... {apply NAME/} applies a named template, and {!comment/} is removed.
  • Placeholders, #NAME#, filled from the placeholders option.
  • Substitutions, &NAME., filled from page items (including properties such as &P20_TEXT%LABEL. and %DISPLAY), a model's columns, built-ins such as &APP_USER. and &APP_PAGE_ID., the extraSubstitutions option, and text messages written as &APP_TEXT$KEY. or &{KEY param=value}. An escape filter can follow the name: !HTML (the default), !ATTR, !STRIPHTML, or !RAW.
OptionTypeDescription
placeholdersobjectValues for #NAME# placeholders.
extraSubstitutionsobjectValues for &NAME. substitutions.
directivesbooleanProcess directives. The default is true.
defaultEscapeFilterstring or falseHTML (the default), ATTR, RAW, or STRIPHTML.
includePageItemsbooleanSubstitute page items. The default is true.
includeBuiltinSubstitutionsbooleanSubstitute APP_ID, APP_USER, and the other built-ins. The default is true.
model, recordmodel, recordA model and record whose columns can be substituted.
falseValuesstring[]Values that {if} treats as false. The default is ["FALSE", "F", "f", "N", "n", "0"].
iterationCallbackfunctionCalled before each item or record of a {loop}.
id, idPostfix, isLazyLoading, componentCSSClassesValues for the built-in placeholders #APEX$DOM_ID#, #APEX$IS_LAZY_LOADING#, and #APEX$COMPONENT_CSS_CLASSES#.
const template = `<b>&P20_TEXT.</b> (&P20_TEXT%LABEL.) for &APP_USER. on page &APP_PAGE_ID. #NOTE#
{if ?P20_NUMBER/}Number: &P20_NUMBER.{else/}No number{endif/}
{case STATUS/}{when SHIPPED/}On its way{when CANCELLED/}Cancelled{otherwise/}Open{endcase/}
{loop "," COLORS/}[&APEX$I. &APEX$ITEM.]{endloop/} {!a comment/}&ORDER_NO!HTML.`;
console.log(apex.util.applyTemplate(template, {
    placeholders: { NOTE: "(a placeholder)", STATUS: "SHIPPED", COLORS: "Red,Olive,Sand" },
    extraSubstitutions: { ORDER_NO: "<ORD-12283>" }
}));

Output:

<b>Trailblazer 2-Person Tent</b> (Text Field) for ADMIN on page 20 (a placeholder)
Number: 1,250.00 On its way [1 Red][2 Olive][3 Sand] &lt;ORD-12283&gt;

One template exercised every token type: page item values and labels, built-ins, a placeholder, an if/else, a case, a loop over a list, a comment that vanished, and an extra substitution whose angle brackets were escaped by the default HTML filter. The older substitution forms, &ITEM without the trailing dot and #COLUMN#, are not supported in the browser.

A loop over a model repeats the text for each record, with the record's columns available as substitutions. Here it runs over the model behind the catalog's cards region:

// Loop over the records of the cards region's model; &SKU., &PRODUCT_NAME., and &PRICE. are its columns.
const out = apex.util.applyTemplate(`{loop catalog/}&APEX$I.. &SKU. &PRODUCT_NAME. &PRICE.\n{endloop/}`);
console.log(out.split("\n").slice(0, 3).join("\n"));

Output:

1. FTW-1005 Alpine Mountaineering Boot $209.99
2. POL-1002 Aluminium Trekking Poles $34.99
3. JKT-1001 Aurora Down Jacket $209.99

apex.util.defineTemplates, applyNamedTemplate, getTemplateDef, and listTemplates

New in 26.1. Named templates are templates you define once and apply by name with arguments, the browser side of template components. defineTemplates defines them; applyNamedTemplate applies one with the options of applyTemplate plus args; getTemplateDef returns a definition; and listTemplates returns the names of all templates, including those of template components on the page.

apex.util.defineTemplates(pTemplates)
apex.util.applyNamedTemplate(pTemplateName, [pOptions]) → string
apex.util.getTemplateDef(pTemplateName) → object | null
apex.util.listTemplates() → string[]
PropertyTypeDescription
namestringThe name, made of upper case letters, digits, underscores, dots, and dollar signs.
templatestringThe template text. The arguments are its placeholders.
argsobject[]The arguments, each with name, required, default, and escape.
defaultEscapestringThe escape filter for arguments without one: HTML, ATTR, STRIPHTML, or RAW.
cssFileUrls, javaScriptFileUrlsobject[]Files to load the first time the template is applied.
apex.util.defineTemplates([{
    name: "ORBIT.BADGE",
    template: `<span class="t-Badge #CLASS#">#LABEL#</span>`,
    args: [{ name: "LABEL", required: true }, { name: "CLASS", default: "u-color-1" }],
    defaultEscape: "HTML"
}]);
console.log(apex.util.getTemplateDef("ORBIT.BADGE").args);
console.log(apex.util.applyNamedTemplate("ORBIT.BADGE", { args: { LABEL: "Shipped & paid" } }));
// {with/}: each argument on a line of its own
console.log(apex.util.applyTemplate(`{with/}
LABEL:=&STATUS.
CLASS:=u-success
{apply ORBIT.BADGE/}`, { extraSubstitutions: { STATUS: "Approved" } }));
console.log(apex.util.applyTemplate(`{with/}LABEL:=&STATUS.{apply ORBIT.BADGE/}`,
                                    { extraSubstitutions: { STATUS: "Approved" } }));

Output:

[
  {
    "name": "LABEL",
    "required": true
  },
  {
    "name": "CLASS",
    "default": "u-color-1"
  }
]
<span class="t-Badge u-color-1">Shipped &amp; paid</span>
<span class="t-Badge u-success">Approved</span>
Error: applyTemplate 'apply' missing required argument LABEL
<span class="t-Badge u-color-1"></span>

The last two calls show a formatting rule the documentation does not stress. Inside a {with/} directive, each argument must be on a line of its own, written NAME:=value. With the argument on the same line as {with/}, it is not found. And a missing required argument does not throw: applyTemplate logs an error to the console and leaves the placeholder empty, so an empty badge on the page usually means a formatting mistake in {with/}.

console.log(apex.util.listTemplates());

Output:

[
  "STOCK_LEVEL",
  "THEME$ACTIONS",
  "THEME$AVATAR",
  "THEME$BADGE",
  "THEME$BUTTON",
  "THEME$COMMENTS",
  "THEME$CONTENT_ROW",
  "THEME$MEDIA_LIST",
  "THEME$METRIC_CARD",
  "THEME$TIMELINE",
  "APEX$TMPL_RPT_77499544179257704"
]

The theme's template components (THEME$...), the application's own (STOCK_LEVEL), and the report template of the Recent Orders region are all named templates you can apply from JavaScript. Building template components in Page Designer is covered in the guide to cards, content rows, and template components.

Escaping Values

apex.util.escapeHTML, escapeHTMLAttr, stripHTML, and escapeCSS

escapeHTML escapes a value for element content, covering &, <, >, double and single quotes, and the slash, like APEX_ESCAPE.HTML in PL/SQL. escapeHTMLAttr hex-escapes everything except letters, digits, and the characters , . - _ for use in an attribute value, like APEX_ESCAPE.HTML_ATTRIBUTE. stripHTML removes tags. escapeCSS escapes the special characters of a CSS selector, for an ID you did not choose yourself.

apex.util.escapeHTML(pValue) → string
apex.util.escapeHTMLAttr(pValue) → string
apex.util.stripHTML(pText) → string
apex.util.escapeCSS(pValue) → string
const name = `O'Brien & Sons <script>alert("x")</script>`;
console.log(apex.util.escapeHTML(name));
console.log(apex.util.escapeHTMLAttr(name));
console.log(apex.util.stripHTML("<p>Order <b>ORD-12283</b> was <em>shipped</em>.</p>"));
console.log("#" + apex.util.escapeCSS("order#12:new"));

Output:

O&#x27;Brien &amp; Sons &lt;script&gt;alert(&quot;x&quot;)&lt;&#x2F;script&gt;
O&#x27;Brien&#x20;&#x26;&#x20;Sons&#x20;&#x3C;script&#x3E;alert&#x28;&#x22;x&#x22;&#x29;&#x3C;&#x2F;script&#x3E;
Order ORD-12283 was shipped.
#order\#12\:new

Pick the function by where the value lands: escapeHTML between tags, escapeHTMLAttr inside an attribute, escapeCSS inside a jQuery or querySelector selector. Without escapeCSS, an ID containing # or : breaks the selector, which is what the last line guards against.

Arrays, Objects, and Dates

apex.util.toArray

Turns a value into an array. A string is split at a separator, a colon by default, or a regular expression, or with {type: "json-array"} for a JSON array string. Array-like objects such as NodeLists are converted, and empty values become an empty array.

apex.util.toArray(pValue, [pSeparator]) → Array
console.log(apex.util.toArray("Red:Olive:Sand"));
console.log(apex.util.toArray("Red, Olive, Sand", /,\s*/));
console.log(apex.util.toArray('["Red","Olive"]', { type: "json-array" }));
console.log(apex.util.toArray(""), apex.util.toArray(null), apex.util.toArray(["a"]));
console.log(apex.util.toArray(document.querySelectorAll("header")).length);

Output:

["Red", "Olive", "Sand"]
["Red", "Olive", "Sand"]
["Red", "Olive"]
[] [] ["a"]
1

This is handy for multi-value items, which store their values joined with colons, and for items using JSON array storage, as described in the guide to getting and setting page items with apex.item.

apex.util.arrayEqual

Returns true when two arrays have the same length and strictly equal elements. The comparison is shallow.

apex.util.arrayEqual(pArray1, pArray2) → boolean
console.log(apex.util.arrayEqual([1, 2, 3], [1, 2, 3]));
console.log(apex.util.arrayEqual([1, 2, 3], [1, 2, "3"]));
console.log(apex.util.arrayEqual([{ a: 1 }], [{ a: 1 }]), "(shallow: two different objects)");

Output:

true
false
false (shallow: two different objects)

apex.util.getNestedObject

Returns the object at a dotted path in an object structure, creating any missing objects on the way. It lets you set a deeply nested property without checking each level first.

apex.util.getNestedObject(pRootObject, pPath) → object
const settings = {};
apex.util.getNestedObject(settings, "report.columns.price").width = 120;
apex.util.getNestedObject(settings, "report.columns.sku").hidden = true;
console.log(settings);

Output:

{"report":{"columns":{"price":{"width":120}, "sku":{"hidden":true}}}}

apex.util.getDateFromISO8601String

Parses a date in simplified ISO 8601 format. Date.parse does the same in current browsers, and apex.date.parse handles Oracle formats, as covered in the guide to formatting dates and numbers with apex.date and apex.locale.

apex.util.getDateFromISO8601String(pDateStr) → Date
const d = apex.util.getDateFromISO8601String("2026-09-23T14:30:00Z");
console.log(d.toISOString());
console.log(apex.util.getDateFromISO8601String("2026-09-23").toISOString());

Output:

2026-09-23T14:30:00.000Z
2026-09-23T00:00:00.000Z

Taming Frequent Events

apex.util.debounce

Returns a function that calls yours only once calls have stopped for the given number of milliseconds, which is exactly what a search as the user types needs. With pImmediate, it calls on the first call and then ignores further calls until they pause.

apex.util.debounce(pFunction, pDelay, [pImmediate]) → function
const search = apex.util.debounce((text) => console.log("search for:", text), 300);
for (const text of ["t", "te", "ten", "tent"]) search(text);        // the user types quickly
await new Promise((resolve) => setTimeout(resolve, 500));
const saveNow = apex.util.debounce(() => console.log("immediate call"), 300, true);
saveNow(); saveNow(); saveNow();
await new Promise((resolve) => setTimeout(resolve, 400));

Output:

search for: tent
immediate call

Four quick keystrokes produced one search, for the final text. The immediate version fired on the first of three calls and swallowed the rest, which suits a save button that users tend to double-click.

apex.util.throttle

Returns a function that calls yours at most once every pWait milliseconds, however often it is called, which suits scroll and resize handlers. With pImmediate, the default, the first call happens at once.

apex.util.throttle(pFunction, pWait, [pImmediate]) → function
const start = Date.now();
const onScroll = apex.util.throttle(() => console.log("handled at", Math.round((Date.now() - start) / 100) * 100, "ms"), 200);
const timer = setInterval(onScroll, 20);                          // 50 calls a second
await new Promise((resolve) => setTimeout(resolve, 650));
clearInterval(timer);
await new Promise((resolve) => setTimeout(resolve, 300));

Output:

handled at 0 ms
handled at 200 ms
handled at 400 ms
handled at 600 ms
handled at 800 ms

Fifty calls a second became one call every 200 milliseconds. The difference from debounce: throttle keeps firing at a steady pace during the activity, while debounce waits until it stops. The old helpers invokeAfterPaint and cancelInvokeAfterPaint are deprecated; use window.requestAnimationFrame and cancelAnimationFrame instead.

Showing Progress

apex.util.showSpinner

Shows a spinner centered in an element, or on the page, and returns it; call .remove() on the result to take it away. The spinner is announced to screen readers.

apex.util.showSpinner([pContainer], [pOptions]) → jQuery
OptionTypeDescription
alertstringThe hidden label for assistive technologies. The default is Processing.
ariaProcessingStarted, ariaStillProcessing, ariaProcessedstringThe live messages when the spinner appears, while it is showing, and when it goes.
spinnerClassstringAn extra class for styling.
fixedbooleanFix it on the page, ignoring pContainer.
suppressAriabooleanMake no announcements.
const spinner = apex.util.showSpinner("#customers", { alert: "Loading customers" });
console.log("spinner:", spinner.attr("class"), "- role:", spinner.attr("role"));
await new Promise((resolve) => setTimeout(resolve, 300));

Output:

spinner: u-Processing - role: region

There is a longer walkthrough in the apex.util.showSpinner example. For spinners during Ajax calls, the loadingIndicator option of apex.server.process draws one for you, as covered in the guide to calling the server with apex.server.process.

apex.util.delayLinger.start and finish

A spinner that appears for a 50-millisecond request only flickers. delayLinger shows it only when the work takes longer than a short delay, and then keeps it on screen long enough to be seen. Call start with a scope name and a function that shows the indicator, and finish with the same name and a function that removes it; finish only calls its function if start's function actually ran.

apex.util.delayLinger.start(pScopeName, pAction)
apex.util.delayLinger.finish(pScopeName, pAction)
// The spinner shows only if the work takes long enough, and then stays long enough not to flicker.
const run = async (label, ms) => {
    let spinner = null;
    const t0 = Date.now();
    apex.util.delayLinger.start(label, () => { spinner = apex.util.showSpinner(); console.log(label, "shown at", Math.round((Date.now() - t0) / 100) * 100, "ms"); });
    await new Promise((resolve) => setTimeout(resolve, ms));        // the work
    apex.util.delayLinger.finish(label, () => { spinner.remove(); console.log(label, "removed at", Math.round((Date.now() - t0) / 100) * 100, "ms"); });
    await new Promise((resolve) => setTimeout(resolve, 1500));
    if (!spinner) console.log(label, "no spinner");
};
await run("fast (100 ms)", 100);
await run("slow (1500 ms)", 1500);

Output:

fast (100 ms) no spinner
slow (1500 ms) shown at 200 ms
slow (1500 ms) removed at 1500 ms

The fast job never showed a spinner at all, and the slow one showed it after a short delay. That is the behavior users perceive as smooth.

apex.widget.waitPopup

Covers the page with an overlay and a spinner to stop the user clicking while the page is submitted, the same overlay apex.page.submit shows with showWait. It returns an object whose remove function takes the overlay away. The keyboard is not blocked.

apex.widget.waitPopup([pContent]) → {remove}
const popup = apex.widget.waitPopup();
console.log("overlay:", apex.jQuery(".u-Processing").length > 0 || apex.jQuery("#apex_wait_overlay").length > 0);
await new Promise((resolve) => setTimeout(resolve, 500));
setTimeout(() => popup.remove(), 2000);
console.log("remove:", typeof popup.remove);

Output:

overlay: true
remove: function

Always make sure remove runs, in a finally block or in both the success and error paths, or the user is stuck behind the overlay. The related apex.widget.initPageItem is deprecated; use apex.item.create instead.

apex.util.getScrollbarSize

Returns the width and height of the system's scrollbars. Both are zero with overlay scrollbars, as in the headless browser that ran this example and on most phones and Macs.

apex.util.getScrollbarSize() → {width, height}
console.log(apex.util.getScrollbarSize());

Output:

{"width":0, "height":0}

Building Markup Safely with htmlBuilder

apex.util.htmlBuilder() returns a builder that assembles markup piece by piece, escaping attribute values and content along the way, so values that came from users cannot inject HTML. APEX's own widgets render with it.

apex.util.htmlBuilder() → htmlBuilder

Every method except toString and toJquery returns the builder, so calls can be chained:

MethodDescription
markup(pMarkup)Adds markup as it is.
attr([pName], pValue)Adds an attribute with an escaped value, or only the escaped value when there is no name.
optionalAttr(pName, pValue)Adds the attribute only for a non-empty string, a non-zero number, or true.
optionalBoolAttr(pName, pValue)Adds a Boolean attribute such as disabled when pValue is true.
content(pContent)Adds escaped text.
css(pName, pValue)New in 26.1. Adds a CSS property as a data-css- attribute that toJquery applies, avoiding an inline style attribute that a Content Security Policy may block.
append(pHtmlBuilder)New in 26.1. Adds the markup and CSS of another builder.
clear()Removes everything so the builder can be reused.
toString()Returns the markup.
toJquery()New in 26.1. Returns the markup as jQuery elements with the CSS properties applied.
const status = `Shipped & "paid"`;
const out = apex.util.htmlBuilder();
out.markup("<span")
   .attr("class", "t-Badge")
   .optionalAttr("title", status)
   .optionalAttr("data-empty", "")                       // left out: empty
   .optionalBoolAttr("hidden", false)                    // left out: false
   .markup(">")
   .content(status)
   .markup("</span>");
console.log(out.toString());
out.clear();
console.log("after clear:", JSON.stringify(out.toString()));

Output:

<span class='t-Badge' title='Shipped&#x20;&#x26;&#x20;&#x22;paid&#x22;'>Shipped &amp; &quot;paid&quot;</span>
after clear: ""

The empty and false optional attributes were left out, and both the attribute value and the content were escaped even though they came from the same unsafe string.

const row = (label, value) => apex.util.htmlBuilder().markup("<dt>").content(label).markup("</dt><dd>").content(value).markup("</dd>");
const list = apex.util.htmlBuilder().markup("<dl>");
list.append(row("Order", "ORD-12283")).append(row("Total", "$463.21")).markup("</dl>");
console.log(list.toString());

Output:

<dl><dt>Order</dt><dd>ORD-12283</dd><dt>Total</dt><dd>$463.21</dd></dl>
const out = apex.util.htmlBuilder();
out.markup("<div").attr("class", "orbit-bar").css("width", "42%").css("color", null).markup("></div>");
console.log(out.toString());                              // a data attribute, not a style attribute
const bar$ = out.toJquery();
console.log(bar$[0].outerHTML, "- style width:", bar$[0].style.width);

Output:

<div class='orbit-bar' data-css-width='42&#x25;'></div>
<div class="orbit-bar" style="width: 42%;"></div> - style width: 42%

The test application's Content Security Policy blocks inline style attributes, which is why css exists: the builder writes a data attribute, and toJquery sets the style from JavaScript on the element, which the policy allows. For more on styling APEX pages from code, see the guide to JavaScript and CSS in Oracle APEX.

Recipes

Show Server Data with a Template

When the user moves through the Recent Orders list, a panel shows the current order: its number, customer, status, lines, and total, fetched from an Ajax callback named ORDER_SUMMARY that returns the order as JSON. The markup is a template in the same syntax as a template component, so the JavaScript holds no string concatenation, and applyTemplate escapes every value by default. The {case} directive turns the status code into a colored label.

Put the template and the function in the page's Function and Global Variable Declaration attribute and the handler in Execute when Page Loads. The callback itself is shown in the guide to calling the server with apex.server.process.

// === Page › Function and Global Variable Declaration ===
const orderCard = `
  <h3 class="u-margin-none">&ORDER_NUMBER. · &CUSTOMER.</h3>
  <p class="u-margin-none">
    {case STATUS/}
      {when APPROVED/}<span class="u-success-text">Approved</span>
      {when PENDING_APPROVAL/}<span class="u-warning-text">Waiting for approval</span>
      {otherwise/}&STATUS.
    {endcase/}
    · &LINES. lines · <strong>&TOTAL.</strong>
  </p>`;

// Shows the summary of an order in the Welcome region.
async function showOrderCard(orderId) {
    const order = await apex.server.process("ORDER_SUMMARY", { x01: orderId });
    const html = apex.util.applyTemplate(orderCard, {
        extraSubstitutions: {                                // values are escaped by default
            ORDER_NUMBER: order.orderNumber, CUSTOMER: order.customer, STATUS: order.status,
            LINES: String(order.lines),
            TOTAL: apex.locale.formatNumber(order.total, "FML999G999G990D00")
        }
    });
    apex.region("welcome").element.find(".t-Region-body").html(html);
}
// === Page › Execute when Page Loads ===
apex.region("recent_orders").element.on("apexcurrentrowchange", () => {
    showOrderCard(apex.region("recent_orders").getCurrentRowValue());
});
// === Try it: click the first order in Recent Orders ===
apex.region("recent_orders").setCurrentRowValue("2277");
await new Promise((resolve) => setTimeout(resolve, 1200));
const body = apex.region("welcome").element.find(".t-Region-body")[0];
console.log(body.innerText.trim().replace(/\s*\n\s*/g, " | "));

Output:

ORD-12277 · Prairie Travel Co. | Approved · 4 lines · $11,495.93
An Oracle APEX region showing order ORD-12277 for Prairie Travel Co. with a green Approved label and the total
The order card rendered from the template, with the status shown as a colored label.

apexcurrentrowchange fires when the user clicks a row or moves to it with the keyboard, as covered in the guide to the apex namespace and page events. To reuse the template on several pages, define it once with apex.util.defineTemplates in a static file and apply it by name.

Draw Simple Bars Without a Chart

Sometimes a chart region is too much, and five bars under a grid showing the largest stores are enough. The markup comes from htmlBuilder, which escapes the store names, and the bars' widths are set with the builder's css method rather than inline style attributes, which the application's Content Security Policy blocks. toJquery returns the elements with those properties applied.

Put the code in the page's Execute when Page Loads attribute, and call the function again after the grid saves or refreshes.

// === Page › Execute when Page Loads ===
// Draws the five largest stores as bars under the grid, from the grid's own data.
function drawStoreBars() {
    const model = apex.region("stores").call("getViews", "grid").model;
    const stores = [];
    model.forEach((record) => stores.push({
        name: model.getValue(record, "STORE_NAME"),
        area: apex.locale.toNumber(model.getValue(record, "FLOOR_AREA_SQFT"))
    }));
    stores.sort((a, b) => b.area - a.area).splice(5);
    const out = apex.util.htmlBuilder();
    out.markup("<div").attr("id", "store_bars").attr("class", "u-padding-md").markup(">");
    for (const store of stores) {
        out.markup("<div").attr("class", "u-flex u-align-items-center u-margin-bottom-sm").markup(">")
           .markup("<span").css("width", "16rem").markup(">").content(store.name).markup("</span>")
           .markup("<span").css("width", `${Math.round(store.area / stores[0].area * 60)}%`)
           .css("height", "1rem").css("background-color", "var(--ut-palette-primary)")
           .markup("></span>")
           .markup("<span").attr("class", "u-margin-left-sm").markup(">")
           .content(apex.locale.formatNumber(store.area, "FM999G999") + " sq ft").markup("</span>")
           .markup("</div>");
    }
    out.markup("</div>");
    $("#store_bars").remove();
    out.toJquery().insertAfter("#stores");       // toJquery applies the css() values: no inline style
}
drawStoreBars();
// === Try it: the bars' widths, as the browser applied them ===
$("#store_bars > div").each((i, row) => console.log($(row).children().first().text(), "-",
    $(row).children().eq(1)[0].style.width));

Output:

Orbit Denver Union Station - 60%
Orbit Chicago River North - 54%
Orbit Seattle Capitol Hill - 47%
Orbit Toronto Queen West - 45%
Orbit Atlanta Ponce City - 41%
Five horizontal bars showing the floor area of the largest Orbit stores
The five largest stores as bars, drawn with htmlBuilder from the grid's own data.

The classes u-flex, u-padding-md, and u-margin-left-sm are Universal Theme utility classes, and --ut-palette-primary is the theme style's primary color, so the bars follow the application's theme automatically.

Conclusion

apex.util.applyTemplate renders APEX templates in the browser with directives, placeholders, and substitutions of items, model columns, built-ins, and text messages, escaping values by default. APEX 26.1 adds named templates with arguments, which need one argument per line after {with/}. escapeHTML, escapeHTMLAttr, stripHTML, and escapeCSS protect content, attributes, and selectors. toArray, arrayEqual, and getNestedObject handle everyday data chores. debounce waits for activity to stop, while throttle keeps a steady pace during it. showSpinner, delayLinger, and waitPopup show progress without flicker, and htmlBuilder assembles markup with escaping and CSP-safe styles.

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