Skip to content

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.

blade
{{ 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.

blade
<?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() ?>
blade
<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().

blade
<?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
<?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.

blade
{{ 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 ​

ArgumentDescription
labelthe title in the header, escaped.
commentthe text under the title, escaped unless commentHtml is set.
successsuccess styling. Default: false.
warningwarning styling. Default: false.
dangerdanger styling. Default: false.
iconicon classes, or false for no icon. Default: the type's icon.
commentHtmlprints 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:

CallDescription
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.

blade
<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.

blade
<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.

blade
<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>
ClassDescription
loading-indicator-containerpositions the container, so the indicator can cover it.
loading-indicatorthe indicator, with a <span> for the spinner and an optional <div> of text.
size-smalla smaller spinner.
indicator-centercenters the spinner.
is-opaquecovers 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.

blade
<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>
OptionDescription
texta line of text under the spinner.
opaquecovers the content with a solid background.
centeredcenters the spinner.
sizesmall 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.

blade
<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>
AttributeDescription
data-change-monitorenables the control on a form.
data-change-monitor-commiton 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:

EventDescription
change-monitor:unchangemarks the form saved.
change-monitor:pausestops watching for changes.
change-monitor:resumestarts watching again.
change-monitor:pause-unloadstops the warning before leaving the page.
change-monitor:resume-unloadturns the warning back on.

jax.changeMonitor.disable() turns every change monitor on the page off, and jax.changeMonitor.enable() turns them back on.