Skip to content

Latest commit

 

History

History
743 lines (601 loc) · 17.7 KB

File metadata and controls

743 lines (601 loc) · 17.7 KB

Laravilt Forms Documentation

Complete form builder system with 30+ field types, validation, and Blade/Inertia.js integration for Laravilt.

Table of Contents

  1. Getting Started
  2. Architecture
  3. Form Generation
  4. Field Types
  5. API Reference
  6. MCP Server Integration

Overview

Laravilt Forms provides a comprehensive form building system with:

  • 30+ Field Types: Text, select, date, file upload, rich editor, and more
  • Fluent Builder: Chainable methods for clean, readable form definitions
  • Validation: Built-in Laravel validation integration
  • Blade Components: Pre-built UI components with Reka UI styling
  • Inertia Integration: Seamless Vue 3 form handling
  • Real-time Reactivity: Dynamic field visibility, options, and validation
  • File Management: Advanced file upload with preview and validation
  • Rich Content: WYSIWYG editor, Markdown editor, and code editor support

Quick Start

# Generate a new form class
php artisan make:form UserForm

# Generate a resource form (CRUD)
php artisan make:form UserForm --resource

# Use in Blade (server-side)
<x-laravilt::form :schema="$form" />

# Use in Inertia (client-side)
<Form :schema="form.schema" />

Key Features

📝 Basic Fields

  • TextInput: Single-line text input with validation
  • Textarea: Multi-line text input
  • TranslatableInput: Multi-language text input with a per-locale dialog
  • NumberField: Numeric input with min/max
  • Select: Dropdown select with search
  • Checkbox: Single checkbox
  • CheckboxList: Multiple checkboxes
  • Radio: Radio button group
  • Toggle: Switch/toggle button
  • ToggleButtons: Button group toggle
  • Hidden: Hidden field

📅 Date & Time

  • DatePicker: Single date selection
  • DateTimePicker: Date and time selection
  • TimePicker: Time selection
  • DateRangePicker: Date range selection

🎨 Advanced Inputs

  • ColorPicker: Color selection with swatches
  • IconPicker: Lucide icon picker
  • FileUpload: File/image upload with preview
  • RichEditor: WYSIWYG editor (TipTap)
  • MarkdownEditor: Markdown editor with preview
  • CodeEditor: Syntax-highlighted code editor (CodeMirror)
  • TagsInput: Multiple tags input
  • KeyValue: Key-value pair editor
  • Slider: Range slider
  • RateInput: Star rating input
  • PinInput: PIN/OTP input

🔄 Dynamic Fields

  • Repeater: Repeatable field groups
  • Builder: Block-based content builder

🎯 Field Features

  • Label and placeholder
  • Help text and hints
  • Required indicator
  • Validation rules
  • Disabled and readonly states
  • Conditional visibility
  • Default values
  • Dependent fields
  • Custom prefixes/suffixes
  • Inline layout

System Requirements

  • PHP 8.3+
  • Laravel 12+
  • Blade or Inertia.js v2+
  • Vue 3 (for Inertia)

Installation

composer require laravilt/forms

The service provider is auto-discovered and will register automatically.

Configuration

Publish the configuration:

php artisan vendor:publish --tag=laravilt-forms-config

Publish the views:

php artisan vendor:publish --tag=laravilt-forms-views

Publish the assets:

php artisan vendor:publish --tag=laravilt-forms-assets

Basic Usage

Creating a Form Class

<?php

namespace App\Forms;

use Laravilt\Forms\Form;
use Laravilt\Forms\Components\TextInput;
use Laravilt\Forms\Components\Textarea;
use Laravilt\Forms\Components\Select;
use Laravilt\Forms\Concerns\HasSchema;

class UserForm extends Form
{
    use HasSchema;

    protected function setUp(): void
    {
        parent::setUp();

        $this->schema([
            TextInput::make('name')
                ->label('Full Name')
                ->required()
                ->placeholder('Enter your name'),

            TextInput::make('email')
                ->label('Email Address')
                ->email()
                ->required()
                ->unique('users', 'email'),

            Textarea::make('bio')
                ->label('Biography')
                ->rows(5)
                ->placeholder('Tell us about yourself'),

            Select::make('role')
                ->label('Role')
                ->options([
                    'admin' => 'Administrator',
                    'editor' => 'Editor',
                    'viewer' => 'Viewer',
                ])
                ->required(),
        ]);
    }
}

Using in a Controller

use App\Forms\UserForm;
use Inertia\Inertia;

public function create()
{
    $form = UserForm::make();

    return Inertia::render('Users/Create', [
        'form' => $form->toLaraviltProps(),
    ]);
}

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'bio' => 'nullable|string',
        'role' => 'required|in:admin,editor,viewer',
    ]);

    User::create($validated);

    return redirect()->route('users.index');
}

Using in Blade

<x-laravilt::form
    :schema="$form->getSchema()"
    action="/users"
    method="POST"
/>

Using in Vue (Inertia)

<template>
  <div>
    <Form
      :schema="form.schema"
      @submit="handleSubmit"
    />
  </div>
</template>

<script setup>
import { useForm } from '@inertiajs/vue3'
import Form from '@/components/forms/Form.vue'

const props = defineProps({
  form: Object
})

const form = useForm({})

const handleSubmit = () => {
  form.post('/users')
}
</script>

Field Types Reference

TextInput

TextInput::make('name')
    ->label('Name')
    ->placeholder('Enter name')
    ->required()
    ->minLength(3)
    ->maxLength(255)
    ->prefix('Mr.')
    ->suffix('@example.com')
    ->helperText('Enter your full name');

TranslatableInput

A multi-language text field. Its value is an array keyed by locale code (['en' => 'Title', 'ar' => 'العنوان', 'ckb' => 'ناونیشان']). The main input edits the active locale; a globe button inside the input opens a dialog with one input per locale (RTL locales render with dir="rtl"), so long titles and content stay readable. The globe is hidden when only one locale is allowed.

TranslatableInput::make('name')
    ->label('Name')
    ->locales(['en', 'ar', 'ckb']) // defaults to the configured locales, see below
    ->activeLocale('en')           // defaults to the app locale when allowed
    ->required()                   // every locale is required...
    ->requiredLocales(['en'])      // ...unless you narrow it down
    ->maxLength(255)               // applied per locale
    ->multiline()                  // textarea per locale
    ->rows(4);

Locales resolve in this order: explicit locales(), then config('laravilt-forms.locales') (published to config/laravilt-forms.php), then config('app.available_locales'), then the application locale.

By default the field reuses the languages the panel already knows about. That is the same list the Locale & Timezone settings page shows, so a project defines its languages once in config/app.php; label becomes the native name shown in the dialog and dir sets dir on each input:

// config/app.php
'available_locales' => [
    ['value' => 'en', 'label' => 'English', 'dir' => 'ltr'],
    ['value' => 'ar', 'label' => 'العربية', 'dir' => 'rtl'],
    ['value' => 'ckb', 'label' => 'کوردی', 'dir' => 'rtl'],
],

Set laravilt-forms.locales only when the languages content is written in differ from the languages the UI is shown in, for example an English-only admin panel that manages content in three languages. It accepts plain codes or per-locale metadata, and an optional label overrides the short badge on the globe button:

// config/laravilt-forms.php
'locales' => [
    'en' => ['name' => 'English', 'direction' => 'ltr'],
    'ar' => ['name' => 'العربية', 'direction' => 'rtl'],
    'ckb' => ['name' => 'کوردی', 'direction' => 'rtl'],
],
// or simply: 'locales' => ['en', 'ar', 'ckb'],

Names and directions are looked up in laravilt-forms.locales first and app.available_locales second, so plain codes in the forms config still pick up the names the panel defines. A code found in neither is shown as its own code, labelled with its uppercased base code, and rendered LTR.

Every locale is validated on its own and errors are reported per locale key (name.en, name.ar, ...), which the field shows next to the matching input. getValidationRules() returns one rule list for the field, as the schema expects, with a TranslationsRule carrying the per-locale rules; use getLocaleValidationRules() for flat keys when validating by hand:

$field = TranslatableInput::make('name')->locales(['en', 'ar', 'ckb'])->required()->maxLength(120);

$field->getValidationRules();
// ['required', 'array', TranslationsRule(en|ar|ckb => ['required', 'string', 'max:120'])]

$field->getLocaleValidationRules();
// [
//     'name'     => ['required', 'array'],
//     'name.en'  => ['required', 'string', 'max:120'],
//     'name.ar'  => ['required', 'string', 'max:120'],
//     'name.ckb' => ['required', 'string', 'max:120'],
// ]

Messages use the field label plus the locale, e.g. "The Name (EN) field is required.", and validationMessages(['required' => '...']) applies to every locale (['en.required' => '...'] targets one).

hydrateState() / dehydrateState() accept a JSON string, an array or null and always return an array with every allowed locale present, so the field works with both storage styles:

// spatie/laravel-translatable
class Product extends Model
{
    use \Spatie\Translatable\HasTranslations;

    public $translatable = ['name'];
}

// or a plain JSON column
class Category extends Model
{
    protected $casts = ['name' => 'array'];
}

// Both accept the field's value as-is:
$product->setTranslations('name', $data['name']); // or $product->name = $data['name'];
$category->name = $data['name'];

Select

Select::make('country')
    ->label('Country')
    ->options([
        'us' => 'United States',
        'uk' => 'United Kingdom',
        'ca' => 'Canada',
    ])
    ->searchable()
    ->required()
    ->placeholder('Select a country');

DatePicker

DatePicker::make('birth_date')
    ->label('Birth Date')
    ->required()
    ->minDate(now()->subYears(100))
    ->maxDate(now()->subYears(18))
    ->displayFormat('F j, Y');

FileUpload

FileUpload::make('avatar')
    ->label('Profile Picture')
    ->image()
    ->maxSize(2048) // 2MB
    ->acceptedFileTypes(['image/jpeg', 'image/png'])
    ->imagePreview()
    ->imageCropAspectRatio('1:1')
    ->imageResizeTargetWidth('500')
    ->imageResizeTargetHeight('500');

RichEditor

RichEditor::make('content')
    ->label('Content')
    ->required()
    ->toolbarButtons([
        'bold',
        'italic',
        'underline',
        'link',
        'bulletList',
        'orderedList',
        'heading',
    ])
    ->minLength(100);

Repeater

use Laravilt\Forms\Components\Repeater;

Repeater::make('contacts')
    ->label('Contacts')
    ->schema([
        TextInput::make('name')
            ->label('Name')
            ->required(),
        TextInput::make('email')
            ->label('Email')
            ->email()
            ->required(),
        TextInput::make('phone')
            ->label('Phone'),
    ])
    ->minItems(1)
    ->maxItems(5)
    ->addActionLabel('Add Contact')
    ->reorderable()
    ->collapsible();

KeyValue

KeyValue::make('metadata')
    ->label('Metadata')
    ->keyLabel('Key')
    ->valueLabel('Value')
    ->addActionLabel('Add Field')
    ->reorderable();

ColorPicker

ColorPicker::make('brand_color')
    ->label('Brand Color')
    ->required()
    ->swatches([
        '#FF0000',
        '#00FF00',
        '#0000FF',
    ]);

Validation

Built-in Rules

TextInput::make('email')
    ->required()
    ->email()
    ->unique('users', 'email')
    ->confirmed();

NumberField::make('age')
    ->required()
    ->min(18)
    ->max(100);

TextInput::make('password')
    ->required()
    ->minLength(8)
    ->maxLength(255)
    ->password()
    ->confirmed();

Custom Validation Rules

TextInput::make('username')
    ->rules(['required', 'string', 'alpha_dash', 'unique:users']);

Conditional Fields

Visibility Based on Another Field

use Laravilt\Support\Utilities\Get;

Select::make('country')
    ->label('Country')
    ->options([
        'us' => 'United States',
        'ca' => 'Canada',
    ])
    ->live(), // Make field reactive

Select::make('state')
    ->label('State')
    ->options(fn (Get $get) =>
        $get('country') === 'us'
            ? ['ca' => 'California', 'ny' => 'New York']
            : ['on' => 'Ontario', 'bc' => 'British Columbia']
    )
    ->visible(fn (Get $get) => in_array($get('country'), ['us', 'ca']))
    ->required();

After State Updated

use Laravilt\Support\Utilities\Set;

Select::make('product_type')
    ->label('Product Type')
    ->options([
        'physical' => 'Physical Product',
        'digital' => 'Digital Product',
    ])
    ->afterStateUpdated(function ($value, Set $set) {
        if ($value === 'digital') {
            $set('requires_shipping', false);
        }
    })
    ->live();

Generator Commands

# Generate a basic form
php artisan make:form UserForm

# Generate a resource form with CRUD operations
php artisan make:form UserForm --resource

# Force overwrite existing file
php artisan make:form UserForm --force

# Generate a form component (custom field)
php artisan make:component CustomField

Best Practices

  1. Use Form Classes: Create dedicated form classes for reusability
  2. Validate on Server: Always validate on server-side even with client-side validation
  3. Use Appropriate Field Types: Choose the right field type for better UX
  4. Add Helper Text: Provide clear instructions with helper text
  5. Group Related Fields: Use sections to organize related fields
  6. Mark Required Fields: Always indicate required fields
  7. Provide Feedback: Show validation errors clearly
  8. Test Accessibility: Ensure forms are keyboard-navigable

Examples

Login Form

use Laravilt\Forms\Form;
use Laravilt\Forms\Components\TextInput;

class LoginForm extends Form
{
    use HasSchema;

    protected function setUp(): void
    {
        parent::setUp();

        $this->schema([
            TextInput::make('email')
                ->label('Email')
                ->email()
                ->required()
                ->autofocus(),

            TextInput::make('password')
                ->label('Password')
                ->password()
                ->required(),

            Checkbox::make('remember')
                ->label('Remember me'),
        ]);
    }
}

Product Form

use Laravilt\Forms\Components\*;
use Laravilt\Schemas\Components\Section;

class ProductForm extends Form
{
    use HasSchema;

    protected function setUp(): void
    {
        parent::setUp();

        $this->schema([
            Section::make('Basic Information')
                ->schema([
                    TextInput::make('name')
                        ->label('Product Name')
                        ->required(),

                    Textarea::make('description')
                        ->label('Description')
                        ->required()
                        ->rows(5),

                    Select::make('category_id')
                        ->label('Category')
                        ->relationship('category', 'name')
                        ->required(),
                ]),

            Section::make('Pricing')
                ->schema([
                    NumberField::make('price')
                        ->label('Price')
                        ->prefix('$')
                        ->required()
                        ->min(0)
                        ->step(0.01),

                    NumberField::make('compare_at_price')
                        ->label('Compare at Price')
                        ->prefix('$')
                        ->min(0)
                        ->step(0.01),

                    Toggle::make('is_taxable')
                        ->label('Taxable')
                        ->default(true),
                ]),

            Section::make('Inventory')
                ->schema([
                    NumberField::make('quantity')
                        ->label('Quantity')
                        ->required()
                        ->min(0)
                        ->default(0),

                    Toggle::make('track_inventory')
                        ->label('Track Inventory')
                        ->default(true),
                ]),

            Section::make('Media')
                ->schema([
                    FileUpload::make('images')
                        ->label('Product Images')
                        ->image()
                        ->multiple()
                        ->maxFiles(5)
                        ->maxSize(5120)
                        ->imagePreview()
                        ->reorderable(),
                ]),
        ]);
    }
}

Available Components

Basic

  • TextInput
  • Textarea
  • TranslatableInput
  • NumberField
  • Select
  • Checkbox
  • CheckboxList
  • Radio
  • Toggle
  • ToggleButtons
  • Hidden

Date & Time

  • DatePicker
  • DateTimePicker
  • TimePicker
  • DateRangePicker

Advanced

  • ColorPicker
  • IconPicker
  • FileUpload
  • RichEditor
  • MarkdownEditor
  • CodeEditor
  • TagsInput
  • KeyValue
  • Slider
  • RateInput
  • PinInput

Dynamic

  • Repeater
  • Builder

Support

  • GitHub Issues: github.com/laravilt/forms
  • Documentation: docs.laravilt.com
  • Discord: discord.laravilt.com