Courses

Vue Laravel 12 Starter Kit: CRUD Project

DB Model, New Vue Page and Menu Item

Summary of this lesson:
- Create Task model, migration, and factory with sample data
- Build controller and routes for CRUD operations
- Set up validation with Form Request classes
- Add Vue navigation menu item with an icon for Tasks

Let's try to create a simple CRUD for Tasks with two fields (for now): name and is_completed.

In this lesson, we'll manage the Model/Migration, Routes, and Controllers and add a navigation link in the top menu.


Preparing the Database

First, we create the DB structure with factories to create some fake records:

php artisan make:model Task -mf

The table structure is in Migration.

database/migrations/xxxx_create_tasks_table.php:

public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->boolean('is_completed')->default(false);
$table->timestamps();
});
}

In the Model, we just make the fields fillable and cast is_completed to boolean.

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Task extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'is_completed'
];
 
protected function casts(): array
{
return [
'is_completed' => 'boolean'
];
}
}

Then, the Factory with the rules.

database/factories/TaskFactory.php:

class TaskFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'is_completed' => fake()->boolean(),
];
}
}

Finally, that Factory should be used in the main seeder to create 10 fake task records.

database/seeders/DatabaseSeeder.php:

use App\Models\Task;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'name' => 'Test User',
'email' => '[email protected]',
]);
 
Task::factory()->count(10)->create();
}
}

And then we run the command:

php artisan migrate --seed

As a result, we have 10 records in the DB.


Controller and Routes

We will create a Resource Controller to manage the tasks with this command:

php artisan make:controller TaskController --resource --model=Task

Then, we assign that Controller to the Routes.

routes/web.php:

use App\Http\Controllers\TaskController;
 
// ...
 
Route::get('dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {
return Inertia::render('Dashboard');
})->name('dashboard');
 
Route::resource('tasks', TaskController::class);
});

Now, what's inside that Controller?

We will fill it with the CRUD actions, with Inertia referencing the new Vue.js components that don't yet exist. We will actually create them in the next lesson.

We will also need validation rules. I prefer to use Form Request classes, so I will generate them right away.

php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest

And here are the contents.

app/Http/Requests/StoreTaskRequest.php:

class StoreTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
];
}
}

app/Http/Requests/UpdateTaskRequest.php:

class UpdateTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'is_completed' => ['boolean'],
];
}
}

Almost identical, with one difference: the create form will not contain the is_completed field, so it's not present in the rules() list.

Now, here's our Controller.

app/Http/Controllers/TaskController.php:

namespace App\Http\Controllers;
 
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use App\Models\Task;
use Inertia\Inertia;
 
class TaskController extends Controller
{
public function index()
{
return Inertia::render('Tasks/Index', [
'tasks' => Task::all(),
]);
}
 
public function create()
{
return Inertia::render('Tasks/Create');
}
 
public function store(StoreTaskRequest $request)
{
Task::create($request->validated() + ['is_completed' => false]);
 
return redirect()->route('tasks.index');
}
 
public function edit(Task $task)
{
return Inertia::render('Tasks/Edit', [
'task' => $task,
]);
}
 
public function update(UpdateTaskRequest $request, Task $task)
{
$task->update($request->validated());
 
return redirect()->route('tasks.index');
}
 
public function destroy(Task $task)
{
$task->delete();
 
return redirect()->route('tasks.index');
}
}

Great, now how do we test if it works?

Let's add a menu item leading to the tasks.index route.


Index Vue Component and Navigation Item

In the index() method of the Controller, we return this Vue component:

return Inertia::render('Tasks/Index');

By default, Inertia is configured to return Vue components from the resources/js/pages folder.

So, let's create that file, which is almost empty for now.

resources/js/pages/Tasks/Index.vue:

<script setup lang="ts">
import AppLayout from '@/layouts/AppLayout.vue';
import { Head } from '@inertiajs/vue3';
</script>
 
<template>
<AppLayout>
<Head title="Index" />
<div>The list will be here.</div>
</AppLayout>
</template>

As you can see, the structure of this Vue component is very simple:

  • Import the Layout and the Head in the script section
  • Use both of them in the HTML of the template

Now, let's add the new menu in the navigation.

resources/js/components/AppHeader.vue:

<script setup lang="ts">
import Breadcrumbs from '@/components/Breadcrumbs.vue';
 
// ... many more imports
 
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-vue-next';
import { BookOpen, BriefcaseIcon, Folder, LayoutGrid, Menu, Search } from 'lucide-vue-next';
 
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
url: '/dashboard',
icon: LayoutGrid,
},
{
title: 'Tasks',
url: '/tasks',
icon: BriefcaseIcon,
},
];
 
</script>
 
// ... the rest of the long file

As you can see, we're importing the BriefcaseIcon and adding a new item to the array of mainNavItems. As a result, we have a menu on top, and if we click it, we see our component, with static text for now:

Great, we've created our first route and page outside the default starter kit!

Here is the GitHub commit for this lesson.


In the next lesson, we will build the actual CRUD with all the Vue.js components for it.

Previous: Customize Layout with Vue/Tailwind

No comments yet…

avatar
You can use Markdown