Courses

Vue Laravel 12 Starter Kit: CRUD Project

Shadcn Vue Table: Tasks List

Summary of this lesson:
- Set up Shadcn Vue UI table component with npx command
- Create TypeScript interface for Task data model
- Display tasks with conditional styling for completion status

In the following lessons, we will fill our "Tasks" page with the full CRUD. Let's start with a table.


Table Component from Shadcn

Let's show the table of tasks. Remember, we had seeded it into the database previously:

Vue starter kit uses a Vue+Tailwind Shadcn UI library so you don't need to install it, it comes already pre-configured.

Generally, using Shadcn components is very easy:

  1. Install the component with npx
  2. Import it on top of your Vue file
  3. Use it inside your Vue file template

Here's the screenshot from the official Shadcn docs for Laravel.

Shadcn has many components:

For this example, we will use Table component, follow its documentation and apply it to our Tasks List page.

Shadcn components are installed with the npx command and then should be imported on top of our Vue script.

npx shadcn-vue@latest add table

This will add a new folder with files at resources/js/components/ui/table which we don't need to edit. We will just use the table components from there.

However, Shadcn emphasizes it as one of its strongest features: the Open Code. It means that components are not in node_modules or vendor. They are in your resources/js/components/ui/ folder, and you can modify the styling if you wish to.

Now, let's use the <Table> component. Here's the screenshot from the official docs:

So, let's try to repeat exactly that. For now, let's build the static table with hard-coded row.

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

<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import AppLayout from '@/layouts/AppLayout.vue';
import { Head } from '@inertiajs/vue3';
</script>
 
<template>
<AppLayout>
<Head title="Index" />
<Table class="mt-4">
<TableHeader>
<TableRow>
<TableHead>Task</TableHead>
<TableHead class="w-[100px]">Status</TableHead>
<TableHead class="w-[100px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Task Name</TableCell>
<TableCell>Completed or In progress</TableCell>
<TableCell class="text-right">Button to edit/delete</TableCell>
</TableRow>
</TableBody>
</Table>
</AppLayout>
</template>

We have our table!

Now, let's add actual data to it.


Tasks: Controller to TypeScript Interface/Type

Remember what we have in the Controller:

app/Http/Controllers/TaskController.php:

public function index()
{
return Inertia::render('Tasks/Index', [
'tasks' => Task::all(),
]);
}

So, we pass the tasks variable to the Vue component.

Now, let's get familiar with TypeScript and create a Task type. Then, our Vue component would have IDE auto-complete and strict validation, so that no incorrect data could be passed.

These are the main two goals of TypeScript, similar to how strict types became more popular in PHP.

For defining types, we have this file, already pre-filled by the starter kit:

resources/js/types/index.d.ts:

import type { PageProps } from '@inertiajs/core';
import type { LucideIcon } from 'lucide-vue-next';
import type { Config } from 'ziggy-js';
 
export interface Auth {
user: User;
}
 
export interface BreadcrumbItem {
title: string;
href: string;
}
 
export interface NavItem {
title: string;
href: string;
icon?: LucideIcon;
isActive?: boolean;
}
 
// ...

Remember the NavItem? We have already used it, right?

Yes, while adding the new menu to the navigation:

const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
url: '/dashboard',
icon: LayoutGrid,
},
{
title: 'Tasks',
url: '/tasks',
icon: BriefcaseIcon,
},
];

See that NavItem[] here? It means that this array of mainNavItems is not just any array. It's an array where each element is of the type of NavItem.

This way, other developers won't make typos in the title / url / icon structure. TypeScript will enforce those fields and throw errors otherwise.

So, we add our own interface to the same file:

resources/js/types/index.d.ts:

export interface NavItem {
title: string;
url: string;
icon?: LucideIcon | null;
isActive?: boolean;
}
 
// ...
 
export interface Task {
id: number;
name: string;
is_completed: boolean;
created_at: string;
updated_at: string;
}

Then, we can pass our tasks object from the Controller and accept the typed array in the Vue component.

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

<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import AppLayout from '@/layouts/AppLayout.vue';
import { Head } from '@inertiajs/vue3';
import { Task } from '@/types';
 
interface Props {
tasks: Task[];
}
 
defineProps<Props>();
</script>

This syntax tasks: Task[] defines that your component accepts the array of tasks where each element is of type Task.


Real Tasks in the Table

Now, let's use the tasks in our table and build a loop instead of the hard-coded rows. In Vue, it's done with the v-for directive:

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

BEFORE:

<TableBody>
<TableRow>
<TableCell>Task Name</TableCell>
<TableCell>Completed or In progress</TableCell>
<TableCell class="text-right">Button to edit/delete</TableCell>
</TableRow>
</TableBody>

AFTER:

<TableBody>
<TableRow v-for="task in tasks" :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 class="text-right">Button to edit/delete</TableCell>
</TableRow>
</TableBody>

The v-for method is a Vue/JS way to render an array list, equivalent to PHP foreach loop. You can read more about rendering lists in the Vue documentation.

And here's the visual result:


Is Using TypeScript a Requirement?

Similarly to PHP type-hinting and return types, TypeScript is totally optional. It may be added "on top" of general Vue/Vue components to enforce strictness. Laravel 12 starter kits and Shadcn library both use TypeScript, and people on Reddit choose TypeScript over JavaScript, but you don't have to.

Some of you may not like that extra step of defining types. No problem, feel free to create your future Vue components in regular JavaScript, without TypeScript. The Laravel starter kit includes the first types, but you don't have to continue this and create new types.

In that case, you just don't add anything to the resources/js/types/index.d.ts, and your Vue component code is this:

<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import AppLayout from '@/layouts/AppLayout.vue';
import { Task } from '@/types';
import { Head } from '@inertiajs/vue3';
 
interface Props {
tasks: Task[];
tasks
}
 
defineProps<Props>();
</script>

Keep in mind that you may get IDE warnings that the tasks variable now has the any type.

But no worries, the table will still work well.

So, if you want to proceed without TypeScript in your project, feel free to do it.


Great, we have our table with data.

Here is the GitHub commit for this lesson.

In the following lessons, we'll take care of the Delete action.

Previous: DB Model, New Vue Page and Menu Item
avatar

First of all thank you for this series and your contributions to the Laravel and PHP community in general!

I'd love to see a lesson about using Laravel 12 Vue Starter Kits with the Shadcn DataTable component. I realize this series is more a of gentle introduction, but being able to search, sort and paginate (while honoring previous searches and sorts) would be awesome!

I've used the spatie/laravel-query-builder package for something similar in the past.

avatar

The problem with DataTable component is - handling all of the cases for table actions. It's definitely not about the display of the table itself, but more about the whole API code (controller).

Just because of this reason - we are for now not focusing on it as it's a huge job to properly prepare an example (otherwise, we will get a lot of questions that some basic function does not work) :)

We might revisit this at a later date, but at the moment we are prioritizing Laravel 12 updates=

avatar

Yep, totally understand! I'd love to see a dedicated tutorial on Vue + Shadcn DataTable components. Cheers :)

avatar
You can use Markdown
avatar
You can use Markdown