Laravel 8 + Vue.js 3 CRUD Composition API

Laravel 8 + Vue.js 3 CRUD Composition API

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.

database_credentials.png

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

resources_folder.png

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.

dashboard_bienvenido.png

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

datatable.png

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

datatable_working.png

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

button.png

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

store_product_form.png

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

delete.png

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

datatable_complete.png

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.