Table of contents
- 1. Create new Laravel Project
- 2. Configure database credentials.
- 3. Configure Auth
- 4. Configure model Product
- 5. Create Product Controller
- 6. Create Product Resource
- 7. Create Product Requests
- 8. Defining routes
- 9. Installing Vue.js 3
- 10. Configure basic structure
- 11. Composition API
- 12. Installing and Configure PrimeVue DataTable
- 13. Products Create Component
- 14. Delete Product
- 15. Products Edit Component
- 16. GitHub Repo
In this tutorial, we are going to learn how we can create a CRUD with Vue.js 3 using Composition API and PrimeVue.
1. Create new Laravel Project
To create a new project execute this command in your console.
composer create-project laravel/laravel laravel-vuejs-crud
2. Configure database credentials.
Open your .env and edit it.
3. Configure Auth
In this step, we need to install Laravel Breeze, to do that execute this command
composer require laravel/breeze
After installing the package, run
php artisan breeze:install
To finalize this part, execute
npm install && npm run dev
Before creating any user you need to run
php artisan migrate
Very cool!!!. Now we can access to register and login page.
4. Configure model Product
In the terminal execute
php artisan make:model Product -mf
Let's go to the product migration, located in database/migrations
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('price', 12, 2);
$table->string('description')->nullable();
$table->timestamps();
});
}
Once the migrations are ready, we can go to app/Models/Product.php and edit it like this.
protected $fillable = [
'name',
'price',
'description'
];
Let's go to the factories in database/factories, open the ProductFactory file, and add
public function definition()
{
return [
'name' => $this->faker->word(),
'price' => $this->faker->numberBetween(20, 100),
'description' => $this->faker->sentence()
];
}
The final step is to go to database/seeders/DatabaseSeeder.php and put this code
public function run()
{
Product::factory(35)->create();
}
Execute this command.
php artisan migrate --seed
Alright. Our data is ready.
5. Create Product Controller
Create a new Product controller with this command
php artisan make:controller API/ProductController --api --model=Product
Just leave the controller for a moment, we will back to configure it later in this tutorial.
6. Create Product Resource
In this tutorial, I won't make any transformation of data but I prefer to use resources.
php artisan make:resource ProductResource
The next step is to create the validation.
7. Create Product Requests
We need to create two files for validating the data:
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest
Let's go to app/Http/Requests/StoreProductRequest.php, add this code:
class StoreProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => [
'required'
],
'price' => [
'numeric',
'required',
],
'description' => [
'nullable'
],
'image' => [
'image',
'mimes:jpg,jpeg,png'
]
];
}
}
Note: the UpdateProductRequest file have the same rules that StoreProductRequest
After that, let's go to our controller app/Http/Controllers/API/ProductController.php and add this code
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
class ProductController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return ProductResource::collection(Product::all());
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\StoreProductRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return new ProductResource($product);
}
/**
* Display the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function show(Product $product)
{
return new ProductResource($product);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\UpdateProductRequest $request
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->validated());
return new ProductResource($product);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function destroy(Product $product)
{
$product->delete();
return response()->noContent();
}
}
8. Defining routes
Everything is ready, now we can define our routes, in the routes/api.php file add the controller
Route::apiResource('products', ProductController::class);
Open routes/web.php and add this route
Route::view('/{any}', 'dashboard')
->middleware(['auth'])
->where('any', '.*');
Let's move on to the interesting part of this tutorial, integrate Vue.js 3.
9. Installing Vue.js 3
To install Vue.js 3 execute the following command
npm install vue@next vue-loader@next vue-router@next
Note: If you don't use @next
, you'll install vue.js version 2.
The next step is to go to webpack.mix.js located in the root of the project and add .vue() like this
mix.js('resources/js/app.js', 'public/js').vue().postCss('resources/css/app.css', 'public/css', [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer'),
]);
10. Configure basic structure
In this step, we need to create the following structure
When you finish that part, we need to move to resources/js/components/products/Index.vue, and add a simple Hello World
<template>
<div>
Hello World Vue.js + Laravel !!!
</div>
</template>
<script>
export default {
setup() {
},
}
</script>
Now, open the resources/js/router/index.js and create the routes of our project with Vue-Router:
import { createRouter, createWebHistory } from 'vue-router'
import ProductsIndex from '../components/products/Index.vue'
const routes = [
{
path: '/dashboard',
name: 'products.index',
component: ProductsIndex
}
]
export default createRouter({
history: createWebHistory(),
routes
})
Now, go to resources/views/layouts/app.blade.php and change some stuff
Original
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
Change to
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
It's important to change asset to mix because you prevent the page reload on cache every time.
One more thing add id=app here:
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100" id="app">
@include('layouts.navigation')
We can search for resources/views/dashboard.blade.php and put the router-view like this
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<router-view />
</div>
</div>
</div>
</div>
Almost done. Now we have to search for resources/js/app.js file.
require('./bootstrap');
require('alpinejs');
import { createApp } from 'vue';
import router from './router'
import CompaniesIndex from './components/companies/CompaniesIndex.vue';
const app = createApp({
components: {
CompaniesIndex
}
});
app.use(router);
app.mount('#app');
That's it, run this command in your terminal.
npm run dev
Note: You can use npm run watch if you want to check changes in live mode.
If everything goes well you can see this screen.
11. Composition API
Composition API: will allow us to create reusable and stateful business logic and make it easier to organize. You can read more about it in this link Composition API
Let's go to resources/js/composables/products.js, when you get there put this code
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
export default function useProducts() {
const product = ref([])
const products = ref([])
const errors = ref('')
const router = useRouter()
const getProducts = async () => {
const response = await axios.get('/api/products');
products.value = response.data.data;
}
const getProduct = async (id) => {
const response = await axios.get(`/api/products/${id}`);
product.value = response.data.data
}
const storeProduct = async (data) => {
try {
await axios.post('/api/products', data)
await router.push({ name: 'products.index' })
} catch (e) {
if (e.response.status === 422) {
errors.value = e.response.data.errors;
}
}
}
const updateProduct = async (id) => {
try {
await axios.put(`/api/products/${id}`, product.value)
await router.push({ name: 'products.index' })
} catch (e) {
if (e.response.status === 422) {
errors.value = e.response.data.errors;
}
}
}
return {
getProducts,
getProduct,
products,
product,
storeProduct,
updateProduct,
errors
}
}
Let me explain some things here:
To define a composable function, we need to use this syntax use{Name}, for example, useCategories, useEmployees.
ref is used to make the primitive values to be reactive (Boolean, Number, String, etc). In this case, we need 3 reactive variables like product, products, and errors.
The methods we define here are only 4: getProducts to obtain the list of all the products available, getProduct to obtain a single product, and a function to Store and Update data.
If we receive a validation error, these errors come with code 422. We can store all the errors we get in the variable errors through e.response.data.errors.
Let's move on to the next step.
12. Installing and Configure PrimeVue DataTable
In your terminal execute
npm install primevue primeicons
After primevue was installed let's go to resources/js/app.js and edit the file like that
require('./bootstrap');
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
import { createApp } from 'vue'
import router from './router/index'
//DataTable
import 'primevue/resources/themes/tailwind-light/theme.css' //theme
import 'primevue/resources/primevue.min.css' //core css
import 'primeicons/primeicons.css' //icons
import PrimeVue from 'primevue/config'
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import ProductsIndex from './components/products/Index'
const app = createApp({
components: {
ProductsIndex
}
});
app.use(router);
app.use(PrimeVue);
app.component('DataTable', DataTable);
app.component('Column', Column);
app.mount('#app');
Execute npm run dev or npm run watch
Everything is ready to go to resources/js/components/products/Index.vue (yes, again), and this time we need to add the DataTable and Vue code:
<template>
<div>
<DataTable :value="products" responsiveLayout="scroll"
>
<Column field="name" header="Name"></Column>
<Column field="price" header="Price"></Column>
<Column field="description" header="Description"></Column>
</DataTable>
</div>
</template>
<script>
import { onMounted } from "vue";
import useProducts from "../../composables/products";
export default {
setup() {
const { products, getProducts } = useProducts();
onMounted(getProducts);
return {
products,
};
},
};
</script>
Let me explain this code:
The lifecycle hooks change in Vue.js 3 using composition API, that's why we need to import onMounted from "vue". You can read more in this link Composition API Lifecycle Hooks
useProducts() contains every method and variable we define in composables/products.js
We define a basic DataTable component using tags and declaring each column we need also we load all the products in :value property
Now you should be able see this screen
Let's make some customization of the DataTable
<DataTable :value="products" responsiveLayout="scroll"
:paginator="true" :rows="10"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[10,25,50]"
:filters="filters"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
>
<template #header>
<div class="flex place-content-end">
<input type="text" v-model="filters['global'].value" placeholder="Search..." class="rounded-md"/>
</div>
</template>
<Column field="name" header="Name" :sortable="true">
</Column>
<Column field="price" header="Price" :sortable="true"></Column>
<Column field="description" header="Description"></Column>
</DataTable>
We add:
- A global search.
- Pagination
- Sortable columns with :sortable in each Column we need
In the script section add this
<script>
import { onMounted, ref } from "vue";
import useProducts from "../../composables/products";
import {FilterMatchMode} from 'primevue/api';
export default {
setup() {
const { products, getProducts } = useProducts();
onMounted(getProducts);
const filters = ref({
'global': {value: null, matchMode: FilterMatchMode.CONTAINS},
});
return {
products,
filters,
};
},
};
</script>
You should see something like this
Note: If you want more customization visit the official documentation PrimeVue DataTable
13. Products Create Component
Before going to the component, let's go to resources/js/router/index.js and edit like this
import ProductsCreate from '../components/products/Create.vue'
const routes = [
{
path: '/dashboard',
name: 'products.index',
component: ProductsIndex
},
{
path: '/products/create',
name: 'products.create',
component: ProductsCreate
}
]
Now, we need to edit the Products/Index.vue component adding a button before the DataTable
<div class="flex place-content-end mb-4">
<div class="px-4 py-2 text-white bg-blue-700 hover:bg-indigo-800 rounded-lg cursor-pointer">
<router-link :to="{ name: 'products.create' }" class="text-sm font-medium">Create product</router-link>
</div>
</div>
You should see
We can go to resources/js/components/products/Create.vue and create a new form
<template>
<div>
<h3 class="text-2xl mb-4">Create new product</h3>
<form class="space-y-6" @submit.prevent="saveProduct" method="POST">
<div class="space-y-4 rounded-md shadow-sm">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<div class="mt-1">
<input type="text" name="name" id="name"
class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
v-model="form.name">
</div>
<div
class="font-medium ml-3 text-red-700"
v-if="errors && errors.name"
>
{{errors.name[0]}}
</div>
</div>
<div>
<label for="price" class="block text-sm font-medium text-gray-700">Price</label>
<div class="mt-1">
<input type="text" name="price" id="price"
class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
v-model="form.price">
</div>
<div
class="font-medium ml-3 text-red-700"
v-if="errors && errors.price">
{{errors.price[0]}}
</div>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<div class="mt-1">
<input type="text" name="description" id="description" class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" v-model="form.description">
</div>
</div>
</div>
<div class="flex place-content-end mb-4">
<button type="submit"
class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-blue-800 rounded-md border border-transparent ring-blue-300 transition duration-150 ease-in-out hover:bg-blue-700 active:bg-blue-900 focus:outline-none focus:border-blue-900 focus:ring disabled:opacity-25">
Create
</button>
</div>
</form>
</div>
</template>
<script>
import { reactive } from 'vue'
import useProducts from '../../composables/products'
export default {
setup() {
const form = reactive({
name: '',
price: '',
description: ''
})
const { errors, storeProduct } = useProducts()
const saveProduct = async () => {
await storeProduct({ ...form})
}
return {
form,
errors,
saveProduct
}
}
}
</script>
Now you see this form
Note: You can test the validation and creation of a new record. This should work but if you have some trouble with that let me a comment.
14. Delete Product
To delete a product, let's go to resources/js/components/products/Index.vue and create a delete button like this
<Column header="actions">
<template #body="slotProps">
<button @click="deleteProduct(slotProps.data.id)"
class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150">Delete</button>
</template>
</Column>
In the script section we need to add the deleteProduct function
export default {
setup() {
const { products, getProducts, destroyProduct } = useProducts();
const deleteProduct = async (id) => {
if (!window.confirm('Are you sure? This record will permanantly deleted')) {
return
}
await destroyProduct(id)
await getProducts()
}
onMounted(getProducts);
const filters = ref({
'global': {value: null, matchMode: FilterMatchMode.CONTAINS},
});
return {
products,
filters,
deleteProduct
};
},
};
We can test this part and we can see
15. Products Edit Component
Go to resources/js/router/index.js (this is the last time we need to edit this file)
import ProductsEdit from '../components/products/Edit.vue'
const routes = [
{
path: '/dashboard',
name: 'products.index',
component: ProductsIndex
},
{
path: '/products/create',
name: 'products.create',
component: ProductsCreate
},
{
path: '/products/:id/edit',
name: 'products.edit',
component: ProductsEdit,
props: true
}
]
Once you finish adding the new route, go to resources/js/components/products/Index.vue and put the buttons in the DataTable to update and delete a product like this
<Column>
<template #body="slotProps">
<router-link :to="{ name: 'products.edit', params: { id: slotProps.data.id } }" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150 mr-4">Edit</router-link>
<button class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150">Delete</button>
</template>
</Column>
You'll see this screen
Open the resources/js/components/products/Edit.vue and add this code
<template>
<div>
<h3 class="text-2xl mb-4">Edit product</h3>
<form class="space-y-6" @submit.prevent="editProduct">
<div class="space-y-4 rounded-md shadow-sm">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<div class="mt-1">
<input type="text" name="name" id="name"
class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
v-model="product.name">
</div>
<div
class="font-medium ml-3 text-red-700"
v-if="errors && errors.name"
>
{{errors.name[0]}}
</div>
</div>
<div>
<label for="price" class="block text-sm font-medium text-gray-700">Price</label>
<div class="mt-1">
<input type="text" name="price" id="price"
class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
v-model="product.price">
</div>
<div
class="font-medium ml-3 text-red-700"
v-if="errors && errors.price"
>
{{errors.price[0]}}
</div>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<div class="mt-1">
<input type="text" name="description" id="description" class="block mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" v-model="product.description">
</div>
</div>
</div>
<div class="flex place-content-end mb-4">
<button type="submit"
class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-blue-800 rounded-md border border-transparent ring-blue-300 transition duration-150 ease-in-out hover:bg-blue-700 active:bg-blue-900 focus:outline-none focus:border-blue-900 focus:ring disabled:opacity-25">
UPDATE
</button>
</div>
</form>
</div>
</template>
<script>
import { onMounted } from 'vue'
import useProducts from '../../composables/products'
export default {
props: {
id: {
required: true,
type: String
}
},
setup(props) {
const { errors, getProduct, product, updateProduct } = useProducts()
onMounted(() => getProduct(props.id))
const editProduct = async () => {
await updateProduct(props.id)
}
return {
product,
errors,
editProduct
}
}
}
</script>
Let me explain some parts of this code.
Almost the same that Create component but there are some differences like props in this case we need to know the id to fetch only the data to the product we want to edit, we need to define the props and pass them as a parameter in the setup(props).
Don't worry if don't see where we fill the variable product remember the composables/products.js through getProduct function we obtain the values of the product.
16. GitHub Repo
This is the link Laravel-Vuejs-CRUD
If you have any doubt leave a message in the comment section.
Happy coding ☕. Thanks for reading.