<h1>Client‑Side Utility Library – <code>pp</code> Object</h1>
<p>
    The <strong>pp</strong> global object provides modern, lightweight utilities that work
    everywhere in the framework — AJAX requests, dialog boxes, form helpers, and common
    functions like debounce and deep clone. It is loaded automatically on every project SPA page.
</p>
<p>
    Dependencies: <strong>SweetAlert2</strong> gives <code>pp.dialog</code> its rich look. When it isn't on the
    page, every <code>pp.dialog</code> method degrades gracefully to a native browser dialog (or a tiny built-in
    toast), so <code>pp.dialog</code> is always safe to call.
</p>
<div class="help-callout help-callout-info">
    <i class="bi bi-box-arrow-in-down"></i>
    <div><strong>Now in generated apps too.</strong> <code>pp.js</code> ships with every generated application, so
        your own client code — including <a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/component_custom_button">Custom&nbsp;Button</a>
        handlers and JS snippets — can use <code>pp.dialog</code>, <code>pp.ajax</code> and <code>pp.util</code> out of the box.</div>
</div>

<h2 id="objects">Objects at a glance</h2>
<p class="lead">Everything the client API gives you, in one place. Pick the object for the job, then jump to its
section for the full method list.</p>
<table class="help-table">
    <thead><tr><th>Object</th><th>What it's for</th></tr></thead>
    <tbody>
        <tr><td><a href="#runner"><code>Runner</code> / <code>pp.ctrl</code></td></a><td>Reach any <strong>field/control</strong> on the page by name — read/write, show/hide, style, validate. Works on add, edit, view, print, pdf, cards, custom.</td></tr>
        <tr><td><a href="#runner"><code>Runner</code> — grid</a></td><td>The <strong>list/card</strong> rows and the current selection — records, keys, cells.</td></tr>
        <tr><td><a href="#runner"><code>Runner.detail()</code></a></td><td><strong>Master/Detail</strong> child grids — read, and write child cells two-way.</td></tr>
        <tr><td><a href="#buttons"><code>Runner.button()</code></a></td><td>Page <strong>buttons</strong> — Save, Cancel, Add, Delete, bulk actions — hide / show / enable / disable.</td></tr>
        <tr><td><a href="#dialog"><code>pp.dialog</code></a></td><td><strong>Dialogs</strong>: toast, confirm, prompt, alert (rich, with native fallback).</td></tr>
        <tr><td><a href="#ajax"><code>pp.ajax</code></a> · <code>pp.run</code></td><td><strong>Server</strong>: fetch JSON, or call a named PHP action.</td></tr>
        <tr><td><a href="#form"><code>pp.form</code></a> · <code>pp.val</code></td><td>Lower-level <strong>form</strong> value + validation helpers.</td></tr>
        <tr><td><a href="#util"><code>pp.util</code></a></td><td><strong>Utilities</strong>: debounce, throttle, formatDate, escapeHtml, uuid…</td></tr>
        <tr><td><code>pp.page</code> · <code>pp.record()</code></td><td>The current <strong>page context</strong> (table, action, id) and record.</td></tr>
    </tbody>
</table>
<div class="help-callout help-callout-info">
    <i class="bi bi-lightbulb"></i>
    <div>New to this? Jump straight to the <a href="#cookbook"><strong>Cookbook</strong></a> — a run of copy-ready
        examples from basic to advanced. Every example here also autocompletes in the <strong>Events</strong>
        editor.</div>
</div>

<h2>Contents</h2>
<ul>
    <li><a href="#objects">Objects at a glance</a></li>
    <li><a href="#runner">Runner – page controls, grid &amp; Master/Detail</a></li>
    <li><a href="#buttons">Runner – page buttons</a></li>
    <li><a href="#dialog">pp.dialog – User Dialogs</a></li>
    <li><a href="#ajax">pp.ajax / pp.run – Server</a></li>
    <li><a href="#form">pp.form – Form Utilities</a></li>
    <li><a href="#util">pp.util – General Helpers</a></li>
    <li><a href="#reactive"><strong>Reactive fields, page elements &amp; rich dialogs</strong></a></li>
    <li><a href="#cookbook"><strong>Cookbook – basic to advanced</strong></a></li>
    <li><a href="#examples">More real‑world examples</a></li>
</ul>

<h2 id="runner">Runner – Page Control Access</h2>
<p>The <strong><code>Runner</code></strong> object (a single control is also reachable as
<code>pp.ctrl(field)</code>) is a clean, chainable way to reach any field on a page by its
database name — no hand-written DOM look-ups. It's ideal in event code such as
<a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/component_custom_button">Custom&nbsp;Button</a>
handlers. Every field carries a <code>data-pp-field="&lt;name&gt;"</code> hook, and plain <code>[name]</code> inputs
are found as a fallback.</p>

<h3>Get a control</h3>
<pre><code>var c = Runner.getControl('first_name');   // current page
// PHPRunner-style, scoped to a page container, is also accepted:
var c = Runner.getControl(pageId, 'first_name');</code></pre>

<h3>Control methods (all chainable)</h3>
<table class="help-table">
    <thead><tr><th>Method</th><th>Does</th></tr></thead>
    <tbody>
        <tr><td><code>getValue()</code> / <code>setValue(v)</code></td><td>Read / write the value (text, select, checkbox, radio, plugins). <code>setValue</code> fires <code>change</code>.</td></tr>
        <tr><td><code>getText()</code></td><td>The visible text (e.g. a select's option label).</td></tr>
        <tr><td><code>clear()</code> · <code>focus()</code></td><td>Empty the value · move focus to it.</td></tr>
        <tr><td><code>show()</code> / <code>hide()</code> / <code>visible(bool)</code></td><td>Show / hide the whole field, label included.</td></tr>
        <tr><td><code>enable()</code> / <code>disable()</code> · <code>readonly(bool)</code> · <code>required(bool)</code></td><td>Toggle input state.</td></tr>
        <tr><td><code>addClass(c)</code> / <code>removeClass(c)</code></td><td>Style the field wrapper.</td></tr>
        <tr><td><code>label()</code></td><td>The field's label text.</td></tr>
        <tr><td><code>on(evt, fn)</code> / <code>onChange(fn)</code></td><td>Attach an event listener.</td></tr>
        <tr><td><code>setMessage(msg[, 'success'])</code> / <code>clearMessage()</code></td><td>Show / clear an inline message under the field.</td></tr>
        <tr><td><code>validate(rules)</code> / <code>invalid()</code></td><td>Check <code>{required, minlength, email, pattern}</code>; shows the message and records the state. Returns <code>true</code> when valid.</td></tr>
        <tr><td><code>reset()</code></td><td>Restore the value the field had when the page loaded.</td></tr>
        <tr><td><code>isReadonly()</code></td><td>Is the control read-only right now?</td></tr>
        <tr><td><code>watch(fn)</code></td><td>Run <code>fn(value, control)</code> on every change — the reactive backbone behind <code>compute()</code>/<code>link()</code>.</td></tr>
        <tr><td><code>addStyle(css)</code> / <code>setStyle(css)</code> · <code>getDispElem()</code></td><td>Inline-style the control · the raw input element.</td></tr>
        <tr><td><code>exists()</code></td><td><code>false</code> for an unknown field — nothing throws.</td></tr>
    </tbody>
</table>

<h3>Page-level shortcuts</h3>
<table class="help-table">
    <thead><tr><th>Method</th><th>Returns / does</th></tr></thead>
    <tbody>
        <tr><td><code>Runner.getControlValue(field)</code></td><td>One value, without an intermediate variable.</td></tr>
        <tr><td><code>Runner.setControlValue(field, v)</code></td><td>Set one field's value.</td></tr>
        <tr><td><code>Runner.getData()</code></td><td>Every field as one object: <code>{ first_name:'…', email:'…' }</code>.</td></tr>
        <tr><td><code>Runner.setData({ … })</code></td><td>Set many fields at once.</td></tr>
        <tr><td><code>Runner.fields()</code></td><td>Array of field names on the page.</td></tr>
        <tr><td><code>Runner.hideField(f)</code> / <code>showField(f)</code> / <code>on(f, evt, fn)</code></td><td>Field shortcuts.</td></tr>
        <tr><td><code>Runner.getPageId()</code> / <code>getPageMode()</code></td><td>The table this page edits / the page mode (add, edit, custom…).</td></tr>
    </tbody>
</table>
<pre><code>// Chain freely — every method returns the control
Runner.getControl('email').setValue('').focus().setMessage('Enter a valid email');

// Read the whole form, guard, and light up the offending field
var d = Runner.getData();
if (!d.first_name) Runner.getControl('first_name').setMessage('Required').focus();</code></pre>
<div class="help-callout help-callout-info">
    <i class="bi bi-lightbulb"></i>
    <div>Familiar with PHPRunner? <code>Runner.getControl(pageId, field)</code> works the same way here — but the
        <code>pageId</code> is optional, so <code>Runner.getControl(field)</code> is all you usually need.</div>
</div>

<h3>List / grid pages</h3>
<p>On a list page, Runner reads the grid straight from the row data the build stamps on every row — the current
selection, each row's record, and any cell — so a toolbar Custom&nbsp;Button can act on the ticked rows without any
DOM work.</p>
<table class="help-table">
    <thead><tr><th>Method</th><th>Returns</th></tr></thead>
    <tbody>
        <tr><td><code>Runner.getSelectedKeys()</code></td><td>Ids of the ticked rows: <code>['1','4','7']</code>.</td></tr>
        <tr><td><code>Runner.getSelectedRecords()</code></td><td>The ticked rows' records: <code>[{ id, customer, total }, …]</code>.</td></tr>
        <tr><td><code>Runner.selectedCount()</code></td><td>How many rows are ticked.</td></tr>
        <tr><td><code>Runner.getAllRecords()</code></td><td>Every visible row's record.</td></tr>
        <tr><td><code>Runner.rows()</code> / <code>getRow(id)</code></td><td>Row object(s) — see below.</td></tr>
        <tr><td><code>Runner.getCellValue(id, field)</code></td><td>One cell's value for a given row.</td></tr>
        <tr><td><code>Runner.onSelectionChange(fn)</code></td><td>Fires with <code>(keys, records)</code> whenever the tick changes.</td></tr>
        <tr><td><code>Runner.reloadGrid()</code></td><td>Refresh the list (after a server change).</td></tr>
    </tbody>
</table>
<p>A <strong>row object</strong> (from <code>rows()</code> / <code>getRow(id)</code>) offers:
<code>.id</code>, <code>.data()</code> (the whole record), <code>.get('field')</code>, <code>.text('field')</code>
(visible cell text), <code>.selected()</code> / <code>.select(true)</code>, <code>.highlight()</code>,
<code>.addClass(c)</code>, <code>.on(evt, fn)</code>.</p>
<pre><code>// A toolbar Custom Button that acts on the ticked rows
var ids = Runner.getSelectedKeys();
if (!ids.length) { pp.dialog.toast('warning', 'Select at least one row.'); return false; }
params.ids = ids;                        // ① Client Before → send to the server

// ② Server: dbExecute('UPDATE orders SET status=? WHERE id IN (' + …', …)
// ③ Client After:
Runner.reloadGrid();</code></pre>
<div class="help-callout help-callout-info">
    <i class="bi bi-signpost-2"></i>
    <div>The older <code>pp.grid</code> / <code>pp.button.getSelectedRecords()</code> helpers still work too — Runner
        is the newer, unified way, and both read the same row data.</div>
</div>

<h3>Master / Detail</h3>
<p>On a master's <strong>view</strong> or <strong>edit</strong> page, each child (detail) grid is its own grid
object via <code>Runner.detail('&lt;detail_table&gt;')</code> — so you can read the children, the current child
selection, and any child cell, right beside the master's own fields.</p>
<pre><code>// The master's fields …
var orderRef = Runner.getControlValue('reference');

// … and its child rows, in the same breath
var items = Runner.detail('order_items').getAllRecords();       // [{ id, sku, qty, price }, …]
var total = items.reduce(function (s, it) { return s + it.qty * it.price; }, 0);
Runner.setControlValue('total', total);                         // write it back to the master

// Act on the ticked child rows
var picked = Runner.detail('order_items').getSelectedKeys();

// Every detail grid on the page
Runner.details().forEach(function (d) { console.log(d.table, d.grid.count()); });</code></pre>
<table class="help-table">
    <thead><tr><th>Call</th><th>Returns</th></tr></thead>
    <tbody>
        <tr><td><code>Runner.detail(table)</code></td><td>A grid object (same methods as the main grid) scoped to that child grid; <code>.table</code>, <code>.fk</code>, <code>.exists()</code>.</td></tr>
        <tr><td><code>.getAllRecords()</code> / <code>.getSelectedRecords()</code></td><td>All / ticked child records.</td></tr>
        <tr><td><code>.getSelectedKeys()</code> / <code>.count()</code></td><td>Ticked child ids / row count.</td></tr>
        <tr><td><code>.getCellValue(id, field)</code> / <code>.getRow(id)</code></td><td>One child cell / one child row.</td></tr>
        <tr><td><code>Runner.details()</code></td><td>Every detail grid: <code>[{ table, fk, grid }]</code>.</td></tr>
    </tbody>
</table>

<h4>Editing child rows (two-way)</h4>
<p>On an editable detail grid you can write a child cell in place <em>and persist it</em> — the change saves through
the same permission-gated endpoint the app uses everywhere, so it respects each table's edit rights.</p>
<pre><code>var d = Runner.detail('order_items');

// one cell → saved to the database (returns a Promise)
await d.setCellValue(itemId, 'qty', 5);

// several cells on one row, one save
await d.getRow(itemId).setValues({ qty: 3, price: 9.5 });

// recalculate every child and push a new price, then reload
for (const it of d.getAllRecords()) {
    await d.setCellValue(it.id, 'line_total', it.qty * it.price);
}
d.reload();</code></pre>
<table class="help-table">
    <thead><tr><th>Call</th><th>Does</th></tr></thead>
    <tbody>
        <tr><td><code>grid.setCellValue(id, field, value)</code></td><td>Write one cell in place + save. Returns a Promise. (Works on the main grid too.)</td></tr>
        <tr><td><code>row.setCell(field, value)</code></td><td>Same, from a row object.</td></tr>
        <tr><td><code>row.setValues({ … })</code></td><td>Write several cells, one save.</td></tr>
        <tr><td><code>row.set(field, value)</code></td><td>Update the cell in the page only — no save (batch, then <code>row.save()</code>).</td></tr>
        <tr><td><code>row.save([patch])</code></td><td>Persist the whole row (or a patch).</td></tr>
        <tr><td><code>grid.editCell(id, field)</code></td><td>Open that cell's inline editor and focus it for the user (reuses the grid's own edit + autosave). Also <code>Runner.editCell(id, field)</code> on the main grid.</td></tr>
    </tbody>
</table>
<pre><code>// "Add a row, then let the user type" — open the cell editor
Runner.editCell(newId, 'note');
// or on a detail grid
Runner.detail('order_items').editCell(itemId, 'qty');</code></pre>
<div class="help-callout help-callout-warn">
    <i class="bi bi-shield-lock"></i>
    <div>Writes go through <code>api/save.php</code> — the signed-in user must have <strong>edit</strong> rights on
        that child table, and only real columns are saved. Reads work anywhere; writes need a table with real
        records behind it.</div>
</div>

<h3>Read-only pages (View · Print · PDF · Cards)</h3>
<p>There's nothing extra to learn: <code>Runner.getControl('field')</code> works on a <strong>View</strong> (and
print / PDF) page too — with no input to type into, <code>getValue()</code> returns the field's <em>displayed</em>
value. Card views behave like grid rows, so <code>Runner.getAllRecords()</code> reads the cards.</p>
<pre><code>// On a View page — read what's shown
var name = Runner.getControl('full_name').getValue();
var when = Runner.getControl('created_at').getText();</code></pre>
<div class="help-callout help-callout-info">
    <i class="bi bi-check2-circle"></i>
    <div>One API, every page: <strong>add</strong>, <strong>edit</strong>, <strong>view</strong>,
        <strong>print</strong>, <strong>pdf</strong>, <strong>list</strong>, <strong>cards</strong>,
        <strong>custom</strong>, and <strong>Master/Detail</strong>. Read everywhere; write where the page has real
        inputs (add / edit).</div>
</div>

<h2 id="buttons">Runner – page buttons</h2>
<p>Reach a page button by name and hide / show / enable / disable it — or restyle it. A button object acts on
<em>every</em> match, so <code>Runner.button('edit')</code> covers every row's Edit button at once.</p>
<pre><code>Runner.button('save').disable();          // grey out Save
Runner.hideButton('deleteSelected');      // hide the bulk Delete
Runner.enableButton('save');
Runner.button('add').setText('New order');
Runner.button('cancel').addStyle('color:#dc2626');

// inside a Master/Detail child grid
Runner.detail('order_items').button('add').hide();</code></pre>
<table class="help-table">
    <thead><tr><th>Name</th><th>Which button</th></tr></thead>
    <tbody>
        <tr><td><code>add</code></td><td>Add&nbsp;New (list toolbar) or a detail grid's Add.</td></tr>
        <tr><td><code>save</code> · <code>saveNew</code> · <code>saveClose</code></td><td>The Add/Edit form's Save buttons.</td></tr>
        <tr><td><code>cancel</code> · <code>back</code> · <code>reset</code></td><td>The form's Cancel / Back / Reset.</td></tr>
        <tr><td><code>deleteSelected</code> · <code>editSelected</code> · <code>exportSelected</code></td><td>The list's bulk-action buttons.</td></tr>
        <tr><td><code>edit</code> · <code>delete</code> · <code>view</code> · <code>copy</code></td><td>The per-row action buttons (all rows).</td></tr>
    </tbody>
</table>
<table class="help-table">
    <thead><tr><th>Method</th><th>Does</th></tr></thead>
    <tbody>
        <tr><td><code>Runner.button(name)</code></td><td>The button object. <code>.exists()</code>, <code>.count()</code>.</td></tr>
        <tr><td><code>.hide()</code> / <code>.show()</code> / <code>.visible(bool)</code></td><td>Show or hide it.</td></tr>
        <tr><td><code>.disable()</code> / <code>.enable()</code></td><td>Grey it out / restore it (also works on links).</td></tr>
        <tr><td><code>.setText(t)</code> · <code>.addStyle(css)</code> · <code>.addClass(c)</code> · <code>.on(evt, fn)</code></td><td>Relabel, restyle, or wire it.</td></tr>
        <tr><td><code>Runner.hideButton/showButton/disableButton/enableButton(name)</code></td><td>One-line shortcuts.</td></tr>
    </tbody>
</table>
<div class="help-callout help-callout-info">
    <i class="bi bi-diagram-3"></i>
    <div><strong>Master/Detail tip.</strong> A common need: on a child (detail) record's page, hide the master-side
        Add or a bulk button when new children shouldn't be created — <code>Runner.detail('items').button('add').hide()</code>,
        or <code>Runner.hideButton('deleteSelected')</code>.</div>
</div>

<!-- =========================================== -->
<h2 id="ajax">pp.ajax – HTTP Requests</h2>
<p>Thin wrappers around the Fetch API with built‑in JSON handling and a loading spinner helper.</p>

<h3>GET request</h3>
<pre><code>const users = await pp.ajax.get('/api/users');</code></pre>

<h3>POST request (JSON)</h3>
<pre><code>const result = await pp.ajax.post('/api/save', {
    name: 'Alice',
    email: 'alice@example.com'
});
console.log(result.success); // true/false</code></pre>

<h3>File upload with progress</h3>
<pre><code>const formData = new FormData();
formData.append('file', fileInput.files[0]);

const response = await pp.ajax.upload('/api/upload', formData, (percent) => {
    console.log(`Uploading: ${percent}%`);
});</code></pre>

<h3>GET with automatic loading spinner</h3>
<pre><code>const data = await pp.ajax.load('/api/slow-endpoint', 'Fetching records…');
// Shows a SweetAlert2 spinner while loading, then auto‑closes</code></pre>

<h3>Adding custom headers</h3>
<pre><code>const data = await pp.ajax.get('/api/secure', {
    headers: { 'X-CSRF-Token': csrfToken }
});</code></pre>

<!-- =========================================== -->
<h2 id="dialog">pp.dialog – User Dialogs</h2>
<p>Unified SweetAlert2 shortcuts that keep every module's UI consistent.</p>

<h3>Toast notification</h3>
<pre><code>pp.dialog.toast('success', 'Record saved!');
pp.dialog.toast('error', 'Something went wrong.');
pp.dialog.toast('warning', 'Please check your input.');
pp.dialog.toast('info', '3 new notifications.');</code></pre>

<h3>Confirmation dialog</h3>
<pre><code>const confirmed = await pp.dialog.confirm(
    'Delete this record?',
    'This action cannot be undone.'
);
if (confirmed) {
    // proceed with deletion
}</code></pre>

<h3>Custom confirmation buttons</h3>
<pre><code>const yes = await pp.dialog.confirm('Archive?', 'Move to archive.', {
    confirmText: 'Archive',
    cancelText: 'Keep',
    confirmColor: '#0d6efd'
});</code></pre>

<h3>Text prompt</h3>
<pre><code>const folderName = await pp.dialog.prompt('New folder', 'Enter folder name');
if (folderName) {
    // create folder
}</code></pre>

<h3>Simple alert</h3>
<pre><code>await pp.dialog.alert('Done', 'All files processed.', 'success');</code></pre>

<!-- =========================================== -->
<h2 id="form">pp.form – Form Utilities</h2>
<p>Quick value access and simple validation without heavyweight plugins.</p>

<h3>Get / Set a form value</h3>
<pre><code>const email = pp.form.getValue('#email');
pp.form.setValue('#name', 'John Doe');</code></pre>

<h3>Checkbox and radio</h3>
<pre><code>const active = pp.form.getValue('#activeCheckbox');  // true/false
pp.form.setValue('#activeCheckbox', true);

const plan = pp.form.getValue('input[name="plan"]');  // 'basic'
pp.form.setValue('input[name="plan"]', 'premium');</code></pre>

<h3>Validate a field</h3>
<pre><code>const { valid, message } = pp.form.validate('#email', {
    required: true,
    email: true,
    requiredMessage: 'Email is required.'
});
if (!valid) {
    pp.dialog.toast('error', message);
}</code></pre>

<h3>Validate with minlength and pattern</h3>
<pre><code>const check = pp.form.validate('#username', {
    required: true,
    minlength: 3,
    pattern: /^[a-zA-Z0-9_]+$/,
    patternMessage: 'Only letters, numbers, and underscores allowed.'
});</code></pre>

<h3>Serialise a form to an object</h3>
<pre><code>const data = pp.form.toObject('#myForm');
// { name: "Alice", email: "alice@example.com", subscribe: "on" }
await pp.ajax.post('/api/submit', data);</code></pre>

<!-- =========================================== -->
<h2 id="util">pp.util – General Helpers</h2>

<h3>Debounce (search input)</h3>
<pre><code>const doSearch = pp.util.debounce((query) => {
    fetch(`/api/search?q=${query}`).then(res => res.json()).then(renderResults);
}, 400);

document.getElementById('searchBox').addEventListener('input', (e) => {
    doSearch(e.target.value);
});</code></pre>

<h3>Throttle (scroll handler)</h3>
<pre><code>window.addEventListener('scroll', pp.util.throttle(() => {
    console.log('User is scrolling...');
}, 200));</code></pre>

<h3>Format a date</h3>
<pre><code>pp.util.formatDate('2025-06-15');                      // "Jun 15, 2025, 12:00 am"
pp.util.formatDate('2025-06-15', 'F j, Y');            // "June 15, 2025"
pp.util.formatDate('2025-06-15 14:30:00', 'H:i');       // "14:30"</code></pre>

<h3>Deep clone an object</h3>
<pre><code>const original = { a: 1, b: { c: 2 } };
const copy = pp.util.deepClone(original);
copy.b.c = 99;
console.log(original.b.c);  // 2  (unchanged)</code></pre>

<h3>Generate a UUID</h3>
<pre><code>const id = pp.util.uuid();  // "c9b5a2e4-1234-4abc-5678-def012345678"</code></pre>

<h3>Escape HTML</h3>
<pre><code>const safe = pp.util.escapeHtml('&lt;script&gt;alert("xss")&lt;/script&gt;');
// "&amp;lt;script&amp;gt;alert(&amp;quot;xss&amp;quot;)&amp;lt;/script&amp;gt;"</code></pre>

<!-- =========================================== -->
<h2 id="reactive">Reactive fields, page elements &amp; rich dialogs</h2>
<p class="lead">Higher-level helpers that turn common multi-line patterns into one line. They pair
especially well with a field's <em>On&nbsp;Change</em>, a page's <em>On&nbsp;Load</em>, and
<a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/component_custom_button">Custom&nbsp;Button</a> handlers.</p>

<h3>Live calculated fields — <code>Runner.compute()</code></h3>
<p>Keep a field in sync with others. The function receives each dependency's value and returns the target's
value; it re-runs the instant any dependency changes — no event wiring, no boilerplate.</p>
<pre><code>// line_total = qty × price, always up to date
Runner.compute('line_total', ['qty', 'price'], function (qty, price) {
    return (+qty || 0) * (+price || 0);
});

// A grand total that follows several inputs
Runner.compute('grand_total', ['subtotal', 'tax', 'shipping'], (s, t, sh) =>
    (+s || 0) + (+t || 0) + (+sh || 0));</code></pre>

<h3>Mirror one field into another — <code>Runner.link()</code></h3>
<pre><code>// Auto-slug a title as the user types
Runner.link('title', 'slug', v => v.toLowerCase().trim().replace(/\s+/g, '-'));

// Copy without transforming
Runner.link('billing_city', 'shipping_city');</code></pre>

<h3>Validate a field on the spot</h3>
<pre><code>var ok = Runner.getControl('email').validate({ email: true });
// shows the inline message + marks the field; returns false if invalid
if (!ok) return false;               // e.g. in Before Form Submit / Custom Button Client Before

Runner.getControl('name').reset();   // put a field back to its load-time value</code></pre>

<h3>Show / hide any element by id — <code>Runner.item()</code></h3>
<p>For headings, panels or custom HTML blocks you gave an <code>id</code> (PHPRunner's <em>findItem/toggleItem</em>).</p>
<pre><code>// Reveal a legal notice only for US customers
Runner.getControl('country').watch(function (c) {
    Runner.item('us_legal').toggle(c === 'US');
});

Runner.item('summary').html('<b>' + Runner.getControlValue('name') + '</b>');</code></pre>

<h3>Drive tabs — <code>Runner.getTabs()</code></h3>
<pre><code>Runner.getTabs().activate(2);                 // open the third tab of the first group
Runner.getTabs(0).activateByText('Billing');  // …or by its label</code></pre>

<h3>Warn about unsaved changes — <code>Runner.setPageModified()</code></h3>
<pre><code>// Arm the browser's "leave page?" prompt when anything changes
Runner.fields().forEach(function (f) {
    Runner.getControl(f).watch(() => Runner.setPageModified(true));
});
// …then disarm after a successful save:
Runner.setPageModified(false);</code></pre>

<h3>Grid maths &amp; selection in one call</h3>
<p>On a list page use <code>Runner.grid()</code>; on a Master/Detail child grid use <code>Runner.detail('table')</code> —
both expose the same conveniences.</p>
<pre><code>var d = Runner.detail('order_items');
d.sum('line_total');                    // total a column — no loop
d.avg('price');                         // average
d.pluck('sku');                         // ['A-1','A-2', …]
d.filter(r => r.status === 'open');     // records matching a predicate
d.find(r => r.sku === 'A-1');           // first match (or null)
d.each(row => row.highlight());         // iterate row objects

d.selectWhere(r => r.overdue);          // tick rows by predicate
d.selectAll();  d.clearSelection();     // tick all / clear</code></pre>

<h3>Per-row messages</h3>
<pre><code>var row = Runner.grid().getRow(id);
row.setMessage('qty', 'Above available stock', 'error');   // inline, in that cell
row.clearMessage('qty');</code></pre>

<h3>Collect several values at once — <code>pp.dialog.form()</code></h3>
<p>A multi-field dialog (PHPRunner's <code>ctrl.dialog({fields})</code>). Resolves to <code>{ name: value }</code>
on OK, or <code>null</code> on Cancel. Perfect just before a Custom Button's server stage.</p>
<pre><code>var v = await pp.dialog.form({
    title: 'Email the results',
    header: 'Where should we send it?',
    fields: [
        { name: 'email',   label: 'Email address', type: 'email', value: 'me@example.com' },
        { name: 'subject', label: 'Subject',       value: 'Your results' },
        { name: 'format',  label: 'Format', options: ['PDF', 'CSV'] },
        { name: 'copyme',  label: 'Send me a copy', type: 'checkbox', value: true },
    ],
    ok: 'Send',
});
if (!v) return false;             // user cancelled
params.email = v.email;           // …hand it to the Server stage
params.subject = v.subject;</code></pre>

<h3 id="search">Search the list — <code>Runner.search()</code></h3>
<p>Drive the list page's Quick Search from code, or filter the loaded rows instantly with no server round-trip.</p>
<pre><code>var s = Runner.search();          // also Runner.getSearchController()
s.submit('ada');                  // server-side search (navigates with ?search=…)
s.getValue();  s.isSearched();    // read the box / is the list filtered?
s.clear();                        // back to the full, unfiltered list

// Instant client-side filter — hide non-matching rows, no fetch:
Runner.getControl('quickfind').watch(v => Runner.search().filterRows(v, ['name','email']));
Runner.search().clearFilter();

// Additional WHERE tabs:
Runner.search().whereTab(1);      // switch to a filter tab; allData() = All data
Runner.search().whereTabs();      // [{ index, label, active }]</code></pre>

<h3>Let the server drive the page — <code>Runner.apply()</code></h3>
<p>When PHP uses the <a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/pp_server_api"><code>pp()</code></a>
command bus (<code>pp()->toast()->setControl()->reloadGrid()</code>), those commands are applied in the browser by
<code>Runner.apply()</code> — automatically after <code>pp.run()</code> and a Custom&nbsp;Button's server stage, so you
usually write <em>no</em> client-after code. You can also apply a command list yourself:</p>
<pre><code>Runner.apply([
    { cmd: 'setControl', field: 'status', value: 'approved' },
    { cmd: 'toast', type: 'success', message: 'Done' },
    { cmd: 'reloadGrid' },
]);</code></pre>

<!-- =========================================== -->
<h2 id="cookbook">Cookbook — basic to advanced</h2>
<p class="lead">A guided run of copy-ready recipes. They live in event code — a
<a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/component_custom_button">Custom&nbsp;Button</a>,
a field's <em>On-Change</em>, or a page's <em>On-Load</em> — and every one of them autocompletes in the Events editor.</p>

<h3 style="color:var(--pp-muted)">Basics</h3>

<h4>1 · Make a field red on the View page</h4>
<p>The classic first example — highlight a value so it can't be missed.</p>
<pre><code>var ctrl = Runner.getControl('status');
ctrl.addStyle('color: red; font-weight: 700');</code></pre>

<h4>2 · Read and write a field</h4>
<pre><code>var email = Runner.getControlValue('email');        // read
Runner.setControlValue('username', email.split('@')[0]);   // write (fires change)</code></pre>

<h4>3 · Ask for confirmation before saving</h4>
<p>In a Custom Button's <strong>Client&nbsp;Before</strong>, or a form's before-submit event:</p>
<pre><code>if (!await pp.dialog.confirm('Submit this order?', 'You can’t edit it afterwards.')) {
    return false;   // cancels the action / stops the submit
}</code></pre>

<h4>4 · Toast instead of alert</h4>
<pre><code>pp.dialog.toast('success', 'Saved.');
pp.dialog.toast('warning', 'Nothing selected.');</code></pre>

<h3 style="color:var(--pp-muted)">Everyday</h3>

<h4>5 · Show or hide a field based on another</h4>
<p>Put this in the <em>type</em> field's On-Change event so a VAT box only shows for companies:</p>
<pre><code>Runner.getControl('type').onChange(function () {
    Runner.getControl('vat_number').visible(Runner.getControlValue('type') === 'company');
});</code></pre>

<h4>6 · Calculate a total on the fly</h4>
<pre><code>function recalc() {
    var qty   = +Runner.getControlValue('qty')   || 0;
    var price = +Runner.getControlValue('price') || 0;
    Runner.setControlValue('total', (qty * price).toFixed(2));
}
Runner.getControl('qty').onChange(recalc);
Runner.getControl('price').onChange(recalc);</code></pre>

<h4>7 · Enable / disable a button</h4>
<pre><code>Runner.disableButton('save');                         // grey out Save
Runner.getControl('agree').onChange(function () {     // enable it only when a box is ticked
    Runner.button('save')[Runner.getControlValue('agree') ? 'enable' : 'disable']();
});</code></pre>

<h4>8 · Hide the bulk “Delete selected” / “Edit selected” buttons</h4>
<pre><code>Runner.hideButton('deleteSelected');
Runner.hideButton('editSelected');</code></pre>

<h4>9 · Change the font / size of a control</h4>
<pre><code>Runner.getControl('notes').addStyle('font-size: 16px; font-family: Georgia, serif');</code></pre>

<h4>10 · Validate before submit, and message the field</h4>
<pre><code>var email = Runner.getControlValue('email');
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    Runner.getControl('email').setMessage('Enter a valid email').focus();
    return false;
}
Runner.getControl('email').clearMessage();</code></pre>

<h3 style="color:var(--pp-muted)">Advanced</h3>

<h4>11 · Autofill from the server (AJAX)</h4>
<p>Look a value up on the server and drop the answer into another field — no page reload:</p>
<pre><code>Runner.getControl('email').onChange(async function () {
    var contact = await pp.run('findContact', { email: Runner.getControlValue('email') });
    if (contact) Runner.setData({ full_name: contact.name, phone: contact.phone });
});
// (server side: define action_findContact($params) in Custom Functions → PHP)</code></pre>

<h4>12 · Bulk action on the selected list rows</h4>
<pre><code>var ids = Runner.getSelectedKeys();
if (!ids.length) { pp.dialog.toast('info', 'Select at least one row.'); return false; }
await pp.run('markShipped', { ids: ids });
Runner.reloadGrid();</code></pre>

<h4>13 · Master/Detail — total the child rows onto the master</h4>
<pre><code>var items = Runner.detail('order_items').getAllRecords();
var total = items.reduce(function (s, it) { return s + (+it.qty) * (+it.price); }, 0);
Runner.setControlValue('total', total);</code></pre>

<h4>14 · Master/Detail — hide Add when the record is locked</h4>
<p>On the master's Edit page (a page On-Load event), stop new children being added to a closed order:</p>
<pre><code>if (Runner.getControlValue('status') === 'closed') {
    Runner.detail('order_items').button('add').hide();
    Runner.hideButton('deleteSelected');
}</code></pre>

<h4>15 · Edit a child cell, and save it</h4>
<pre><code>var d = Runner.detail('order_items');
await d.setCellValue(itemId, 'qty', 5);          // write + persist one cell
await d.getRow(itemId).setValues({ qty: 3, price: 9.5 });   // several, one save
Runner.editCell(newRowId, 'sku');                // or open the cell editor for the user</code></pre>

<h4>16 · Refresh the List page after editing in a popup</h4>
<pre><code>// In the popup's after-save event (or a Custom Button's Client After):
if (window.parent && window.parent !== window) window.parent.location.reload();
else Runner.reloadGrid();</code></pre>

<h3 style="color:var(--pp-muted)">Going further</h3>

<h4>17 · Focus the first field when an Add form opens</h4>
<p>A small polish that saves the user a click. Put it in the page's On-Load event.</p>
<pre><code>if (Runner.getPageMode() === 'add') {
    Runner.getControl(Runner.fields()[0]).focus();   // first field on the page
}</code></pre>

<h4>18 · Format a currency field as the user types</h4>
<p><code>on('input', …)</code> reacts to every keystroke; <code>pp.util</code> keeps it tidy.</p>
<pre><code>Runner.getControl('amount').on('blur', function () {
    var n = parseFloat(String(Runner.getControlValue('amount')).replace(/[^0-9.]/g, '')) || 0;
    Runner.setControlValue('amount', n.toFixed(2));
});</code></pre>

<h4>19 · Cascading lookups — filter a child list by the parent</h4>
<p>When <em>country</em> changes, reload <em>city</em> options from the server (via a named action).</p>
<pre><code>Runner.getControl('country').onChange(async function () {
    var cities = await pp.run('citiesFor', { country: Runner.getControlValue('country') });
    var sel = Runner.getControl('city').el;   // the &lt;select&gt; element
    if (sel) sel.innerHTML = cities.map(function (c) { return '&lt;option&gt;' + pp.util.escapeHtml(c) + '&lt;/option&gt;'; }).join('');
});</code></pre>

<h4>20 · Confirm a delete showing the record's name</h4>
<p>Attach to the row Delete buttons and read the row's own record for a friendlier prompt.</p>
<pre><code>Runner.rows().forEach(function (row) {
    row.tr.querySelector('.ppd-action-delete')?.addEventListener('click', async function (e) {
        e.preventDefault(); e.stopPropagation();
        if (await pp.dialog.confirm('Delete “' + row.get('name') + '”?', 'This cannot be undone.')) {
            await pp.run('deleteRecord', { id: row.id }); Runner.reloadGrid();
        }
    }, true);
});</code></pre>

<h4>21 · Block save when a detail grid is empty</h4>
<p>In a Custom Button's Client Before, or a before-submit event on the master:</p>
<pre><code>if (Runner.detail('order_items').count() === 0) {
    pp.dialog.toast('warning', 'Add at least one line item first.');
    return false;
}</code></pre>

<h4>22 · Sum a grid column and show it</h4>
<pre><code>var total = Runner.getAllRecords().reduce(function (s, r) { return s + (+r.amount || 0); }, 0);
pp.dialog.toast('info', 'Total on this page: ' + total.toFixed(2));</code></pre>

<h4>23 · Highlight grid rows that match a condition</h4>
<pre><code>Runner.rows().forEach(function (row) {
    if ((+row.get('balance') || 0) &lt; 0) row.highlight('rgba(220,38,38,.12)');
});</code></pre>

<h4>24 · Live server-side uniqueness check</h4>
<p>Debounced so it only asks the server when the user pauses typing.</p>
<pre><code>var check = pp.util.debounce(async function () {
    var ok = await pp.run('emailAvailable', { email: Runner.getControlValue('email') });
    Runner.getControl('email').setMessage(ok ? '' : 'Already registered', ok ? 'success' : 'error');
}, 400);
Runner.getControl('email').on('input', check);</code></pre>

<h4>25 · Copy a master value into every child row, then save</h4>
<pre><code>var currency = Runner.getControlValue('currency');
var d = Runner.detail('order_items');
for (var it of d.getAllRecords()) { await d.setCellValue(it.id, 'currency', currency); }
d.reload();</code></pre>

<h4>26 · Warn before leaving with unsaved changes</h4>
<pre><code>var dirty = false;
Runner.fields().forEach(function (f) { Runner.getControl(f).on('input', function () { dirty = true; }); });
window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } });</code></pre>

<h4>27 · Save with Ctrl/Cmd + S</h4>
<pre><code>document.addEventListener('keydown', function (e) {
    if ((e.ctrlKey || e.metaKey) &amp;&amp; e.key.toLowerCase() === 's') {
        e.preventDefault(); Runner.button('save').els[0]?.click();
    }
});</code></pre>

<h4>28 · Read the current record / URL context</h4>
<pre><code>var id   = pp.page.id;          // the record id from the URL
var mode = Runner.getPageMode();  // 'add' | 'edit' | 'view' | 'custom'
var rec  = pp.record();          // the whole record on view/edit (may be null)</code></pre>

<h4>29 · Lock the whole form to read-only</h4>
<pre><code>Runner.fields().forEach(function (f) { Runner.getControl(f).readonly(true); });
Runner.button('save').hide();</code></pre>

<h4>30 · Poll a value and update a field live</h4>
<pre><code>setInterval(async function () {
    var n = await pp.run('pendingCount', {});
    Runner.getControl('pending').setValue(n);
}, 15000);   // every 15s</code></pre>

<!-- =========================================== -->
<h2 id="examples">More real‑world examples</h2>

<h3>Example 1 – Save form with loading spinner</h3>
<pre><code>async function saveRecord() {
    const data = pp.form.toObject('#recordForm');
    const result = await pp.ajax.load('/api/save', 'Saving…', {
        method: 'POST',
        body: JSON.stringify(data)
    });
    if (result.success) {
        pp.dialog.toast('success', 'Record saved!');
    } else {
        pp.dialog.toast('error', result.message);
    }
}</code></pre>

<h3>Example 2 – Delete with confirmation</h3>
<pre><code>async function deleteRecord(id) {
    const ok = await pp.dialog.confirm('Delete?', 'Record #' + id + ' will be permanently removed.');
    if (!ok) return;

    const result = await pp.ajax.post('/api/delete', { id });
    if (result.success) {
        pp.dialog.toast('success', 'Deleted.');
    }
}</code></pre>

<h3>Example 3 – Live search with debounce</h3>
<pre><code>const search = pp.util.debounce(async (query) => {
    const results = await pp.ajax.get(`/api/search?q=${encodeURIComponent(query)}`);
    renderResults(results);
}, 300);

document.getElementById('searchInput').addEventListener('input', e => search(e.target.value));</code></pre>

<h3>Example 4 – Form validation before submit</h3>
<pre><code>document.getElementById('saveBtn').addEventListener('click', async () => {
    const emailCheck = pp.form.validate('#email', { required: true, email: true });
    const nameCheck  = pp.form.validate('#name', { required: true, minlength: 2 });

    if (!emailCheck.valid) return pp.dialog.toast('error', emailCheck.message);
    if (!nameCheck.valid)  return pp.dialog.toast('error', nameCheck.message);

    // All good – submit
    await pp.ajax.post('/api/save', pp.form.toObject('#myForm'));
    pp.dialog.toast('success', 'Saved!');
});</code></pre>

<hr>
<h3>🔗 Related Help Topics</h3>
<p>
    <a href="DB" class="btn btn-sm btn-outline-primary me-1">Database API</a>
    <a href="Security" class="btn btn-sm btn-outline-primary me-1">Security API</a>
    <a href="Search" class="btn btn-sm btn-outline-primary">Search API</a>
</p>