Skip to content

Installation ​

Larajax UI renders forms, lists and filters from YAML configuration in any Laravel application. This page installs it and wires it into a layout and a controller, using the demo app's own files as the example.

Requirements ​

  • PHP 8.2 or higher
  • Laravel 12 or higher
  • october/rain, which provides the database, validation and HTML helpers the library builds on. Composer installs it with the library.
  • Larajax, which provides the view component interface the widgets implement and the window.jax browser API their controls register with. Composer installs it with the library; follow its installation guide for its own assets and setup.
  • Bootstrap 6. The library's styles layer over Bootstrap's, and its menus use Bootstrap's data API. Load Bootstrap's stylesheet and JavaScript module in your layout before the library's assets (see Bootstrap 6 below).

Larajax UI runs against a plain Laravel install, with no CMS. File uploads also need a file model and two database tables, described in file attachments.

Bootstrap 6 ​

The library targets Bootstrap 6.0.0-alpha.1. Bootstrap 6 is still an alpha release, so its class names and tokens may change before the stable version, and a few things follow from it:

  • Modern browsers only. Bootstrap 6 needs Chrome or Edge 130, Firefox 132 or Safari 18 and newer, with no fallbacks below that.
  • ES modules only. Bootstrap's JavaScript loads with <script type="module"> or an import in your bundle. There is no window.bootstrap global, and the library does not need one.
  • The library owns its popups. A popup is the library's own native <dialog>, styled with Bootstrap's dialog-* classes, so it needs no Bootstrap JavaScript. Menus open through Bootstrap's data-bs-toggle="menu" data API, which the Bootstrap module provides.
  • Set the colour mode on <html>. Bootstrap follows the operating system's light or dark preference when no data-bs-theme attribute is set, but the library's palette follows the attribute. Set data-bs-theme="light" or data-bs-theme="dark" on <html> before the page paints, from a saved choice or the system preference, as the demo app's layout does below and as in Bootstrap's color modes guide.

Install the package ​

Require the package with Composer:

bash
composer require larajax/ui

The package registers its UiServiceProvider through Laravel's package discovery, so there is no provider to add by hand.

Publish the assets ​

The widgets load a small CSS and JS bundle in the browser. It is plain CSS and ES modules, with no build step. The package's resources/assets directory is the browser-ready root, and publishing copies it as it is, so the published paths always match the source paths. Publish them once after installing, and again after upgrading:

bash
php artisan vendor:publish --tag=larajax-ui-assets

The files are copied into public/vendor/larajax/ui/. Larajax's own assets are published as its installation guide describes. The demo app republishes both with --force from the post-autoload-dump scripts in its composer.json, so every composer install and composer update keeps them current.

Include the assets in your layout ​

Add the stylesheets and scripts to your Blade layout. This is the demo app's layout:

blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>
            @yield('title', 'Amber') - {{ config('app.name', 'Amber') }}
        </title>

        {{-- Sets data-bs-theme before the page paints, from the saved choice or else the OS preference --}}
        <script>
            (function () {
                var amberTheme = null;

                try {
                    amberTheme = localStorage.getItem('amber-theme');
                }
                catch (e) {
                    // Storage can be unavailable, so the OS preference applies
                }

                if (amberTheme !== 'light' && amberTheme !== 'dark') {
                    amberTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
                }

                document.documentElement.setAttribute('data-bs-theme', amberTheme);
            })();
        </script>

        {{-- No build step: assets load from a CDN and the published vendor/larajax files. --}}
        <script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2"></script>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@6.0.0-alpha.1/dist/css/bootstrap.min.css" rel="stylesheet">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
        <link href="{{ asset('vendor/larajax/ui/ui.css') }}" rel="stylesheet">
        <link href="{{ asset('css/amber.css') }}" rel="stylesheet">
        <script type="module" src="https://cdn.jsdelivr.net/npm/bootstrap@6.0.0-alpha.1/dist/js/bootstrap.bundle.min.js"></script>
        <script src="{{ asset('vendor/larajax/framework-bundle.js') }}"></script>
        <script type="module" src="{{ asset('vendor/larajax/ui/ui.js') }}"></script>
        <script src="{{ asset('js/amber.js') }}" defer></script>
        <script type="module" src="{{ asset('js/code-blocks.js') }}"></script>

        {{-- Assets added by custom widgets; the page content renders before this layout, so its widgets are included --}}
        {{ Ui::renderAssets() }}
    </head>
    <body class="amber-layout">
        <div class="app-shell">
            <button
                class="sidebar-scrim"
                type="button"
                aria-label="Close navigation"
                data-amber-menu-close></button>

            @include('partials.site.sidebar')

            @include('partials.site.topbar')

            <main class="app-main">
                @yield('content')
            </main>
        </div>
    </body>
</html>

The order matters:

  • Bootstrap's stylesheet comes before ui.css, so the library's rules layer over it.
  • Larajax's framework-bundle.js loads before ui.js, because the widgets register their controls with the window.jax API it creates. Use the bundle build: the plain framework.js build does not include the control API (jax.registerControl) the library needs.
  • Bootstrap's JavaScript module opens the menus.
  • Ui::renderAssets() prints, at the end of <head>, any files that widgets add during the request, such as a custom widget that loads its own script with addJs(). It works in the layout because the page content renders before the layout does. AJAX responses carry the same files when a widget is created during a request, and the browser skips files it already has. See printing the assets.

The rest belongs to the demo: the script at the top sets the colour mode before the page paints, the Phosphor icon font draws the icons (the library's built-in icons use Phosphor unless you choose another pack), and amber.css and amber.js style and drive the demo's own sidebar and top bar. Beyond Bootstrap and Larajax, the library has no framework dependency.

Using with Vite ​

If your application already runs a Vite pipeline, you can skip publishing the library's assets and import the entries straight from the vendor directory. The source tree resolves the same way the published tree does:

js
// resources/js/app.js
import '../../vendor/larajax/ui/resources/assets/ui.js';
css
/* resources/css/app.css */
@import '../../vendor/larajax/ui/resources/assets/ui.css';

Vite then bundles the library into your application build. Larajax must still load before your bundle runs, and Bootstrap's stylesheet before ui.css.

Prepare your controller ​

Pages that show widgets use a Larajax controller, as Larajax's controller integration guide sets up, and add the library's ViewMaker trait, which lets widgets render partials through the controller, such as toolbar buttons, partial columns and hint fields. The demo app's controllers extend this base class:

php
<?php

namespace App\Classes;

use Larajax\LarajaxController;
use Larajax\Ui\Traits\ViewMaker;

/**
 * ControllerBase is the base for pages that render Larajax UI widgets, where LarajaxController routes each widget's AJAX handlers back to the controller that built it and ViewMaker adds the view helpers the widgets use.
 */
abstract class ControllerBase extends LarajaxController
{
    use ViewMaker;
}

Routes ​

A page and its widgets' AJAX requests share one address, so each page route answers both GET and POST, as Larajax's controller integration guide explains:

php
Route::match(['get', 'post'], '/posts', [PostsController::class, 'index'])->name('posts.index');
Route::match(['get', 'post'], '/posts/create', [PostsController::class, 'create'])->name('posts.create');
Route::match(['get', 'post'], '/posts/{post}/edit', [PostsController::class, 'edit'])->name('posts.edit');

Render your first form ​

Describe the form in a YAML file. The demo app keeps its definitions in resources/ui, one folder per model, and the group form has four fields:

yaml
# ===================================
#  Group Form Field Definitions
# ===================================

fields:

    name:
        label: Name
        span: left

    code:
        label: Code
        span: right
        comment: A unique machine-readable identifier, e.g. "editors".

    description:
        label: Description
        type: textarea
        size: small
        span: full

    users:
        label: Members
        type: taglist
        nameFrom: name
        placeholder: Add members

The controller's create and edit actions build the form for their record and pass it to the view:

php
<?php

namespace App\Controllers;

use App\Classes\ControllerBase;
use App\Controllers\GroupsController\GroupForm;
use App\Controllers\GroupsController\GroupList;
use App\Models\UserGroup;
use Illuminate\Contracts\View\View;

/**
 * GroupsController holds the group pages, with the list and form widgets and their AJAX handlers in the GroupList and GroupForm traits.
 */
class GroupsController extends ControllerBase
{
    use GroupList;
    use GroupForm;

    /**
     * index displays the list of groups, where clicking a row edits the group in a popup.
     */
    public function index(): View
    {
        return view('groups.index', $this->prepareListVars());
    }

    /**
     * create shows the form for a new group.
     */
    public function create(): View
    {
        $this->formWidget = $this->makeFormWidget(new UserGroup, 'create');

        return view('groups.create', [
            'formWidget' => $this->formWidget,
        ]);
    }

    /**
     * edit shows the form for an existing group on its own page.
     */
    public function edit(UserGroup $userGroup): View
    {
        $this->formWidget = $this->makeFormWidget($userGroup, 'update');

        return view('groups.edit', [
            'userGroup' => $userGroup,
            'formWidget' => $this->formWidget,
        ]);
    }
}

makeFormWidget() builds the form with Form::make(), binding it to the model. A path starting with ~/ resolves from the application root:

php
/**
 * makeFormWidget builds the group form for a given context.
 */
protected function makeFormWidget(UserGroup $group, string $context): Form
{
    return Form::make(
        context: $context,
        model: $group,
        fields: '~/resources/ui/user_group/fields.yaml',
    );
}

The view wraps the widget in a <form> element whose data-request names the handler that saves it, and adds the buttons:

blade
@extends('layouts.app')
@section('title', 'Edit Group')
@section('content')

<form data-request="onUpdate" data-request-flash>
    <div class="d-flex align-items-center justify-content-between mb-7">
        <h1 class="mb-0">
            Edit Group
        </h1>
        <a href="{{ route('groups.index') }}" class="btn-link text-decoration-none">
            &larr; Back to groups
        </a>
    </div>

    {{ $formWidget }}

    <div class="mt-7 d-flex gap-3">
        {{ Ui::ajaxButton(dataRequestFlash: true, handler: 'onUpdate', label: 'Save', primary: true) }}
        {{ Ui::ajaxButton(
            dataRequestFlash: true,
            handler: 'onDestroy',
            label: 'Delete',
            danger: true,
            dataRequestConfirm: 'Delete this group? Its members keep their accounts.'
        ) }}
    </div>
</form>

@endsection

That is a complete, working form: the fields render from YAML, show the group's values, and post back to the onUpdate handler through Larajax. Building a Form Page covers saving, validation and the rest.

Organising a controller ​

As a controller grows, keep the class to its page actions, as GroupsController does above, and move each widget's setup into a trait, together with the AJAX handlers that belong to it, in a folder named after the controller. The posts pages have three:

text
app/Controllers/PostsController.php
app/Controllers/PostsController/PostList.php        the list, filter and toolbar, and the bulk actions
app/Controllers/PostsController/PostForm.php        the form, and the store, update and destroy handlers
app/Controllers/PostsController/QuickPostForm.php   the quick post popup and its handler

Each trait is plain code you own. It declares the widget properties it uses, builds its widgets with Form::make(), Lists::make() and the others, and holds their handlers. The traits share one class, so give each method a name that says which concern it belongs to, such as makeListWidget() and makeQuickPostFormWidget(). The form guide walks through PostForm, and the list guide through PostList and QuickPostForm.

Timezones ​

Dates are stored in the application timezone. To show them in each person's own timezone, register a resolver with Ui::resolveTimezoneUsing(), see timezones. The columns, fields and scopes that convert, and the useTimezone option that turns it off, are listed in timezones in lists, filters and forms.

Next steps ​