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
| Task | Function |
|---|---|
| Render an APEX-style template in the browser | apex.util.applyTemplate |
| Define and apply reusable named templates | defineTemplates, applyNamedTemplate, getTemplateDef, listTemplates |
| Escape values safely | escapeHTML, escapeHTMLAttr, stripHTML, escapeCSS |
| Work with arrays, objects, and ISO dates | toArray, arrayEqual, getNestedObject, getDateFromISO8601String |
| Tame frequent events | debounce, throttle |
| Show progress | showSpinner, delayLinger, apex.widget.waitPopup |
| Measure scrollbars | getScrollbarSize |
| Build markup safely | htmlBuilder |
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.
| Option | Type | Description |
|---|---|---|
| placeholders | object | Values for #NAME# placeholders. |
| extraSubstitutions | object | Values for &NAME. substitutions. |
| directives | boolean | Process directives. The default is true. |
| defaultEscapeFilter | string or false | HTML (the default), ATTR, RAW, or STRIPHTML. |
| includePageItems | boolean | Substitute page items. The default is true. |
| includeBuiltinSubstitutions | boolean | Substitute APP_ID, APP_USER, and the other built-ins. The default is true. |
| model, record | model, record | A model and record whose columns can be substituted. |
| falseValues | string[] | Values that {if} treats as false. The default is ["FALSE", "F", "f", "N", "n", "0"]. |
| iterationCallback | function | Called before each item or record of a {loop}. |
| id, idPostfix, isLazyLoading, componentCSSClasses | Values 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] <ORD-12283>
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[]
| Property | Type | Description |
|---|---|---|
| name | string | The name, made of upper case letters, digits, underscores, dots, and dollar signs. |
| template | string | The template text. The arguments are its placeholders. |
| args | object[] | The arguments, each with name, required, default, and escape. |
| defaultEscape | string | The escape filter for arguments without one: HTML, ATTR, STRIPHTML, or RAW. |
| cssFileUrls, javaScriptFileUrls | object[] | 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 & 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'Brien & Sons <script>alert("x")</script> O'Brien & Sons <script>alert("x")</script> 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
| Option | Type | Description |
|---|---|---|
| alert | string | The hidden label for assistive technologies. The default is Processing. |
| ariaProcessingStarted, ariaStillProcessing, ariaProcessed | string | The live messages when the spinner appears, while it is showing, and when it goes. |
| spinnerClass | string | An extra class for styling. |
| fixed | boolean | Fix it on the page, ignoring pContainer. |
| suppressAria | boolean | Make 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:
| Method | Description |
|---|---|
| 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 & "paid"'>Shipped & "paid"</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%'></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

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%

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.
