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 popovernpx 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.
No comments yet…