return function () {
$self = url('/@object/@action/' . urlencode('htmx/inline_edit'));
$key = 'docs_demo_htmx_rows';
// The rows live in the visitor's session (five sample products), never in a table
if ( ! isset($_SESSION[$key]) || ! is_array($_SESSION[$key]) || (isset($_POST['reset']) && _class('csrf_guard')->check())) {
$_SESSION[$key] = [];
foreach (array_slice(_class('sample_products', YF_PATH . '.dev/samples/classes/')->load(), 0, 5, true) as $id => $row) {
$_SESSION[$key][(int) $id] = ['name' => $row['name'], 'price' => $row['price']];
}
}
$rows = &$_SESSION[$key];
$id = (int) ($_GET['row'] ?? 0);
$error = '';
$show_row = function ($id, $row) use ($self) {
return '<tr id="row-' . $id . '"><td>' . $id . '</td><td>' . _prepare_html($row['name'], 0) . '</td><td>' . _prepare_html($row['price'], 0) . '</td>'
. '<td><button type="button" class="btn btn-default btn-outline-secondary btn-sm" hx-get="' . $self . '?row=' . $id . '&edit=1">Edit</button></td></tr>';
};
$edit_row = function ($id, $row, $error = '') use ($self) {
return '<tr id="row-' . $id . '" class="docs-demo-editing"><td>' . $id . '</td>'
. '<td><input name="name" class="form-control input-sm form-control-sm" maxlength="60" value="' . _prepare_html($row['name'], 0) . '">'
. ($error !== '' ? '<div class="text-danger small">' . _prepare_html($error, 0) . '</div>' : '') . '</td>'
. '<td><input name="price" class="form-control input-sm form-control-sm" inputmode="decimal" value="' . _prepare_html($row['price'], 0) . '"></td>'
. '<td class="text-nowrap"><button type="button" class="btn btn-primary btn-sm" hx-post="' . $self . '?row=' . $id . '" hx-include="closest tr">Save</button> '
. '<button type="button" class="btn btn-default btn-outline-secondary btn-sm" hx-get="' . $self . '?row=' . $id . '">Cancel</button></td></tr>';
};
$htmx = _class('htmx');
// One URL, two answers (fragment or page): keep caches from mixing them up
header('Vary: HX-Request, HX-History-Restore-Request');
if ($htmx->_wants_fragment() && isset($rows[$id])) {
if (is_post()) {
// the session token comes in the X-CSRF-Token header (hx-headers on the table)
if ( ! _class('csrf_guard')->check()) {
$error = 'The CSRF token is missing or wrong: reload the page.';
} else {
$name = trim(preg_replace('~\s+~u', ' ', (is_string($_POST['name'] ?? null) ? $_POST['name'] : '')));
$price = str_replace(',', '.', trim((is_string($_POST['price'] ?? null) ? $_POST['price'] : '')));
if ($name === '' || mb_strlen($name) > 60) {
$error = 'Name: 1 to 60 characters.';
} elseif ( ! preg_match('~^\d{1,4}(\.\d{1,2})?$~', $price)) {
$error = 'Price: a number from 0 to 9999.99.';
} else {
$rows[$id] = ['name' => $name, 'price' => number_format((float) $price, 2, '.', '')];
return $htmx->_result($show_row($id, $rows[$id]));
}
}
$posted = ['name' => (is_string($_POST['name'] ?? null) ? $_POST['name'] : ''), 'price' => (is_string($_POST['price'] ?? null) ? $_POST['price'] : '')];
return $htmx->_result($edit_row($id, $posted, $error));
}
return $htmx->_result(isset($_GET['edit']) ? $edit_row($id, $rows[$id]) : $show_row($id, $rows[$id]));
}
asset('htmx');
$intro = '<p class="docs-demo-intro">Inline editing with htmx: <b>Edit</b> swaps the row for a form row (<code>hx-get</code>),
<b>Save</b> posts it (<code>hx-post</code> with <code>hx-include="closest tr"</code>) and gets the updated row back, or the form row
with an error. The values are validated on the server and kept in your session only. The POST carries the session CSRF token in
the <code>X-CSRF-Token</code> header (<code>hx-headers</code>), checked with <code>_class(\'csrf_guard\')->check()</code>.
Classes: <code>yf_htmx</code>, <code>yf_csrf_guard</code>.</p>';
$body = '';
foreach ($rows as $row_id => $row) {
$body .= $show_row($row_id, $row);
}
$headers = htmlspecialchars(json_encode(['X-CSRF-Token' => _class('csrf_guard')->session_token()]), ENT_QUOTES);
return $intro
. '<table class="table table-bordered table-condensed table-sm docs-demo-inline" hx-target="closest tr" hx-swap="outerHTML" hx-headers="' . $headers . '">'
. '<thead><tr><th>Id</th><th>Name</th><th>Price</th><th></th></tr></thead><tbody>' . $body . '</tbody></table>'
. '<form method="post" action="' . $self . '"><input type="hidden" name="reset" value="1">'
. '<input type="hidden" name="_token" value="' . _class('csrf_guard')->session_token() . '">'
. '<button type="submit" class="btn btn-default btn-outline-secondary btn-sm">Reset the rows</button></form>';
};