New Resort
Every LaraForm field type on one form. Put the mouse away: Enter advances
(Shift+Enter back), the date takes 31/12 or 311224,
selects open on focus, the city combobox searches remotely, GST takes
y/n, and Enter on Manager opens the contact panel.
No Livewire, no page JS — assets auto-inject.
Prefer an edit round-trip? Edit “Hello” to see defaults, canonical values and old() repopulation.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Resort;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use LaraForm\Fields\CheckboxField;
use LaraForm\Fields\CheckboxGroupField;
use LaraForm\Fields\DateField;
use LaraForm\Fields\IntegerField;
use LaraForm\Fields\MoneyField;
use LaraForm\Fields\MultiSelectField;
use LaraForm\Fields\RadioField;
use LaraForm\Fields\SearchSelectField;
use LaraForm\Fields\SelectField;
use LaraForm\Fields\TextareaField;
use LaraForm\Fields\TextField;
use LaraForm\Fields\ToggleField;
use LaraForm\Fields\YesNoField;
use LaraForm\Form;
use LaraForm\Panel;
/**
* New/Edit Resort — the LaraForm showcase: every field type on one form, entirely
* keyboard-drivable (Enter advances, dates type freeform, selects open on focus, the
* Manager field opens a package-rendered contact panel), no Livewire and no page JS.
*
* Grid tie-in: active maps to the visibility column and room_rate to comparison_tariff,
* so a saved resort shows up correctly in the LaraGrid demo at /resorts.
*/
class ResortFormController extends Controller
{
private const TYPES = ['Homestay', 'Resort', 'Hotel', 'Guest House', 'Package', 'Activities', 'Hospitality', 'Star Category'];
private const AMENITIES = ['pool' => 'Swimming Pool', 'spa' => 'Spa', 'gym' => 'Gym', 'wifi' => 'Free WiFi', 'restaurant' => 'Restaurant', 'bar' => 'Bar', 'kids' => 'Kids Club', 'beach' => 'Beach Access'];
private const FACILITIES = ['parking' => 'Parking', 'ev' => 'EV Charging', 'pets' => 'Pet Friendly', 'wheelchair' => 'Wheelchair Access'];
private const STARS = ['3' => '3 Star', '4' => '4 Star', '5' => '5 Star', 'unrated' => 'Unrated'];
private const CITIES = [
'Agra', 'Ahmedabad', 'Bengaluru', 'Bhopal', 'Chandigarh', 'Chennai', 'Coimbatore',
'Darjeeling', 'Dehradun', 'Goa', 'Gokarna', 'Hyderabad', 'Jaipur', 'Kochi', 'Kolkata',
'Leh', 'Lucknow', 'Madurai', 'Manali', 'Mumbai', 'Munnar', 'Mysuru', 'Nainital',
'Ooty', 'Pondicherry', 'Pune', 'Rishikesh', 'Shimla', 'Udaipur', 'Varanasi', 'Wayanad',
];
public function create(): View
{
return view('resorts.form', [
'form' => $this->buildForm(),
'heading' => 'New Resort',
'latest' => Resort::query()->latest('id')->first(),
]);
}
public function store(Request $request): RedirectResponse
{
$resort = $this->persist($request, new Resort());
return redirect()->route('resorts.create')
->with('status', "Resort “{$resort->name}” saved (#{$resort->id}).");
}
public function edit(Resort $resort): View
{
return view('resorts.form', [
'form' => $this->buildForm($resort),
'heading' => "Edit Resort — {$resort->name}",
'latest' => null,
]);
}
public function update(Request $request, Resort $resort): RedirectResponse
{
$this->persist($request, $resort);
return redirect()->route('resorts.edit', $resort)
->with('status', "Resort “{$resort->name}” updated.");
}
/** Remote source for the city search-select: GET /api/cities?q= → [{value, label}]. */
public function cities(Request $request): JsonResponse
{
$q = mb_strtolower(trim((string) $request->query('q', '')));
$matches = array_values(array_filter(
self::CITIES,
fn (string $city): bool => $q === '' || str_contains(mb_strtolower($city), $q),
));
return response()->json(array_map(
fn (string $city): array => ['value' => $city, 'label' => $city],
array_slice($matches, 0, 12),
));
}
/**
* The whole form, declared fluently — create and edit differ only in defaults,
* action and method. Every LaraForm field type appears once.
*/
private function buildForm(?Resort $resort = null): Form
{
$editing = $resort !== null;
return Form::make()
->action($editing ? route('resorts.update', $resort) : route('resorts.store'))
->method($editing ? 'PUT' : 'POST')
->submit($editing ? 'Update Resort' : 'Save Resort')
->fields([
TextField::make('name')->label('Resort name')->required()->autofocus()
->placeholder('e.g. Coral Cove Beach Resort')
->rules('max:255')
->default($resort?->name)->class('span-2'),
TextField::make('shortcode')->label('Short code')->upper()
->minLength(2)->maxLength(8)
->help('Forced UPPERCASE · 2–8 characters')->default($resort?->shortcode),
SelectField::make('type')->label('Type')->placeholder('Pick a type…')
->options(self::TYPES)->required()->default($resort?->type),
SearchSelectField::make('city')->label('City')
->optionsUrl(route('cities.search'))
->placeholder('Type to search…')
->help('Remote combobox — fetches ?q= as you type')
->default($resort?->city ? ['value' => $resort->city, 'label' => $resort->city] : null),
DateField::make('opened_on')->label('Opened on')->financialYear(4)
->help('Freeform: 31/12 · 31.12.24 · 311224 — FY-aware year inference')
->default($resort?->opened_on),
MoneyField::make('room_rate')->label('Room rate / night')->indian()->prefix('₹')
->help('₹1000–₹3000 for the demo — Enter refuses values outside')
->min(1000)
->max(3000)
->required()
->default($resort?->room_rate),
IntegerField::make('rooms')->label('Total rooms')->min(0)->default($resort?->rooms),
RadioField::make('star_rating')->label('Star rating')->inline()
->options(self::STARS)->default($resort?->star_rating),
MultiSelectField::make('amenities')->label('Amenities')->max(5)
->options(self::AMENITIES)
->help('Enter toggles · Backspace removes the last chip · max 5')
->default($resort?->amenities)->class('span-2'),
CheckboxGroupField::make('facilities')->label('Facilities')->inline()
->options(self::FACILITIES)->default($resort?->facilities)->class('span-2'),
YesNoField::make('gst_applicable')->label('GST applicable?')
->help('Type y / n — Space or ↑↓ toggles')
->default($resort?->gst_applicable),
ToggleField::make('active')->label('Visible on site')
->help('Maps to the grid’s visibility column')
->default($editing ? $resort->visibility === 'show' : true),
CheckboxField::make('featured')->label('Feature on homepage')
->default($resort?->featured),
TextField::make('manager')->label('Manager')
->placeholder('Name — Enter opens contact panel')
->default($resort?->manager)
->opensPanel(
Panel::make('manager-contact')
->title('Manager contact')
->fields([
TextField::make('contact_phone')->label('Phone')->type('tel')
->default($resort?->contact_phone),
TextField::make('contact_email')->label('Email')->email()
->default($resort?->contact_email),
]),
),
TextareaField::make('description')->label('Description')->rows(3)
->placeholder('Enter = newline here; Ctrl+Enter advances')
->default($resort?->description)->class('span-2'),
]);
}
private function persist(Request $request, Resort $resort): Resort
{
// One source of truth: the form definition supplies presence + type + option
// rules (required, string, numeric+min/max, date_format:Y-m-d, in:options,
// array + members, in:0,1 — panel fields included). Only app-specific
// constraints ride on top.
$data = $this->buildForm()->validate($request, [
'city' => [Rule::in(self::CITIES)],
'manager' => ['max:255'],
'contact_phone' => ['max:30'],
'description' => ['max:2000'],
]);
// 'active' is form-only (it maps to visibility below), never a column.
$attributes = collect($data)->except('active')->all();
$resort->fill([
...$attributes,
'slug' => str($data['name'])->slug()->value(),
'gst_applicable' => isset($data['gst_applicable']) ? (bool) $data['gst_applicable'] : null,
'featured' => (bool) $data['featured'],
// The two mappings that keep the LaraGrid demo page coherent:
'visibility' => $data['active'] === '1' ? 'show' : 'hide',
'comparison_tariff' => isset($data['room_rate']) ? (int) round((float) $data['room_rate']) : null,
]);
$resort->save();
return $resort;
}
}
<x-layouts.app>
@push('styles')
<style>
.page-intro { color: #52525b; font-size: .9rem; margin: 0 0 1.25rem; max-width: 60ch; }
.status-banner { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46;
border-radius: .5rem; padding: .6rem .9rem; font-size: .875rem; margin-bottom: 1.25rem; }
.status-banner a { color: #047857; font-weight: 600; }
.edit-hint { font-size: .8125rem; color: #71717a; margin: -0.5rem 0 1.25rem; }
.resort-form { background: #fff; border: 1px solid #e4e4e7; border-radius: .75rem;
padding: 1.5rem; max-width: 56rem; margin-bottom: 2rem; }
/* Two-column flow: LaraForm fields are plain blocks, so a CSS grid on the
<form> is all the layout this page needs. */
.resort-form form.lf-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 1.5rem; }
.resort-form .span-2, .resort-form .lf-actions { grid-column: 1 / -1; }
@media (max-width: 640px) {
.resort-form form.lf-form { grid-template-columns: 1fr; }
}
</style>
@endpush
<h1>{{ $heading }}</h1>
<p class="page-intro">
Every LaraForm field type on one form. Put the mouse away: Enter advances
(Shift+Enter back), the date takes <code>31/12</code> or <code>311224</code>,
selects open on focus, the city combobox searches remotely, GST takes
<code>y</code>/<code>n</code>, and Enter on Manager opens the contact panel.
No Livewire, no page JS — assets auto-inject.
</p>
@if (session('status'))
<div class="status-banner">
{{ session('status') }} <a href="/resorts">View it in the grid →</a>
</div>
@endif
@if ($latest !== null)
<p class="edit-hint">
Prefer an edit round-trip? <a href="{{ route('resorts.edit', $latest) }}">Edit “{{ $latest->name }}”</a>
to see defaults, canonical values and old() repopulation.
</p>
@endif
<div class="resort-form">
{!! $form !!}
</div>
<x-source-code
title="Source"
:files="[
'app/Http/Controllers/ResortFormController.php',
'resources/views/resorts/form.blade.php',
]"
/>
</x-layouts.app>