Now let's create a new restaurant form.
This form has the following workflow:
vendor
is created for the ownerThis will be a pretty long lesson: we will create a form, additional Vue components for it, and some extra styling, and then process the form and submit it on the back end.
In RestaurantController
, add a new create()
method to render a new restaurant form:
app/Http/Controllers/Admin/RestaurantController.php
use App\Models\City; // ... public function create(){ $this->authorize('restaurant.create'); return Inertia::render('Admin/Restaurants/Create', [ 'cities' => City::get(['id', 'name']), ]);}
We pass cities
to the Admin/Restaurants/Create
Component for dropdown city selection in the form. Now, let's go to the Vue part and create that form.
Let's define our form component, which will be rendered when we visit the /admin/restaurants/create
route.
Along the way, we must create a few extra Vue components. Laravel Breeze provides functional components for our application like InputLabel
, TextInput
, InputError
, PrimaryButton
etc., but also we will manually create the SelectInput
and TextareaInput
.
This is the complete code of the Create component, with comments below.
resources/js/Pages/Admin/Restaurants/Create.vue
<script setup>import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'import { Head, useForm } from '@inertiajs/vue3'import InputError from '@/Components/InputError.vue'import InputLabel from '@/Components/InputLabel.vue'import PrimaryButton from '@/Components/PrimaryButton.vue'import SelectInput from '@/Components/SelectInput.vue'import TextareaInput from '@/Components/TextareaInput.vue'import TextInput from '@/Components/TextInput.vue' defineProps({ cities: { type: Array }}) const form = useForm({ restaurant_name: '', email: '', owner_name: '', city_id: '', address: ''}) const submit = () => { form.post(route('admin.restaurants.store'))}</script> <template> <Head title="Add New Restaurant" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight">Add New Restaurant</h2> </template> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 overflow-x-scroll"> <div class="p-6 text-gray-900 overflow-x-scroll"> <form @submit.prevent="submit" class="flex flex-col gap-4"> <div class="form-group"> <InputLabel for="restaurant_name" value="Restaurant Name" /> <TextInput id="restaurant_name" type="text" v-model="form.restaurant_name" :disabled="form.processing" /> <InputError :message="form.errors.restaurant_name" /> </div> <div class="form-group"> <InputLabel for="email" value="Owner Email" /> <TextInput id="email" type="email" v-model="form.email" :disabled="form.processing" /> <InputError :message="form.errors.email" /> </div> <div class="form-group"> <InputLabel for="owner_name" value="Owner Name" /> <TextInput id="owner_name" type="text" v-model="form.owner_name" :disabled="form.processing" /> <InputError :message="form.errors.owner_name" /> </div> <div class="form-group"> <InputLabel for="city_id" value="City" /> <SelectInput id="city_id" v-model="form.city_id" :options="cities" option-value="id" option-label="name" :default-option="{ id: '', name: 'City Name' }" :disabled="form.processing" /> <InputError :message="form.errors.city_id" /> </div> <div class="form-group"> <InputLabel for="address" value="Address" /> <TextareaInput id="address" v-model="form.address" class="resize-none" rows="3" :disabled="form.processing" /> <InputError :message="form.errors.address" /> </div> <div> <PrimaryButton :disabled="form.processing">Create New Restaurant</PrimaryButton> </div> </form> </div> </div> </div> </div> </div> </AuthenticatedLayout></template>
As we did earlier, we define the cities
property we pass to this view using the defineProps()
method.
defineProps({ cities: { type: Array }})
Form field data is defined using Inertia's useForm()
method.
const form = useForm({ restaurant_name: '', email: '', owner_name: '', city_id: '', address: ''})
Now we have a form
object that we can bind data to our form components using v-model
:
<InputLabel for="restaurant_name" value="Restaurant Name" /><TextInput id="restaurant_name" type="text" v-model="form.restaurant_name" :disabled="form.processing"/><InputError :message="form.errors.restaurant_name" />
v-model
is used to implement two-way data binding. The form.restaurant_name
value is updated automatically when you enter text into the TextInput
component.
The form
object created using useForm()
method also provides processing
property to disable buttons or input fields when the form is being processed, like this :disabled="form.processing"
.
We can easily submit data of the form
object using the form.post()
method. We can use the route()
method the same way we use it with Blade files.
const submit = () => { form.post(route('admin.restaurants.store'))}
Then we bind the submit
method to the <form>
element. It means "On submit, prevent default html form behavior and call submit
method".
<form @submit.prevent="submit">
Add some CSS classes for the form so all the elements flow nicely. When the form is processed, input fields and buttons will be transparent.
resources/css/app.css
@layer components { // ... .form-group { @apply flex flex-col gap-2 } input[type="text"], input[type="email"], select, textarea { @apply disabled:opacity-50; }}
Now, you saw that SelectInput
and TextareaInput
components I mentioned earlier? Let's define those two.
For a reusable Vue component for select dropdowns, ideally, we would like to have this structure:
<SelectInput v-model="form.some_field" :options="array_of_objects" option-value="id" option-label="name" :default-option="{ id: '', name: 'Please select' }"/>
We want it to be able to:
v-model
to bind the value to the form
objectv-model
value and which to displayWe will mimic the behavior of already existing Laravel Breeze components from resources/js/Components
folder. Let's create a new SelectInput.vue
component there:
resources/js/Components/SelectInput.vue
<script setup>import { onMounted, ref } from 'vue' defineProps({ modelValue: { type: String, required: true }, options: { type: Array, required: true }, optionValue: { type: String, required: true }, optionLabel: { type: String, required: true }, defaultOption: { type: Object, required: false }}) defineEmits(['update:modelValue']) const input = ref(null) onMounted(() => { if (input.value.hasAttribute('autofocus')) { input.value.focus() }}) defineExpose({ focus: () => input.value.focus() })</script> <template> <select class="border-gray-300 focus:border-primary-500 focus:ring-primary-500 rounded-md shadow-sm" :value="modelValue" @change="$emit('update:modelValue', $event.target.value)" ref="input" > <option v-if="defaultOption" disabled hidden :value="defaultOption[optionValue]"> {{ defaultOption[optionLabel] }} </option> <option v-for="option in options" :key="option[optionValue]" :value="option[optionValue]"> {{ option[optionLabel] }} </option> </select></template>
In the same way, we define a new TextareaInput
component. It is primarily a copy of Laravel Breeze's TextInput
component with minor changes.
resources/js/Components/TextareaInput.vue
<script setup>import { onMounted, ref } from 'vue' defineProps({ modelValue: { type: String, required: true }}) defineEmits(['update:modelValue']) const input = ref(null) onMounted(() => { if (input.value.hasAttribute('autofocus')) { input.value.focus() }}) defineExpose({ focus: () => input.value.focus() })</script> <template> <textarea class="border-gray-300 focus:border-primary-500 focus:ring-primary-500 rounded-md shadow-sm" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" ref="input" ></textarea></template>
Here's the visual result of the form:
Ok, so we have built our form. Now let's add a [Add New Restaurant]
button to our Restaurant index page to navigate to it.
You will typically use the Inertia <Link>
component to create links to other pages within an Inertia app. This component is a light wrapper around a standard anchor <a>
link that intercepts click events and prevents full page reloads.
resources/js/Pages/Admin/Restaurants/Index.vue
<script setup>import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'import { Head } from '@inertiajs/vue3' import { Head, Link } from '@inertiajs/vue3' // ...
<template> <!-- ... --> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6"> <Link class="btn btn-primary" :href="route('admin.restaurants.create')"> Add New Restaurant </Link> </div> <div class="p-6 text-gray-900 overflow-x-scroll"> <table class="table"> <thead> <!-- ... --></template>
And finally, add CSS classes for the button.
resources/css/app.css
.btn { @apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-full font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2 transition ease-in-out duration-150 w-full sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed;} .btn-primary { @apply bg-primary-500 text-white hover:bg-primary-600 focus:bg-primary-600 active:bg-primary-500 focus:ring-primary-500}
These CSS changes are optional, but apply them if you want all components to look consistent.
resources/js/Components/InputError.vue
<template> <div v-show="message"> <p class="text-sm text-red-600"> <p class="text-sm text-danger-600"> {{ message }} </p> </div>
resources/js/Components/PrimaryButton.vue
<template> <button class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150" > <button class="btn btn-primary"> <slot /> </button></template>
resources/js/Components/TextInput.vue
<template> <input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" class="border-gray-300 focus:border-primary-500 focus:ring-primary-500 rounded-md shadow-sm" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" ref="input"
The visual result for the button above the table:
Finally, let's return to the back end and submit the form data.
Before creating the store()
method in the Controller, let's generate a Form Request class and add validation rules there.
php artisan make:request Admin/StoreRestaurantRequest
app/Http/Requests/Admin/StoreRestaurantRequest.php
namespace App\Http\Requests\Admin; use App\Models\User;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Support\Facades\Gate; class StoreRestaurantRequest extends FormRequest{ public function authorize(): bool { return Gate::allows('restaurant.create'); } public function rules(): array { return [ 'restaurant_name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class], 'owner_name' => ['required', 'string', 'max:255'], 'city_id' => ['required', 'numeric', 'exists:cities,id'], 'address' => ['required', 'string', 'max:1000'], ]; }}
In the authorize()
method using the Gate
facade, we check if the user has permission to create a restaurant.
We define field names and their validation rules in the rules()
method. A few of them explained:
unique:<table>,<column>
- must be unique in the database table and column. The trick we did here is that it also accepts a Class name, and Laravel automatically resolves the table name from the User
Model.exists:<table>,<column>
- The field value must exist in the database. This means you cannot assign a non-existent city_id
.All available validation rules can be found in the Laravel Documentation.
Then add a new store()
method to the Controller. It will create a new user, assign a role and create a restaurant for the user.
app/Http/Controllers/Admin/RestaurantController.php
use App\Enums\RoleName;use App\Http\Requests\Admin\StoreRestaurantRequest;use App\Models\Role;use App\Models\User;use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\DB; // ... public function store(StoreRestaurantRequest $request): RedirectResponse{ $validated = $request->validated(); DB::transaction(function () use ($validated) { $user = User::create([ 'name' => $validated['owner_name'], 'email' => $validated['email'], 'password' => '', ]); $user->roles()->sync(Role::where('name', RoleName::VENDOR->value)->first()); $user->restaurant()->create([ 'city_id' => $validated['city_id'], 'name' => $validated['restaurant_name'], 'address' => $validated['address'], ]); }); return to_route('admin.restaurants.index');}
We use the DB::transaction()
method to run a set of operations within a Database Transaction. If an Exception is thrown within the transaction closure, the transaction will automatically be rolled back. If the closure executes successfully, the transaction will automatically be committed.
The $validated
variable is invisible in the DB::transaction()
closure, so we need to import it with the use ($validated)
statement.
To redirect to a specific route after successful operations, we use the to_route()
helper in Laravel.
If we fill in the form successfully, we get redirected to the index view and see our new record!
In case of any validation errors, they will be shown near each of the fields.
The final thing in this lesson is to invite the new user - the restaurant owner - to use the system.
There are multiple ways to accomplish this, I suggest this:
Let's create an email text, I made it in Markdown:
resources/views/mail/restaurant/owner-invitation.blade.php:
<x-mail::message> # Hello! {{ __('This email is being sent to you as a notification that an account for :restaurant has been created for you at :app.', [ 'restaurant' => $restaurant, 'app' => config('app.name'), ]) }} {{ __('Please note that new accounts do not have a pre-set password. Therefore, it is necessary for you to set your password manually.') }} <x-mail::button :url="$setUrl"> Set Password </x-mail::button> {{ __('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]) }} {{ __('In the event that the link has expired, you have the option to request a new one.') }} <x-mail::button :url="$requestNewUrl"> Request New Link </x-mail::button> Thanks,<br> {{ config('app.name') }}</x-mail::message>
What parameters do we have inside?
To pass those parameters to Blade, let's create a Notification class to send the email:
php artisan make:notification RestaurantOwnerInvitation
We will pass the restaurant name to that class from the Controller, everything else will be generated inside the Notification class.
app/Notifications/RestaurantOwnerInvitation.php:
namespace App\Notifications; use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Notifications\Messages\MailMessage;use Illuminate\Notifications\Notification;use Illuminate\Support\Facades\Password; class RestaurantOwnerInvitation extends Notification{ use Queueable; /** * Create a new notification instance. */ public function __construct(public string $restaurantName) { } /** * Get the notification's delivery channels. * * @return array<int, string> */ public function via(object $notifiable): array { return ['mail']; } /** * Get the mail representation of the notification. */ public function toMail(object $notifiable): MailMessage { $url = route('password.reset', [ 'token' => Password::createToken($notifiable), 'email' => $notifiable->getEmailForPasswordReset(), ]); return (new MailMessage) ->subject(__('We invite you to join :app to manage :restaurant', [ 'restaurant' => $this->restaurantName, 'app' => config('app.name'), ])) ->markdown('mail.restaurant.owner-invitation', [ 'setUrl' => $url, 'restaurant' => $this->restaurantName, 'requestNewUrl' => route('password.request'), ]); }}
Finally, from the Controller, we fire that Notification, like this:
use App\Notifications\RestaurantOwnerInvitation; // ... class RestaurantController extends Controller{ public function store(StoreRestaurantRequest $request): RedirectResponse { $validated = $request->validated(); DB::transaction(function () use ($validated) { $user->restaurant()->create([ 'city_id' => $validated['city_id'], 'name' => $validated['restaurant_name'], 'address' => $validated['address'], ]); $user->notify(new RestaurantOwnerInvitation($validated['restaurant_name'])); }); return to_route('admin.restaurants.index'); }}
And that's it, now restaurant owner would get the notification email and would be able to click the link and set their password.