roro/roroform
Composer 安装命令:
composer require roro/roroform
包简介
Laravel form components with repeatable groups that actually work — searchable selects, a chainable type-aware JS API, full old()/validation/CSRF integration, and a zero-CSS-framework theme. Zero JS dependencies.
关键字:
README 文档
README
Build full Laravel forms out of Blade components — and drive every field from JavaScript like it's 2026.
25+ input components · searchable selects · repeatable groups that actually work · a chainable, type-aware JS API · fully responsive & accessible · Tailwind, Bootstrap & framework-free themes — wired into Laravel's old(), validation errors and CSRF out of the box.
▶ Live playground · Quick start · Docs
Why RoroForm?
Most Laravel form helpers stop at rendering an <input>. RoroForm goes the whole way — from the Blade tag to the runtime behaviour in the browser.
- 🧩 One tag per field, zero boilerplate.
<x-roro-text>,<x-roro-select>,<x-roro-file>… 25+ components that render label, input, validation border, error message and required marker — consistently, in your theme. - 🔁 Repeatable groups that just work. Nest anything (including searchable selects, multi-selects and file inputs), add/remove/reorder rows, and submit a clean
contacts[0][name]array. Old input is restored after a failed validation with zero extra wiring. This is the feature other packages don't ship. - 🎛️ A real JavaScript API. A chainable, type-aware facade:
roro('email').value('a@b.c').required().focus(). The same.value()call reads/writes a text input, a select, a multi-select array, a checkbox boolean, a radio group or a list of file names. Most form packages ship no runtime API at all. - 🪶 Zero dependencies. The runtime is vanilla JavaScript — no jQuery, no framework, nothing to load on the page. Drop it into any stack (Livewire, Alpine, Inertia, Vue, React or plain Blade) without conflicts.
- 🔎 Smart selects, server-rendered. Searchable single & multi-selects with tags, option groups, and dynamic options — added client-side instantly or fetched from your JSON endpoint. Options render server-side, so there's no flash and no mandatory round-trip.
- 🪄 Laravel-native by default. Auto-repopulation from
old(), per-field error messages pulled straight fromsession('errors'), CSRF, AJAX submit with server-side validation errors mapped back onto the right fields. - 🎨 Three themes — including no framework at all. Ships Tailwind, Bootstrap, and a framework-free
rawtheme that brings its own stylesheet, so you get a clean modern look with zero CSS framework on the page. Switch with one config line; publish the views or the CSS to own them. - ♿ Accessible out of the box. Every theme ships real labels,
aria-describedbyerror wiring,aria-invalid/aria-required, fieldset/legend radio groups, and a full ARIA combobox for the custom selects — keyboard-navigable (↑↓, Enter, Escape, Home/End, search box) and screen-reader friendly. Most packages' custom selects are mouse-only. - 📱 Responsive & touch-friendly — the same UI on every device. Components flow from desktop to mobile out of the box: full-width controls, dropdowns and search bars that fit small screens, tags and radio groups that wrap, and comfortable tap targets. There's one custom UI everywhere — no device-specific
<select>fallback to second-guess, so what you test is what every visitor gets. - 📦 No build step for you. The vanilla-JS runtime is injected inline the first time a form renders. No npm, no Vite config, no bundler in your app.
RoroForm vs. the alternatives
If you've shopped for Laravel form tooling, you've met the usual options. Here's where RoroForm sits:
| Feature | RoroForm | Collective / Spatie helpers | Filament / Livewire |
|---|---|---|---|
| Nested repeatable groups (selects & files inside) | ✅ just works | ❌ | ⚠️ heavy stack |
| Runtime JS API to drive fields | ✅ chainable, type-aware | ❌ | ⚠️ partial |
| JS dependencies | none | varies | full framework |
| Works in plain Blade / any stack | ✅ | ✅ | ❌ tied to their runtime |
| Zero-CSS-framework option | ✅ raw theme |
❌ | ❌ |
| Searchable, accessible custom selects | ✅ ARIA combobox | ❌ | ✅ |
Laravel Collective's
Form/Htmlpackage is archived; Filament and Livewire are excellent, but they pull in a full reactive runtime. RoroForm aims at the middle: server-rendered Blade with a real JS layer, no framework lock-in.
Quick start
Requirements: PHP
^8.0· Laravel9 → 13. CSS is optional — therawtheme (framework-free) ships its own stylesheet, so you only need Tailwind or Bootstrap if you pick that theme. No JavaScript dependencies — the runtime is vanilla JS.
1. Install
composer require roro/roroform
2. Publish the config (optional but recommended)
php artisan vendor:publish --tag=roro-config
This creates config/roroform.php:
return [ // 'tailwind' (default) | 'bootstrap' | 'raw' 'theme' => 'tailwind', // Add the HTML `required` attribute on required fields by default. 'defaultJsValidation' => true, ];
3. Drop a form in any Blade view — no scripts to add:
<x-roro-form action="/subscribe" :multipart="true" id="signup"> <x-roro-text name="name" label="Full name" :required="true"/> <x-roro-email name="email" label="Email" :required="true"/> <x-roro-select name="country" label="Country" :options="['fr' => 'France', 'es' => 'Spain']"/> <x-roro-button :ajax="true" form-id="signup">Send</x-roro-button> </x-roro-form>
That's it. Label, validation, error display, CSRF and the AJAX submit are all handled — and the assets are injected automatically. No <script src>, no @vite, nothing else to add.
A real form in 30 seconds
A taste of the breadth — every field below is a single tag:
<x-roro-form id="demo" :multipart="true"> {{-- Text-like inputs --}} <x-roro-text name="first_name" label="First name" :required="true"/> <x-roro-email name="email" label="Email" :required="true"/> <x-roro-password name="password" label="Password"/> <x-roro-url name="website" label="Website"/> <x-roro-tel name="phone" label="Phone"/> {{-- Dates, numbers, ranges, colors --}} <x-roro-date name="dob" label="Date of birth"/> <x-roro-number name="age" label="Age"/> <x-roro-range name="satisfaction" label="Satisfaction" step="5"/> <x-roro-color name="color" label="Favourite colour"/> {{-- Choices --}} <x-roro-checkbox name="newsletter" label="Subscribe" position="right"/> <x-roro-radio-container name="plan" label="Plan"> <x-roro-radio name="plan" value="free" label="Free"/> <x-roro-radio name="plan" value="pro" label="Pro"/> </x-roro-radio-container> {{-- Searchable selects --}} <x-roro-select name="country" label="Country" :options="['Europe' => ['fr' => 'France', 'es' => 'Spain']]"/> <x-roro-multi-select name="tags[]" label="Tags" :values="['a']" :options="['a' => 'Alpha', 'b' => 'Beta']"/> {{-- File upload --}} <x-roro-file name="docs[]" :multiple="true" label="Documents" requirements-text="Accepted: jpg, pdf"/> {{-- Repeatable group → submits as contacts[0][name], contacts[1][name], … --}} <x-roro-repeatable name="contacts" label="Contacts" item-label="Contact" :min="1" :max="5" :reorder="true"> <x-roro-text name="name" label="Name" :required="true"/> <x-roro-email name="email" label="Email"/> <x-roro-select name="type" label="Type" :options="['mobile' => 'Mobile', 'home' => 'Home']"/> </x-roro-repeatable> <x-roro-button :ajax="true" form-id="demo">Submit</x-roro-button> </x-roro-form>
// Drive it from JavaScript — the same API, whatever the field type. roro.form('demo') .fill({ first_name: 'Ada', country: 'fr', tags: ['a', 'b'] }) .onSuccess(res => console.log('Saved!', res)); roro('contacts').addRow({ name: 'Grace', type: 'home' });
📚 Documentation
Everything below is collapsed to keep this page scannable. Open the part you need.
🧱 The component catalog — every input type
Every component renders a full field: wrapper, optional label, the control, a validation border and an error slot — all themed. Names map onto Laravel's request payload exactly as you'd expect, including nested array names like user[26][name].
| Component | Renders | Notable props |
|---|---|---|
<x-roro-text> |
<input type=text> |
value placeholder required disabled readonly |
<x-roro-email> <x-roro-password> <x-roro-url> <x-roro-tel> |
typed text inputs | same as text |
<x-roro-number> |
<input type=number> |
min max step |
<x-roro-hidden> |
<input type=hidden> |
value |
<x-roro-date> <x-roro-time> <x-roro-datetime-local> <x-roro-week> <x-roro-month> |
native date/time pickers | value |
<x-roro-range> |
slider | min max step list |
<x-roro-color> |
colour picker | hide-text-input |
<x-roro-checkbox> |
checkbox | label position (left/right) |
<x-roro-radio-container> + <x-roro-radio> |
a radio group | subtitle; radios wrap responsively |
<x-roro-select> |
searchable single select | :options value |
<x-roro-multi-select> |
searchable multi-select with tags | :options :values (name ends with []) |
<x-roro-file> |
styled file input | :multiple requirements-text |
<x-roro-repeatable> |
repeatable group of any of the above | see its section below |
<x-roro-button> |
submit button | :ajax form-id :ajax-errors |
<x-roro-form> |
the <form> + asset injection + overlay |
action method :multipart enctype :overlay |
Shared field props (from InputMain): id · name · label · value · placeholder · :required · :disabled · :readonly · :hidden · class · wrapperClass · labelClass · tooltip · :enableError · :hasTopMargins · :populate · :disableJsValidation.
🧾 Every attribute, every component — the full reference
Conventions. Write string attributes plainly (label="Email"); add a : prefix to bind booleans, numbers, arrays and PHP expressions (:required="true", :options="[...]"). camelCase props are written in kebab-case in Blade — wrapperClass → wrapper-class, hasTopMargins → :has-top-margins, requirementsText → requirements-text. Any attribute not listed below lands on the underlying element as a plain HTML attribute (see Arbitrary HTML attributes pass-through).
Shared — every field input (from InputMain)
Applies to all field components below (text, number, date, select, file, checkbox, radio, repeatable…) unless noted.
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
string |
auto (uniqid()) |
Element id; auto-generated when omitted. |
name |
string |
'' |
Field name — maps onto the request payload (supports nested user[26][name]). |
label |
string |
null |
Label text rendered above/beside the control. |
value |
string |
'' |
Initial value (overridden by old() after a failed validation). |
placeholder |
string |
'' |
Placeholder text. |
:required |
bool |
false |
Adds required + aria-required. |
:disabled |
bool |
false |
Disables the control. |
:readonly |
bool |
false |
Marks the control read-only. |
:hidden |
bool |
false |
Renders the field's wrapper hidden (display:none). |
class |
string |
'' |
Extra classes on the control (merged with the roro-* classes). |
wrapper-class |
string |
'' |
Extra classes on the field wrapper. |
label-class |
string |
'' |
Extra classes on the label. |
tooltip |
string |
null |
Tooltip text shown next to the label. |
:enable-error |
bool |
true |
Render the inline error slot for this field. |
:has-top-margins |
bool |
true |
Apply the default top margin to the field. |
:populate |
array |
[] |
Candidate values for repopulation (first truthy wins; old() takes precedence). |
:disable-js-validation |
bool |
config | Drop the HTML validation attributes for this field. Defaults to the inverse of defaultJsValidation. |
Text-like — <x-roro-text> <x-roro-email> <x-roro-password> <x-roro-url> <x-roro-tel> <x-roro-hidden>
No extra attributes — shared only. They differ purely by their HTML type (text/email/password/url/tel/hidden).
Numeric & date/time — <x-roro-number> <x-roro-range> <x-roro-date> <x-roro-time> <x-roro-datetime-local> <x-roro-week> <x-roro-month> (from NumericMain)
Shared plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
min |
string |
null |
Minimum value (a number, or a date/time bound for the date inputs). |
max |
string |
null |
Maximum value. |
step |
string |
null |
Step increment. |
list |
string |
null |
Id of a <datalist> to attach (handy on range). |
Color — <x-roro-color>
Shared plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
:hide-text-input |
bool |
false |
Hide the hex text field, leaving just the colour swatch. |
Checkbox — <x-roro-checkbox> (from CheckableMain)
Shared (with value defaulting to '1') plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
:checked |
bool |
false |
Whether the box is checked (auto-set to true when a non-empty value is repopulated). |
Radio group — <x-roro-radio-container> + <x-roro-radio>
<x-roro-radio-container> wraps the group in a <fieldset>/<legend>; each <x-roro-radio> is one option (it's a CheckableMain, so its value is the option value and :checked flags the default).
<x-roro-radio-container> — shared plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
subtitle |
string |
null |
Helper text shown under the group legend. |
subtitle-class |
string |
'' |
Extra classes on the subtitle. |
fieldset-class |
string |
'' |
Extra classes on the <fieldset>. |
<x-roro-radio> — shared (with value = the option value) plus :checked (bool, false).
Selects — <x-roro-select> <x-roro-multi-select> (from SelectableMain)
Shared plus (for the multi-select, end the name with []):
| Attribute | Type | Default | Description |
|---|---|---|---|
:options |
array |
[] |
Options — flat ['fr' => 'France'] or grouped ['Europe' => ['fr' => 'France']]. |
:values |
array |
[] |
Pre-selected values (multi-select; single-select uses value). |
:search-bar |
bool |
true |
Show the in-dropdown search field. |
:clear-button |
bool |
true |
Show the clear (✕) button. |
:options-open |
bool |
false |
Render with the dropdown initially open. |
File — <x-roro-file>
Shared plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
:multiple |
bool |
true |
Allow selecting multiple files (end the name with []). |
accept |
string |
'' |
The native accept filter (e.g. image/*,.pdf). |
max-size |
string |
'' |
Max size hint shown to the user. |
requirements-text |
string |
'' |
Free-text requirements line under the control. |
Repeatable — <x-roro-repeatable>
Forwards the shared field props (except value/placeholder — a repeatable holds rows, not one value) plus:
| Attribute | Type | Default | Description |
|---|---|---|---|
name |
string |
'' |
Array prefix the rows submit under (e.g. contacts). |
:rows |
array |
[] |
Initial dataset: an array of rows (maps, or scalars in token mode). |
:min |
int |
1 |
Minimum number of rows (can't remove below it). |
:max |
int |
null |
Maximum number of rows (null = unlimited). |
:reorder |
bool|string |
false |
false, true/'buttons' (▲▼), 'drag' (handle), or 'both'. |
item-label |
string |
null |
Per-row heading prefix, numbered automatically (Contact 1…). |
key-field |
string |
null |
Inner field whose value uniquely identifies a row (e.g. id) — target rows by stable key. |
add-label |
string |
'+ Add' |
Text/markup of the add button. |
remove-label |
string |
null |
Text/markup of the per-row remove button (theme default when null). |
index-token |
string |
'' |
If set (e.g. #), replaces the token in inner names instead of auto-prefixing. |
:indexed |
bool |
true |
Set to false to leave inner names verbatim (no prefix[i]/token). |
row-class |
string |
'' |
Extra classes applied to every row. |
Button — <x-roro-button>
A standalone ComponentMain (not an InputMain):
| Attribute | Type | Default | Description |
|---|---|---|---|
type |
string |
'submit' |
The button type (submit/button/reset). |
id |
string |
auto | Element id. |
class |
string |
'' |
Extra classes on the button. |
form-id |
string |
null |
The form this button submits (required for AJAX submit). |
button-color |
string |
'bg-blue-600' |
Base colour class. |
button-hover-color |
string |
'bg-blue-700' |
Hover colour class. |
button-text-color |
string |
'text-white' |
Text colour class. |
:disabled |
bool |
false |
Disable the button. |
:has-top-margins |
bool |
true |
Apply the default top margin. |
:ajax |
bool |
false |
Submit the form over AJAX (FormData, overlay, events). |
:enable-ajax-errors |
bool |
true |
Map a 422 response's errors back onto the matching fields. |
Form — <x-roro-form>
| Attribute | Type | Default | Description |
|---|---|---|---|
action |
string |
'' |
Form action URL. |
method |
string |
'POST' |
HTTP method (set on the <form>); @csrf is injected for POST/PUT/PATCH/DELETE. |
id |
string |
auto | Form id (used by roro.form(id) and the button's form-id). |
class |
string |
'' |
Extra classes on the <form>. |
:multipart |
bool |
false |
Shortcut for enctype="multipart/form-data" (for file uploads). |
enctype |
string |
null |
Explicit enctype — takes precedence over :multipart. |
:overlay |
bool |
true |
Render the loading overlay used during AJAX submits. |
🔎 Smart selects — searchable, grouped, dynamic
Options are rendered server-side, then read and enhanced by JS. No flash of an empty dropdown, no mandatory network round-trip — and the basic value is present even before JS runs.
{{-- flat options --}} <x-roro-select name="country" :options="['fr' => 'France', 'es' => 'Spain']" value="fr"/> {{-- grouped options --}} <x-roro-select name="city" label="City" :options="['Europe' => ['par' => 'Paris', 'mad' => 'Madrid'], 'Asia' => ['tok' => 'Tokyo']]"/> {{-- multi-select with pre-selected tags (note the [] in the name) --}} <x-roro-multi-select name="tags[]" :values="['a', 'b']" :options="['a' => 'Alpha', 'b' => 'Beta', 'c' => 'Gamma']"/>
Add options after load — instantly, or from your own endpoint:
// One option, client-side, no request. Optional 4th arg = category. roro('country').addOption('Germany', 'de', 'Europe'); // A batch: roro('country').addOptions([{ label: 'Italy', value: 'it', category: 'Europe' }]); // Or fetch from YOUR JSON endpoint: // GET /api/countries?q=ge -> [{ "label": "Germany", "value": "de", "category": "Europe" }, ...] roro('country').addOptionsAjax('/api/countries', { q: 'ge' }); // returns a Promise roro('country').setOptions([...]); // replace every option roro('country').removeOption('fr'); roro('country').options(); // -> [{ label, value, category }, ...] roro('country').open(); roro('country').close();
Selects emit a real roro:change event — fired only on actual user/programmatic changes, never on initial population:
roro('country').change(value => console.log('picked', value));
The legacy globals (
roroAddOption,roroAddOptionsAjax,roroDisableSelect, …) still work unchanged — theroro()facade is purely additive on top of them.
⭐ Repeatable groups — the headline feature
<x-roro-repeatable> repeats whatever you nest inside it — one field or many, of any type, including searchable selects, multi-selects and file inputs. Users add, remove and (optionally) reorder rows; everything submits as a clean array.
<x-roro-repeatable name="contacts" label="Contacts" :min="1" :max="5"> <x-roro-text name="name" label="Name" :required="true"/> <x-roro-email name="email" label="Email"/> <x-roro-select name="type" :options="['mobile' => 'Mobile', 'home' => 'Home']" label="Type"/> </x-roro-repeatable>
Inner field names are relative to the row (name, email, type) — the component prefixes them automatically, so the form posts:
$request->input('contacts'); // [ // ['name' => 'Alice', 'email' => 'alice@x.com', 'type' => 'mobile'], // ['name' => 'Bob', 'email' => 'bob@x.com', 'type' => 'home'], // ]
Prefilling (edit forms) & validation
Pass an array of rows via :rows. After a failed validation, the re-submitted old() input is restored automatically — no extra wiring:
<x-roro-repeatable name="contacts" :rows="$user->contacts->toArray()"> <x-roro-text name="name" label="Name" :required="true"/> <x-roro-select name="type" :options="$types"/> </x-roro-repeatable>
required (and any HTML constraint) on inner fields is enforced per row; the hidden blueprint is inert, so it never blocks submission.
Flat lists of scalars
For a plain list (tags[]), use the index-token (default #) where the index should go:
<x-roro-repeatable name="tags" index-token="#" :rows="['red', 'green']"> <x-roro-text name="tags[#]" placeholder="Tag"/> </x-roro-repeatable> {{-- posts: tags => ['red', 'green'] --}}
Or turn indexing off entirely with :indexed="false" and keep your own names
(<x-roro-text name="tags[]"/> → posts the same flat tags array).
Attributes
| Attribute | Default | Description |
|---|---|---|
name |
— | Array prefix the rows submit under (e.g. contacts). |
:rows |
[] |
Initial dataset: an array of rows (objects, or scalars in token mode). |
:min |
1 |
Minimum number of rows (can't remove below it). |
:max |
null |
Maximum number of rows (null = unlimited; Add disables at the cap). |
:reorder |
false |
Show up/down buttons to reorder rows. |
item-label |
null |
Per-row heading prefix, numbered automatically (Contact 1, Contact 2…). |
key-field |
null |
Inner field whose value uniquely identifies a row (e.g. id) — lets JS target a row by a stable key instead of position. |
add-label |
+ Add |
Text/markup of the add button. |
remove-label |
✕ |
Text/markup of the per-row remove button. |
index-token |
'' |
If set (e.g. #), replaces the token in inner names instead of auto-prefixing — use for flat lists or full control. |
:indexed |
true |
Set to false to leave inner names verbatim (no prefix[i]/token) — you control the naming. |
row-class |
'' |
Extra classes applied to every row. |
Selects, multi-selects and file inputs added in new rows are wired up exactly like on page load (regenerated ids, registered instances) — nothing extra to call.
🎛️ The roro() facade — one chainable, type-aware API
A small, dependency-free facade so you can drive any field without caring about its underlying markup. Everything is chainable and type-aware — the same call works on a text input, a select, a checkbox or a file field.
roro('email') // -> handle, auto-detecting the field type .value('john@example.com') // set (chainable); roro('email').value() to read .required() // toggle the HTML required flag .focus(); roro('age').disable(); // works on every input type roro('age').error('Too young'); roro('age').clearError(); roro('newsletter').value(true); // checkbox -> boolean roro('satisfaction').value(80); // range roro('file-upload').value(); // file -> ['a.png', ...]
value() is universal: text for inputs, the selected option for a select, an array for a multi-select, the checked state for a checkbox, the checked value for a radio group, file names for a file input.
| Group | Methods |
|---|---|
| Value | value(v?) · val(v?) · clear() · reset() |
| State | disable(b=true) · enable() · readonly(b=true) · editable() · required(b=true) · optional() · isDisabled() · isReadonly() · isRequired() |
| Display | show() · hide() · toggle(b?) · isVisible() · label(t?) · placeholder(t?) |
| Error | error(msg) · clearError() |
| Events | on(ev, fn) · off(ev, fn) · trigger(ev) · change(fn) · input(fn) · click(fn) · focus() · blur() |
| Misc | type() · name() · exists() · $el() · $control() · $wrapper() · attr() · prop() · addClass() / removeClass() / toggleClass() |
Select-only methods (no-op elsewhere): addOption · addOptions · addOptionsAjax · setOptions · removeOption · clearOptions · options · open · close.
Repeatables from JS
roro('contacts').addRow(); // append empty row roro('contacts').addRow({ name: 'Ada', type: 'home' }); // append prefilled row roro('contacts').removeRow(0); // remove first row roro('contacts').rowsCount(); // -> 2 roro('contacts').value(); // -> [{name, email, type}, ...] roro('contacts').value([{ name: 'Ada' }, { name: 'Bob' }]); // replace every row roro('contacts').change(rows => console.log('changed', rows));
Need one specific row? Prefer a stable key over a position — add key-field="id" to the component (with an id field, often hidden, per row):
const row = roro('contacts').row(12); // the row whose id is 12 — stable across reorder/remove row.field('email').value('a@b.c'); // drive one field of that row row.field('type').disable(); // works on nested selects too row.lockRemoval(); // disable its remove button (front-end lock) row.disable(); // disable every field in the row row.value({ name: 'Ada', type: 'home' }); // set the whole row; row.value() to read it row.moveUp(); row.moveDown(); row.remove(); row.key(); row.index(); roro('contacts').rowAt(0); // by position, explicitly roro('contacts').rowWhere(r => r.email === 'a@b.c'); // by predicate (row data) roro('contacts').rowHandles(); // every row as a handle
📨 The roro.form() facade — drive the whole form
const form = roro.form('signup'); form.data(); // -> plain object of every field value form.fill({ name: 'Ada', country: 'fr', tags: ['a', 'b'] }); // prefill (edit flows) form.onSuccess(res => console.log('saved', res)) .onError(xhr => console.log('failed', xhr)); form.validate(); // native reportValidity() form.errors({ email: ['Already taken'] }); // show server-side errors form.clearErrors(); form.submit(); // honours the AJAX <x-roro-button> if present form.disable(); form.enable(); // every field at once form.reset(); form.overlay(true); form.field('email').focus(); // resolve a field by name **or** id
fill() matches keys against field names first (so it maps straight onto your server payload / old() data) and falls back to element ids. It even understands repeatable groups and custom selects, not just native inputs.
📋 Complete helper reference
Every flat roro*(id, …) global is a one-liner shortcut for the matching roro(id).method(…) call — use whichever reads better. id is the component's id (or, for forms, the form id). Below is the full list.
Entry points & introspection
| Helper | What it returns / does | Example |
|---|---|---|
roro(target) |
A type-aware handle (field or form); target is an id or a DOM element |
roro('email').focus() |
roro.field(id) |
A field handle (explicit) | roro.field('email') |
roro.form(id) |
A form handle | roro.form('signup') |
roro.select(id) |
The underlying RoroSelect/RoroMultiSelect instance |
roro.select('country') |
roro.repeatable(id) |
The underlying RoroRepeatable instance |
roro.repeatable('contacts') |
roro.exists(target) |
true if the field/form is in the DOM |
if (roro.exists('email')) … |
roro.all(root?) |
Handles for every field under root (default: document) |
roro.all().forEach(h => h.clear()) |
roro.ready(fn) |
Run fn on DOM ready |
roro.ready(() => …) |
roro.version |
The runtime version string | roro.version // '2.0.0' |
roroGetSelect(id) |
Same as roro.select(id) |
roroGetSelect('country') |
roroGetRepeatable(id) |
Same as roro.repeatable(id) |
roroGetRepeatable('contacts') |
Field value & state
| Helper | Does | Example |
|---|---|---|
roroValue(id, v?) |
Get (omit v) or set the value — type-aware |
roroValue('email', 'a@b.c') · roroValue('email') |
roroClear(id) |
Clear / reset the field | roroClear('email') |
roroDisable(id, b=true) |
Disable (or enable with false) |
roroDisable('age') |
roroEnable(id) |
Enable | roroEnable('age') |
roroReadonly(id, b=true) |
Toggle readonly | roroReadonly('age') |
roroRequired(id, b=true) |
Toggle the HTML required flag |
roroRequired('email') |
roroShow(id) / roroHide(id) |
Show / hide the whole field (wrapper) | roroHide('coupon') |
roroToggleVisibility(id, b?) |
Toggle visibility | roroToggleVisibility('coupon', true) |
roroFocus(id) |
Focus the control | roroFocus('email') |
roroLabel(id, text?) |
Get or set the label text | roroLabel('email', 'E-mail') |
roroField(id) |
Get the field handle | roroField('email').value() |
Errors
| Helper | Does | Example |
|---|---|---|
roroClearError(id) |
Hide the field's error | roroClearError('email') |
roroShowError(id, msg, show=true) |
Show (or hide) an inline error + set aria-invalid |
roroShowError('email', 'Already taken') |
Events
| Helper | Does | Example |
|---|---|---|
roroOnChange(id, fn) |
Run fn(value, event) when the field changes |
roroOnChange('country', v => …) |
roroTrigger(id, event='change') |
Dispatch an event on the control | roroTrigger('email', 'input') |
roroTriggerChangeAll() |
Fire change on every field |
roroTriggerChangeAll() |
Selects
| Helper | Does | Example |
|---|---|---|
roroAddOption(id, label, value, category?) |
Add one option (client-side) | roroAddOption('c', 'France', 'fr', 'EU') |
roroAddOptions(id, list) |
Add a batch [{label,value,category?}] |
roroAddOptions('c', [{label:'Spain',value:'es'}]) |
roroAddOptionsAjax(id, url, params?) |
Fetch options from your JSON endpoint → Promise | roroAddOptionsAjax('c', '/api/countries', {q:'fr'}) |
roroSetOptions(id, list) |
Replace all options | roroSetOptions('c', list) |
roroRemoveOption(id, value) |
Remove one option | roroRemoveOption('c', 'fr') |
roroClearOptions(id) |
Remove every option | roroClearOptions('c') |
roroOptions(id) |
Read options [{label,value,category}] |
roroOptions('c') |
roroDisableSelect(id, b=true) |
Disable a custom select | roroDisableSelect('c') |
roroReadonlySelect(id, b=true) |
Make a custom select readonly | roroReadonlySelect('c') |
roroShowDropDown(id, show=true) |
Open / close the dropdown | roroShowDropDown('c', true) |
Repeatables
| Helper | Does | Example |
|---|---|---|
roroAddRow(id, data?) |
Append a row (optionally prefilled) | roroAddRow('contacts', {name:'Ada'}) |
roroRemoveRow(id, index) |
Remove the row at a position | roroRemoveRow('contacts', 0) |
roroClearRows(id) |
Remove every row | roroClearRows('contacts') |
roroRows(id) |
Read all rows as data | roroRows('contacts') |
roroRowsCount(id) |
Number of rows | roroRowsCount('contacts') |
roroRow(id, target) |
A single-row handle (by key or position) | roroRow('contacts', 12) |
roroRowField(id, target, name) |
A field handle inside one row | roroRowField('contacts', 12, 'email') |
roroLockRow(id, target, b=true) |
Lock/unlock a row's remove button | roroLockRow('contacts', 12) |
Row handles (roroRow(...) / roro('contacts').row(...)) expose: field(name) · fields() · value(data?) · key() · index() · remove() · lockRemoval(b?) · allowRemoval() · isRemovable() · disable(b?) · enable() · moveUp() · moveDown().
Forms
| Helper | Does | Example |
|---|---|---|
roroFormData(formId) |
Serialize the form to a plain object | roroFormData('signup') |
roroFillForm(formId, data) |
Prefill fields (by name, then id) | roroFillForm('signup', user) |
roroSubmit(formId) |
Submit (honours the AJAX button) | roroSubmit('signup') |
roroResetForm(formId) |
Native reset + clear custom selects | roroResetForm('signup') |
roroClearForm(formId) |
Clear every field | roroClearForm('signup') |
roroValidateForm(formId) |
reportValidity() → bool |
roroValidateForm('signup') |
roroFormErrors(formId, errors) |
Show server-side errors {field:[msg]} |
roroFormErrors('signup', {email:['Taken']}) |
roroClearFormErrors(formId) |
Clear all form errors | roroClearFormErrors('signup') |
roroOnSuccess(formId, fn) |
AJAX success → fn(response, event) |
roroOnSuccess('signup', r => …) |
roroOnError(formId, fn) |
AJAX error → fn(xhr, event) |
roroOnError('signup', x => …) |
Overlay & low-level
| Helper | Does |
|---|---|
roroShowOverlay(show=true) |
Show/hide the form's loading overlay |
roroSubmitButton(buttonId, formId) |
Programmatically run a submit button's flow |
roroRegisterButtonOnClick(buttonId) |
Wire a .roro-btn-submit (done automatically on load) |
roroGetWrapper(id) |
The field's wrapper element |
populateFormErrors(form, errors) · clearFormErrors(form) |
The error helpers roroFormErrors/roroClearFormErrors wrap |
addSelect(el) · addMultiSelect(el) · addRepeatable(el) |
Register a freshly-inserted component (done automatically) |
All of the above are plain
window.*globals — no import, no namespace. The chainable equivalents live onroro(id)(see the previous two sections).
🪄 Laravel-native validation, old() & AJAX
RoroForm is wired into the framework, so the usual controller flow needs no front-end glue:
- Repopulation. Every field reads
old()automatically after a failed validation — including nested array names and repeatable rows. - Error display. Each field pulls its first message from
session('errors')and shows it inline, with a red border on the control. - CSRF.
<x-roro-form>injects@csrfforPOST/PUT/PATCH/DELETE. - AJAX submit.
<x-roro-button :ajax="true">posts viaFormData(file uploads included), shows a loading overlay, and fires events:
// Native events — the payload is on event.detail: const form = document.getElementById('signup'); form.addEventListener('roro:ajax:success', e => { const response = e.detail; /* ... */ }); form.addEventListener('roro:ajax:error', e => { const xhr = e.detail; /* ... */ }); // …or the facade shortcut (payload first): roro.form('signup') .onSuccess(response => { /* ... */ }) .onError(xhr => { /* ... */ });
Add :ajax-errors="true" to the button and a 422 response's errors payload is mapped straight back onto the matching fields — no manual error handling:
// Your controller just validates as usual. $request->validate([ 'email' => 'required|email', 'contacts.*.name' => 'required', ]);
🎨 Theming & publishing the views
Pick a theme once in config/roroform.php:
'theme' => 'tailwind', // 'bootstrap' | 'raw'
Three themes ship complete, accessible, and component-for-component identical:
| Theme | Styling | Dark mode trigger |
|---|---|---|
tailwind |
TailwindCSS utility classes | dark: utilities — .dark class (or prefers-color-scheme, per your Tailwind darkMode config) |
bootstrap |
Bootstrap 5 classes | data-bs-theme="dark" on an ancestor |
raw |
Framework-free — no Tailwind, no Bootstrap. Injects its own small stylesheet automatically, so you get a clean, modern look on any page with zero CSS framework. | .dark class or data-roro-theme="dark" on an ancestor |
All three themes are fully dark-mode aware — every component remaps colors, borders, backgrounds and contrast in dark mode. Each is driven by a class/attribute on an ancestor (usually <html>), so you stay in control. To follow the OS setting, map it once:
if (matchMedia('(prefers-color-scheme: dark)').matches) document.documentElement.classList.add('dark'); // or data-bs-theme="dark"
Test it on the playground — click the Dark mode toggle to switch.
The raw theme is themed with CSS custom properties — override --roro-accent, --roro-radius, --roro-border, … to restyle it, or publish the stylesheet:
php artisan vendor:publish --tag=roro-styles # -> public/vendor/roroform/roroform.css
Every theme is accessible: proper <label>s, aria-describedby error association, aria-invalid / aria-required, <fieldset>/<legend> radio groups, and a keyboard-navigable ARIA combobox for the custom selects. All components adapt to dark mode — no configuration needed.
Need to own the markup? Publish the Blade views and edit them in place:
php artisan vendor:publish --tag=roro-views
# -> resources/views/vendor/roroform/...
🔧 Arbitrary HTML attributes pass-through
Every input forwards arbitrary HTML attributes (the Blade attribute bag) onto the underlying element — no need to declare them in PHP. Great for data-*, aria-*, autocomplete, maxlength, Alpine x- directives, etc.
<x-roro-text name="email" label="Email" data-testid="email" autocomplete="email" maxlength="120"/>
renders ... data-testid="email" autocomplete="email" maxlength="120"> on the <input>. Custom CSS classes go through the dedicated class attribute and are merged with the component's own roro-* classes.
Requirements
| PHP | ^8.0 |
| Laravel | 9, 10, 11, 12, 13 |
| JavaScript | none — the runtime is dependency-free vanilla JS |
| CSS | Tailwind, Bootstrap, or none — the raw theme ships its own stylesheet |
Installation recap
composer require roro/roroform php artisan vendor:publish --tag=roro-config # config/roroform.php php artisan vendor:publish --tag=roro-views # (optional) own the Blade markup
Upgrading
Coming from the 1.x (jQuery) runtime? 2.0 drops jQuery for dependency-free vanilla JS. The everyday facade is unchanged; a few front-end event/return contracts changed — see the CHANGELOG for the short migration guide.
License
统计信息
- 总下载量: 7
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: Apache-2.0
- 更新时间: 2025-09-08
