Forms
Custom fields
PrelineForm provides standard HTML5 inputs, but real business forms need date pickers, file uploaders, rich text editors, and domain-specific controls. This guide shows how to build custom field components that integrate with PrelineForm's validation, old input, and error display.
Why custom fields
Standard inputs cover 80% of forms, but the remaining 20% — date ranges, multi-file uploads, WYSIWYG editors, color pickers — require JavaScript libraries and custom markup. Custom fields let you:
- Wrap third-party JS libraries (Flatpickr, Dropzone, Trix) in reusable Blade components
- Preserve PrelineForm's validation and error display
- Keep the fluent API consistent across standard and custom fields
- Share field definitions across projects
Anatomy of a custom field
Every custom field is a Blade component that:
- Accepts
name,value,label,error, andattributesprops - Renders the field markup with Preline UI classes
- Preserves
old()input on validation failure - Displays validation errors below the field
- Integrates with JavaScript libraries via
@push('scripts')
Basic structure
{{-- resources/views/components/forms/date-picker.blade.php --}}
@props([
'name',
'value' => null,
'label' => null,
'error' => null,
'required' => false,
])
<div class="space-y-2">
@if($label)
<label for="{{ $name }}" class="block text-sm font-medium text-gray-900 dark:text-white">
{{ $label }}
@if($required)<span class="text-red-500">*</span>@endif
</label>
@endif
<input
type="text"
id="{{ $name }}"
name="{{ $name }}"
value="{{ old($name, $value) }}"
{{ $attributes->merge(['class' => 'block w-full rounded-lg border-gray-200 px-4 py-3 text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400']) }}
data-datepicker
/>
@if($error)
<p class="text-sm text-red-600 dark:text-red-400">{{ $error }}</p>
@endif
</div>
@once
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-datepicker]').forEach(el => {
flatpickr(el, {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'F j, Y'
});
});
});
</script>
@endpush
@endonceUsage in forms
{!! PrelineForm::open('events.store')->post() !!}
{!! PrelineForm::text('title')->label('Event title')->required() !!}
<x-forms.date-picker
name="start_date"
:value="$event->start_date ?? null"
label="Start date"
:error="$errors->first('start_date')"
required
/>
{!! PrelineForm::submit('Save')->primary() !!}
{!! PrelineForm::close() !!}Integration with PrelineForm
To make custom fields feel native to PrelineForm, register them as macros:
// app/Providers/AppServiceProvider.php
use Laravolt\PrelineForm\Facade as PrelineForm;
public function boot(): void
{
PrelineForm::macro('datePicker', function (string $name, ?string $value = null) {
$error = session('errors')?->first($name);
return view('components.forms.date-picker', [
'name' => $name,
'value' => old($name, $value),
'error' => $error,
'attributes' => new \Illuminate\View\ComponentAttributeBag(),
])->render();
});
}Now use it like any PrelineForm field:
{!! PrelineForm::datePicker('start_date')->label('Start date')->required() !!}Example: File upload with preview
{{-- resources/views/components/forms/image-upload.blade.php --}}
@props([
'name',
'value' => null,
'label' => null,
'error' => null,
'accept' => 'image/*',
])
<div class="space-y-2" x-data="imageUpload('{{ $value }}')">
@if($label)
<label class="block text-sm font-medium text-gray-900 dark:text-white">
{{ $label }}
</label>
@endif
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<img
x-show="preview"
:src="preview"
class="h-24 w-24 rounded-lg object-cover border border-gray-200 dark:border-neutral-700"
alt="Preview"
/>
<div
x-show="!preview"
class="h-24 w-24 rounded-lg bg-gray-100 dark:bg-neutral-800 flex items-center justify-center"
>
<svg class="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
<div class="flex-1">
<input
type="file"
id="{{ $name }}"
name="{{ $name }}"
accept="{{ $accept }}"
@change="handleFileChange"
{{ $attributes->merge(['class' => 'block w-full text-sm text-gray-900 border border-gray-200 rounded-lg cursor-pointer bg-gray-50 dark:text-neutral-400 dark:bg-neutral-900 dark:border-neutral-700']) }}
/>
<p class="mt-1 text-xs text-gray-500 dark:text-neutral-500">
PNG, JPG, GIF up to 2MB
</p>
</div>
</div>
@if($error)
<p class="text-sm text-red-600 dark:text-red-400">{{ $error }}</p>
@endif
</div>
@once
@push('scripts')
<script>
function imageUpload(initialValue) {
return {
preview: initialValue,
handleFileChange(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.preview = e.target.result;
};
reader.readAsDataURL(file);
}
}
};
}
</script>
@endpush
@endonceExample: Rich text editor (Trix)
{{-- resources/views/components/forms/rich-text.blade.php --}}
@props([
'name',
'value' => null,
'label' => null,
'error' => null,
])
<div class="space-y-2">
@if($label)
<label for="{{ $name }}" class="block text-sm font-medium text-gray-900 dark:text-white">
{{ $label }}
</label>
@endif
<input id="{{ $name }}_input" type="hidden" name="{{ $name }}" value="{{ old($name, $value) }}">
<trix-editor
input="{{ $name }}_input"
class="trix-content block w-full rounded-lg border-gray-200 dark:bg-neutral-900 dark:border-neutral-700"
></trix-editor>
@if($error)
<p class="text-sm text-red-600 dark:text-red-400">{{ $error }}</p>
@endif
</div>
@once
@push('styles')
<link rel="stylesheet" href="https://unpkg.com/trix@2.0.0/dist/trix.css">
@endpush
@push('scripts')
<script src="https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js"></script>
@endpush
@endonceValidation patterns
Custom fields must preserve Laravel validation errors:
// Controller
public function store(Request $request)
{
$validated = $request->validate([
'start_date' => 'required|date|after:today',
'avatar' => 'nullable|image|max:2048',
'content' => 'required|min:100',
]);
// Process validated data
}{{-- View --}}
<x-forms.date-picker
name="start_date"
:error="$errors->first('start_date')"
/>
<x-forms.image-upload
name="avatar"
:error="$errors->first('avatar')"
/>
<x-forms.rich-text
name="content"
:error="$errors->first('content')"
/>Common pitfalls
Missing old() input
Always use old($name, $value) to preserve input on validation failure:
{{-- ❌ Wrong: loses input on validation error --}}
<input name="date" value="{{ $value }}">
{{-- ✅ Correct: preserves input --}}
<input name="date" value="{{ old('date', $value) }}">Script loading order
Use @once and @push('scripts') to avoid duplicate script tags:
@once
@push('scripts')
<script src="https://cdn.example.com/library.js"></script>
@endpush
@endonceMissing error display
Always show validation errors below the field:
@if($error)
<p class="text-sm text-red-600 dark:text-red-400">{{ $error }}</p>
@endifVerification commands
Test custom fields:
# Create a test form with custom fields
php artisan make:controller TestFormController
# Add validation rules
# Submit invalid data and verify errors display correctly
# Check old() input preservation
# Fill form → submit with validation error → verify fields retain values
# Inspect rendered HTML
# Verify Preline UI classes are applied
# Verify accessibility attributes (id, for, aria-*)What to read next
- Form layouts — organize custom fields in responsive layouts
- Validation — client-side and server-side validation patterns
- UI foundation — Preline UI component library