Re-usability has been always the buzz word in the coding community. Every software developer everyday think about it, at least one time in his/her daily work.
One of the most common scenarios is reusing forms. As we all know creating and updating/editing an entity have same form structure, so being a responsible web artisans we all try to avoid repeating ourselves by not creating two separate forms for two similar operations.
Here is an approach that I follow, when I found myself in such situation.
Let me take an example, suppose I have a Post model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'body',
'type',
];
}
I have some routes
<?php
Route::get('/posts', [PostController::class, 'index'])->name('posts');
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('post.store');
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
Route::post('/posts/{post}/update', [PostController::class, 'update'])->name('posts.update');
And the controller, which is responsible for passing the form_state to the front end. Checkout the edit() method.
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class PostController extends Controller
{
private array $post_types = [
'blog' => 'Blog',
'article' => 'Article',
'basic page' => 'Basic Page',
];
public function index()
{
return inertia('Posts/Index', [
'posts' => Post::all(),
]);
}
public function create()
{
return inertia('Posts/Create', [
'types' => $this->post_types,
'form_state' => [],
]);
}
public function store()
{
$post = new Post;
$post->fill(request()->validate([
'title' => ['required', 'max:255'],
'body' => ['required', 'max:65535'],
'type' => ['required', Rule::in(array_keys($this->post_types))]
]))->save();
return back(303);
}
public function edit(Post $post)
{
return inertia('Posts/Edit', [
'types' => $this->post_types,
'form_state' => $post->only([
'id',
'title',
'body',
'type'
]),
]);
}
public function update(Post $post) {
$post->fill(request()->validate([
'title' => ['required', 'max:255'],
'body' => ['required', 'max:65535'],
'type' => ['required', Rule::in(array_keys($this->post_types))]
]))->save();
return back(303);
}
}
At the front end It has following code for creating and updating a post, notice in these files I am accepting the form_state from the backend passing it over to the Form.vue
Create.vue
<script setup lang="ts">
import GuestLayout from '@/Layouts/GuestLayout.vue';
import { Head } from '@inertiajs/vue3';
import Form from "@/Pages/Posts/Form.vue";
import {PostFormState} from "@/Pages/Posts/interface";
defineProps<{
types: Object,
form_state: PostFormState
}>();
</script>
<template>
<GuestLayout>
<Head title="Log in" />
<Form :types="types" :form_state="form_state"/>
</GuestLayout>
</template>
Edit.vue
<script setup lang="ts">
import GuestLayout from '@/Layouts/GuestLayout.vue';
import { Head } from '@inertiajs/vue3';
import Form from "@/Pages/Posts/Form.vue";
import {PostFormState} from "@/Pages/Posts/interface";
defineProps<{
types: Object;
form_state: PostFormState,
}>();
</script>
<template>
<GuestLayout>
<Head title="Log in" />
<Form :types="types" :form_state="form_state" />
</GuestLayout>
</template>
Now comes the Form.vue which is common for both edit and create operations.
<script setup lang="ts">
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import {router, useForm} from '@inertiajs/vue3';
import {PostFormState} from "@/Pages/Posts/interface";
const props = defineProps<{
types: Object,
form_state: PostFormState,
}>();
const form = useForm(Object.assign({
title: '',
body: '',
type: 'blog',
}, props.form_state));
const submit = () => {
let path = props.form_state.id == null ? route('post.store') : route('posts.update', {post: props.form_state.id})
form.post(path, {
onFinish: () => {
form.reset()
router.visit(route('posts'))
},
});
};
</script>
<template>
<form @submit.prevent="submit">
<div>
<InputLabel for="title" value="Title"/>
<TextInput
id="title"
type="text"
class="mt-1 block w-full"
v-model="form.title"
required
/>
<InputError class="mt-2" :message="form.errors.title"/>
</div>
<div class="mt-4">
<InputLabel for="body" value="Body"/>
<textarea
id="body"
type="body"
class="mt-1 block w-full"
v-model="form.body"
required></textarea>
<InputError class="mt-2" :message="form.errors.body"/>
</div>
<div class="mt-4">
<InputLabel for="type" value="Type"/>
<select v-model="form.type">
<option v-for="(type,id) in types" :value="id" :key="id">{{ type }}</option>
</select>
<InputError class="mt-2" :message="form.errors.type"/>
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton
class="ml-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
v-text="form_state?.id ? 'Update' : 'Create'"
>
</PrimaryButton>
</div>
</form>
</template>
here in this code I have defined the prop form_state and based on that decided weather the form is for creation or editing a post.
const form = useForm(Object.assign({
title: '',
body: '',
type: 'blog',
}, props.form_state));
this code is mainly responsible for maintaining the form state. It is initializing the form with the form_state coming from the backend and making values default.
I hope you enjoyed the article and learned something new today.
Thank you!
Video on the same topic: