Feedback
Messages and figures for the reader, plus the states that controls add while a request runs or a form changes: callouts from Ui::callout(), the scoreboard element, the loader container, the load indicator and the change monitor.
Callout types
Ui::callout() renders a highlighted message block. Its header shows a label as the title and a comment under it, both escaped. Info is the default type, and success: true, warning: true or danger: true pick the others. Each type shows its icon from the icon pack: the callout.info, callout.success, callout.warning and callout.danger keys.
{{ Ui::callout(label: 'Heads up', comment: 'Info is the default type.') }}
{{ Ui::callout(label: 'Saved', comment: 'Your changes are live.', success: true) }}
{{ Ui::callout(label: 'Trial ending', comment: 'Your trial ends in 3 days.', warning: true) }}
{{ Ui::callout(label: 'Payment failed', comment: 'We could not charge your card.', danger: true) }}Used inline like this, with no body, the call prints the callout straight away with {{ }}.
Callout body
A callout can hold any markup below its header. Open it with ->slot(), which captures everything printed after it as the body, up to Ui::end(), which renders the callout. Because the body is captured rather than returned, the opening and closing calls go in <?php ?> tags. The body is plain Blade, so it can hold any markup, print variables or include a view.
<?php Ui::callout(label: 'Release notes', comment: 'Version 2.1 is out.')->slot() ?>
<p>
Reports now load twice as fast, and exports keep your column order.
</p>
<?php Ui::end() ?>
<?php Ui::callout(label: 'Setup complete', success: true)->slot() ?>
@include('reference.ui._callout_checklist')
<?php Ui::end() ?>
<?php Ui::callout(warning: true)->slot() ?>
<p>
With a body and no label or comment, the callout has no header and no icon.
</p>
<?php Ui::end() ?><ul>
<li>
Your workspace is ready.
</li>
<li>
Two teammates accepted their invites.
</li>
<li>
The first backup runs tonight.
</li>
</ul>With a body and no label or comment, the callout has no header, and so no icon.
Callout actions
Ui::slot('action') switches the capture to the action area, which sits on the right of the header, such as a button that fixes what the callout is about. Call it after the body, before Ui::end(). When the callout has an action and no body, open it with ->slot('action') in place of ->slot().
<?php Ui::callout(label: 'Trial ending', comment: 'Your trial ends in 3 days.', warning: true)->slot() ?>
<p>
Add a payment method to keep your projects. Everything you made during the trial stays.
</p>
<?php Ui::slot('action') ?>
{{ Ui::ajaxButton(label: 'Add a card', handler: 'onCalloutAction', primary: true, dataRequestFlash: true) }}
<?php Ui::end() ?>
<?php Ui::callout(label: 'Card expired', comment: 'Update your payment method to keep your plan.', danger: true)->slot('action') ?>
{{ Ui::ajaxButton(label: 'Update', handler: 'onCalloutAction', danger: true, dataRequestFlash: true) }}
<?php Ui::end() ?><?php
namespace App\Controllers\Reference\UiController;
use Illuminate\Support\Sleep;
use Larajax\Classes\AjaxResponse;
/**
* FeedbackHandlers holds the AJAX handlers behind the Feedback page.
*/
trait FeedbackHandlers
{
/**
* onCalloutAction handles the buttons inside the callouts.
*/
public function onCalloutAction(): AjaxResponse
{
return ajax()->flash('info', 'Payment details are not part of this demo.');
}
/**
* onSlowRequest sleeps briefly so the loader container and the loading button have time to show.
*/
public function onSlowRequest(): AjaxResponse
{
Sleep::for(1500)->milliseconds();
return ajax()->flash('success', 'The slow request finished.');
}
/**
* onSaveProfile stands in for a save, as the handler for the change monitor form.
*/
public function onSaveProfile(): AjaxResponse
{
return ajax()->flash('success', 'Profile saved.');
}
}Callout icons
icon swaps the type's icon for any icon classes, and icon: false leaves the icon out, so the text starts at the left edge.
{{ Ui::callout(label: 'New dashboard', comment: 'Try the redesigned reports.', icon: 'ph ph-rocket-launch') }}
{{ Ui::callout(label: 'No icon', comment: 'Pass false to leave the icon out.', warning: true, icon: false) }}Callout arguments
| Argument | Description |
|---|---|
| label | the title in the header, escaped. |
| comment | the text under the title, escaped unless commentHtml is set. |
| success | success styling. Default: false. |
| warning | warning styling. Default: false. |
| danger | danger styling. Default: false. |
| icon | icon classes, or false for no icon. Default: the type's icon. |
| commentHtml | prints the comment as HTML, unescaped. Use it only with markup you trust. Default: false. |
Any other argument becomes an attribute on the callout, with class merged into its classes. Add class: 'is-flush' to drop the bottom margin and corners, so the callout sits on top of another panel.
The callout's capture calls:
| Call | Description |
|---|---|
| Ui::callout(...)->slot() | opens the callout and captures what follows as its body. |
| Ui::callout(...)->slot('action') | opens the callout and captures what follows as its action area. |
| Ui::slot('action') | switches the capture of the open callout to its action area. |
| Ui::end() | ends the capture and prints the callout. |
The form's hint field renders the same callout markup, from YAML (see Field Types).
Scoreboard
The scoreboard element lays out key figures in a row. Each scoreboard-item title-value holds a heading, the figure and a description line. A positive or negative class on the figure adds an up or down trend arrow, and the figure can hold an icon.
<div class="scoreboard">
<div data-control="toolbar">
<div class="scoreboard-item title-value">
<h4>
Orders
</h4>
<p>
1,284
</p>
<p class="description">
this month
</p>
</div>
<div class="scoreboard-item title-value">
<h4>
Revenue
</h4>
<p class="positive">
$48,210
</p>
<p class="description">
previous month: $41,950
</p>
</div>
<div class="scoreboard-item title-value">
<h4>
Signups
</h4>
<p class="negative">
31
</p>
<p class="description">
previous month: 42
</p>
</div>
<div class="scoreboard-item title-value">
<h4>
Top seller
</h4>
<p>
<i class="ph ph-star"></i>
Desk lamp
</p>
<p class="description">
312 sold
</p>
</div>
</div>
</div>The items sit inside a data-control="toolbar" element, so the row scrolls sideways by dragging when it runs out of room, as a toolbar does.
Loader container
data-control="loader-container" dims an element and ignores clicks on it while a request sent from inside it runs, so the visitor cannot start a second action until the first one finishes. The container has an is-loading class while the request runs. The handler here sleeps for a moment so you can see it.
<div class="d-flex flex-wrap gap-3" data-control="loader-container">
{{ Ui::ajaxButton(
label: 'Run a slow request',
handler: 'onSlowRequest',
primary: true,
dataRequestFlash: true
) }}
{{ Ui::button(label: 'Blocked while loading', secondary: true) }}
</div>Load indicator
A loading-indicator element inside a loading-indicator-container covers the container with a spinner and an optional line of text in a <div>. Add size-small for a smaller spinner, or indicator-center to center it. Put the indicator first in the container, and remove it when the work is done.
<div class="ui-field-grid">
<div class="loading-indicator-container ui-load-box">
<div class="loading-indicator">
<span></span>
<div>
Loading...
</div>
</div>
</div>
<div class="loading-indicator-container ui-load-box">
<div class="loading-indicator size-small">
<span></span>
<div>
Small
</div>
</div>
</div>
<div class="loading-indicator-container ui-load-box">
<div class="loading-indicator indicator-center">
<span></span>
</div>
</div>
</div>| Class | Description |
|---|---|
| loading-indicator-container | positions the container, so the indicator can cover it. |
| loading-indicator | the indicator, with a <span> for the spinner and an optional <div> of text. |
| size-small | a smaller spinner. |
| indicator-center | centers the spinner. |
| is-opaque | covers the content with a solid background, in place of a see-through one. |
Load indicator from JavaScript
The LoadIndicator class adds and removes the indicator for you. LoadIndicator.show(element, options) places it in the element and adds an in-progress class, and LoadIndicator.hide(element) removes it. Import it from the library's index.js, at the same published address that ui.js loads it from, so the page shares one copy.
<div id="uiLoadTarget" class="loading-indicator-container ui-load-box mb-4">
<p class="mb-0">
The indicator covers this box while it shows.
</p>
</div>
<button type="button" class="btn-solid theme-secondary" id="uiLoadButton">
Show for two seconds
</button>
<script type="module">
import { LoadIndicator } from '{{ asset('vendor/larajax/ui/index.js') }}';
document.getElementById('uiLoadButton').addEventListener('click', () => {
const box = document.getElementById('uiLoadTarget');
LoadIndicator.show(box, { text: 'Saving...', opaque: true });
setTimeout(() => LoadIndicator.hide(box), 2000);
});
</script>| Option | Description |
|---|---|
| text | a line of text under the spinner. |
| opaque | covers the content with a solid background. |
| centered | centers the spinner. |
| size | small for a smaller spinner. |
Change monitor
data-change-monitor on a form watches its fields. As soon as one changes, it adds the oc-data-changed class to the form, and the browser warns before the visitor leaves the page with unsaved changes. Here the class switches the badge from saved to unsaved.
A button with dataChangeMonitorCommit, which renders data-change-monitor-commit, marks the form saved: when clicked, and again when its request succeeds. In text fields only keys that change the value count, so moving the cursor with the arrow keys does not.
<form data-change-monitor class="ui-narrow">
<p class="ui-change-status">
<span class="badge is-saved">
<i class="ph ph-check"></i>
No unsaved changes
</span>
<span class="badge theme-warning is-changed">
<i class="ph ph-pencil-simple"></i>
Unsaved changes
</span>
</p>
<div class="form-field mb-4">
<label class="form-label" for="uiProfileName">
Display name
</label>
<input type="text" class="form-control" id="uiProfileName" name="display_name" value="Ada Lovelace">
</div>
<div class="form-field mb-4">
<input type="checkbox" class="check" id="uiProfilePublic" name="is_public" value="1" checked>
<label for="uiProfilePublic">
Show my profile publicly
</label>
</div>
{{ Ui::ajaxButton(
label: 'Save',
handler: 'onSaveProfile',
primary: true,
hotkey: ['ctrl+s', 'cmd+s'],
dataRequestFlash: true,
dataChangeMonitorCommit: true
) }}
</form>| Attribute | Description |
|---|---|
| data-change-monitor | enables the control on a form. |
| data-change-monitor-commit | on an element inside the form, marks the form saved when clicked and when its request succeeds. |
The form dispatches change-monitor:ready when the control starts, change-monitor:changed when the first change is made, and change-monitor:unchanged when it is marked saved. Dispatch these on the form to steer it:
| Event | Description |
|---|---|
| change-monitor:unchange | marks the form saved. |
| change-monitor:pause | stops watching for changes. |
| change-monitor:resume | starts watching again. |
| change-monitor:pause-unload | stops the warning before leaving the page. |
| change-monitor:resume-unload | turns the warning back on. |
jax.changeMonitor.disable() turns every change monitor on the page off, and jax.changeMonitor.enable() turns them back on.