Courses

Vue Laravel 12 Starter Kit: CRUD Project

Due Date: Using Shadcn Vue Date Picker

Summary of this lesson:
- Add a `due_date` field to tasks with database integration
- Implement date picker with Shadcn Date Picker component
- Configure TypeScript types and date formatting
- Display and edit dates in task listing and forms

Now, let's try to add one more field to our form and database: due_date with a date picker.


Prepare Database/Controller

I will just show you the code without any comments. These are Laravel fundamentals, I'm sure you know them.

Migration:

php artisan make:migration add_due_date_to_tasks_table
Schema::table('tasks', function (Blueprint $table) {
$table->date('due_date')->nullable();
});

Then, we run:

php artisan migrate

Adding to Model fillables:

app/Models/Task.php:

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

Validation rules:

app/Http/Requests/StoreTaskRequest.php:

// ...
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'due_date' => ['nullable', 'date'],
];
}

app/Http/Requests/UpdateTaskRequest.php:

public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'is_completed' => ['required', 'boolean'],
'due_date' => ['nullable', 'date'],
];
}

No modifications are needed for the Controller since we use $request->validated() to create/update tasks.

Ok, the back end is ready. It's time to dive into the JavaScript/TypeScript side.


Adding Field to TypeScript Types

We need to update our type in the file of all types:

resources/js/types/index.d.ts

// ...
 
export interface Task {
id: number;
name: string;
is_completed: boolean;
due_date?: string;
created_at: string;
updated_at: string;
}

Also, in our Create.vue page, we need to add this field to the form's internal types.

resources/js/pages/Tasks/Create.vue

<script>
// ...
 
const form = useForm({
name: '',
due_date: null
});
</script>

Adding the Input Date to the Form

Then, we finally create the date input in our form:

First, we import shadcn components:

npx shadcn-vue@latest add popover
npx shadcn-vue@latest add calendar

Then, we add the date input to the form:

resources/js/pages/Tasks/Create.vue

<script setup lang="ts">
// ...
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Head, useForm } from '@inertiajs/vue3';
import { DateFormatter, getLocalTimeZone } from '@internationalized/date';
import { CalendarIcon } from 'lucide-vue-next';
 
const df = new DateFormatter('en-US', {
dateStyle: 'long',
});
 
const submitForm = () => {
form..post(route('tasks.store'), {
form.transform((data) => ({
...data,
due_date: data.due_date ? data.due_date.toDate(getLocalTimeZone()) : null,
})).post(route('tasks.store'), {
preserveScroll: true,
});
};
// ...
</script>
 
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Create Task" />
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
<form class="space-y-6" @submit.prevent="submitForm">
// ...
<div class="grid gap-2">
<Label htmlFor="name">Due Date</Label>
 
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('w-[280px] justify-start text-left font-normal', !form.due_date && 'text-muted-foreground')"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ form.due_date ? df.format(form.due_date.toDate(getLocalTimeZone())) : 'Pick a date' }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="form.due_date" initial-focus />
</PopoverContent>
</Popover>
 
<InputError :message="form.errors.due_date" />
</div>
// ...
</form>
</div>
</AppLayout>
</template>

Here's the visual result:

And if we submit the form, the due_date is successfully saved into the DB:


Show Date in the Table

This one is easy. Just add a few lines in the Index.vue and import the library for date formatting.

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

<script>
// ...
 
import { TablePagination } from '@/components/table-pagination';
import { DateFormatter } from '@internationalized/date';
 
const df = new DateFormatter('en-US', {
dateStyle: 'long',
});
 
// ...
</script>
 
<template>
<TableHeader>
<TableRow>
<TableHead>Task</TableHead>
<TableHead class="w-[100px]">Status</TableHead>
<TableHead class="w-[200px]">Status</TableHead>
<TableHead class="w-[200px]">Due Date</TableHead>
<TableHead class="w-[200px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="task in tasks.data" :key="task.id">
<TableCell>{{ task.name }}</TableCell>
<TableCell :class="{ 'text-green-600': task.is_completed, 'text-red-700': !task.is_completed }">
{{ task.is_completed ? 'Completed' : 'In Progress' }}
</TableCell>
<TableCell>{{ task.due_date ? df.format(new Date(task.due_date)) : '' }}</TableCell>
<TableCell class="flex gap-x-2 text-right">
<Link :class="buttonVariants({ variant: 'default' })" :href="`/tasks/${task.id}/edit`">Edit </Link>
<Button variant="destructive" @click="deleteTask(task.id)" class="mr-2">Delete</Button>
</TableCell>
</TableRow>
</TableBody>
</template>

And here's the visual result:


Finally: The Edit Form

This one will also be easy, repeating the Create form logic and passing the due_date where needed.

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

<script setup lang="ts">
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { DateFormatter, fromDate, getLocalTimeZone } from '@internationalized/date';
import { CalendarIcon } from 'lucide-vue-next';
 
// ...
 
const df = new DateFormatter('en-US', {
dateStyle: 'long',
});
 
const form = useForm({
name: task.name,
is_completed: task.is_completed,
due_date: task.due_date ? fromDate(new Date(task.due_date)) : null,
});
 
const submitForm = () => {
form.put(route('tasks.update', task.id), {
form.transform((data) => ({
...data,
due_date: data.due_date ? data.due_date.toDate(getLocalTimeZone()) : null,
})).put(route('tasks.update', task.id), {
preserveScroll: true,
});
};
</script>
 
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Edit Task" />
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
<form class="space-y-6" @submit.prevent="submitForm">
// ...
 
<div class="grid gap-2">
<Label htmlFor="name">Due Date</Label>
 
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('w-[280px] justify-start text-left font-normal', !form.due_date && 'text-muted-foreground')"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ form.due_date ? df.format(new Date(form.due_date.toDate(getLocalTimeZone()))) : 'Pick a date' }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="form.due_date" initial-focus />
</PopoverContent>
</Popover>
 
<InputError :message="form.errors.due_date" />
</div>
 
// ...
</form>
</div>
</AppLayout>
</template>

And the Edit form works, including passing the value from the database!

Here's the GitHub commit for this lesson.

Previous: Table Pagination with ShadCN Pagination Component

No comments yet…

avatar
You can use Markdown