Laravel 9 VueJs SPA CRUD with Image

Hello Artisans, today we'll discuss about SPA (single page application) CRUD with Laravel & VueJs. VueJs is one of the most popular frontend frameworks nowadays. It's a very small framework with a massive performance. For more you can check here So, no more talk, let's see how we can easily create a simple SPA CRUD application using Laravel & VueJs.

Note: Tested on Laravel 9.11

Table of Contents

  1. Install and Configure Vue and its Dependency
  2. Setup Migration and Model
  3. Create and Setup Controller
  4. Create and Setup Component
  5. Define Routes
  6. Setup blade File
  7. Output

Install and Configure Vue and its Dependency

Then we'll install the vue 2.6.14. Because as for now it's on of the most stable one. So, fire the below command in the terminal.

npm i [email protected]

Now, we'll install the laravel-vue-pagination package. It's quite easy to use pagination in our list. Fire the below command to install it.

npm install [email protected]

Now fire the below command to install the vue-template-complier and vue-loader. So that our mixin and webpack can recognize the .vue file.

npm install vue-template-compiler vue-loader@^15.9.7 --save-dev --legacy-peer-deps

Now we'll install the sweetalert2. So that we can notify the user through toastr notification.

npm i sweetalert2

Now we installed all the necessary packages. Now we need to setup our app.js file. So, open the file and replace it with below codes.

resources/js/app.js
require('./bootstrap');

import Vue from 'vue/dist/vue'

//Pagination laravel-vue-pagination
Vue.component('pagination', require('laravel-vue-pagination'));

import Swal from 'sweetalert2'
window.Swal = require('sweetalert2')

const Toast = Swal.mixin({
    toast: true,
    position: 'top-end',
    showConfirmButton: false,
    timer: 3000,
    timerProgressBar: true
});

window.Toast = Toast;

let Fire = new Vue()
window.Fire = Fire;

Vue.component('users', require('./components/users.vue').default);

const app = new Vue({
    el: '#app',
});

Now we need to configure webpack.mix.js, so that we can mix our js and css file. So, open the file and replace it with below codes.

webpack.mix.js
const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
    .postCss('resources/css/app.css', 'public/css', [
        //
    ]).vue();

And finally run the below command to watch your developments as well as generate the mix files.

npm run watch

And that's it. We're done with our vuejs setup.

Setup Migration and Model

First of all, we'll modify our default users table by adding the images column. Let's open the file and replace with below codes

database/migrations/2014_10_12_000000_create_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('image')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
};

And after modification run the below command in terminal to migrate the migration file.

php artisan migrate

After that we need to setup our User model. So, open the file and replace with below codes.

app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'image',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    protected $appends = ['profile_pic'];

    public function getProfilePicAttribute(): string
    {
        return $this->image ? asset($this->image) : '';
    }
}

Create and Setup Controller

First of all, create a controller so that we can write our logics or query to show the result. So, fire the below commands in the terminal.

php artisan make:controller UserController

It'll create a controller under app\Http\Controllers called UserController.php. Open the file and replace with below codes.

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class UserController extends Controller
{
    public function index(): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'users' => User::latest()->paginate(10)
        ], 200);
    }

    public function store(Request $request): \Illuminate\Http\JsonResponse
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|min:8',
        ]);

        DB::beginTransaction();

        $data = $request->all();

        if ($request->image) {
            $image = $request->image;
            $image_new_name = time() . $image->getClientOriginalName();
            $image->move('uploads/users', $image_new_name);
            $data['image'] = 'uploads/users/' . $image_new_name;
        }

        $data['password'] = bcrypt($request->password);
        User::create($data);
        DB::commit();
        return response()->json(['success' => 'User Created Successfully'], 201);

    }


    public function update(Request $request, $id): \Illuminate\Http\JsonResponse
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email'
        ]);

        $user = User::find($request->id);

        $data = $request->all();

        if ($request->image) {
            $image = $request->image;
            $image_new_name = time() . $image->getClientOriginalName();
            $image->move('uploads/users', $image_new_name);
            $data['image'] = 'uploads/users/' . $image_new_name;
        }

        if ($request->password) {
            $data['password'] = bcrypt($request->password);
        }

        $user->update($data);

        return response()->json(['success' => 'User Updated Successfully'], 200);
    }

    public function destroy($id): \Illuminate\Http\JsonResponse
    {
        User::destroy($id);

        return response()->json([
            'success' => 'User Removed Successfully',
        ]);
    }

}

Create and Setup Component

Now we need to create a vue component where we'll fetch our user and perform read/write operation. So, create a file under resources/js/components named users.vue and replace with following codes.

resources/js/components/users.vue
<template>
  <div class="container mt-5">
    <div class="row">
      <div class="col-md-12">
        <h4 class="mb-5">Laravel Vue SPA - shouts.dev</h4>
        <table id="table" class="table table-bordered table-striped">
          <thead>
          <tr>
            <th>Image</th>
            <th>Name</th>
            <th>Email</th>
            <th class="check">Action
              <a data-toggle="modal" data-target="#user"
                 style="float: right;cursor: pointer; color: white; padding: 2px;"
                 @click="openModalWindow" class="btn btn-sm btn-warning py-0">
                <i class="fa fa-plus-square"> Add User
                </i>
              </a>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="user in users.data" :key="user.id">
            <td><img width="80" :src="user.profile_pic" :alt="user.name"></td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td class="check">
              <a title="Edit category" class="btn btn-sm btn-dark py-0"
                 style="color:white;cursor: pointer;" @click="edit(user)">Edit</a>
              <a class="btn btn-sm btn-danger py-0" @click="deleteUser(user.id)" style="color:white;">Delete</a>
            </td>
          </tr>
          </tbody>
          <pagination :data="users" @pagination-change-page="getResults"></pagination>
        </table>
        <div class="modal fade" id="user" tabindex="-1" role="dialog" aria-labelledby="addNewLabel"
             aria-hidden="true">
          <div class="modal-dialog modal-dialog-centered" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <h5 v-if="form.id" class="modal-title">Update User</h5>
                <h5 v-else class="modal-title">Add New User</h5>
              </div>

              <form @submit.prevent="form.id ? updateUser() : createUser()">
                <div class="modal-body">
                  <div class="form-group">
                    <input v-model="form.name" type="text" name="name"
                           placeholder="Name"
                           class="input2 form-control" :class="{ 'is-invalid': errors.name }"
                           data-validate="User name is required">
                    <span class="focus-input2" data-placeholder="Company Name"></span>
                    <span class="text-danger" v-if="errors.name">{{ errors.name[0] }}</span>
                  </div>
                  <div class="form-group">
                    <input v-model="form.email" type="text" name="email"
                           placeholder="Email"
                           class="input2 form-control" :class="{ 'is-invalid': errors.email }"
                           data-validate="User Email is required">
                    <span class="focus-input2" data-placeholder="User Email"></span>
                    <span class="text-danger" v-if="errors.email">{{ errors.email[0] }}</span>
                  </div>
                  <div class="form-group">
                    <input v-model="form.password" type="password" name="password"
                           placeholder="password"
                           class="input2 form-control" :class="{ 'is-invalid': errors.password }"
                           data-validate="Password is required">
                    <span class="focus-input2" data-placeholder="Password"></span>
                    <span class="text-danger" v-if="errors.password">{{ errors.password[0] }}</span>
                  </div>
                  <div class="form-group">
                    <input v-model="form.password_confirmation" type="password" name="password"
                           placeholder="password"
                           class="input2 form-control" :class="{ 'is-invalid': errors.password_confirmation }"
                           data-validate="Password is required">
                  </div>
                  <div class="form-group">
                    <input type="file" class="input2 form-control" @change="imageUp">
                  </div>
                </div>
                <div class="modal-footer">
                  <button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
                  <button v-if="form.id" type="submit" class="btn btn-success">Update</button>
                  <button v-else type="submit" class="btn btn-success">Create</button>
                </div>

              </form>

            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>


export default {

  data() {
    return {
      users: {},
      errors: [],
      form: {
        id: '',
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
        image: '',
      },
    }
  },
  methods: {
    resetForm() {
      for (var key in this.form) {
        this.form[key] = '';
      }
      this.errors = [];
    },
    openModalWindow() {
      this.resetForm();
      $('#user').modal('show');
    },
    getUsers() {
      axios.get("/users").then(data => (this.users = data.data.users));
    },
    getResults(page = 1) {
      axios.get('/users?page=' + page)
          .then(response => {
            this.users = response.data.users;
          });
    },
    createUser() {
      axios.post('/users', this.createFormData(), {
        headers: {
          'Content-Type': "multipart/form-data; charset=utf-8; boundary=" + Math.random().toString().substr(2)
        }
      }).then(() => {
            Fire.$emit('load_user');
            Toast.fire({
              icon: 'success',
              title: 'User created successfully'
            });
            $('#user').modal('hide');
          })
          .catch((error) => {
            console.log("Error......");
            if (error.response.status == 422) {
              this.errors = error.response.data.errors;
            }
          })
    },
    createFormData() {
      let formData = new FormData();

      for (var key in this.form) {
        formData.append(key, this.form[key]);
      }
      return formData;
    },
    edit(user) {
      this.resetForm();
      $('#user').modal('show');
      this.form = user;
    },
    updateUser() {
      axios.post('/users/' + this.form.id, this.createFormData(), {
        headers: {
          'Content-Type': "multipart/form-data; charset=utf-8; boundary=" + Math.random().toString().substr(2)
        }
      }).then(() => {

            Toast.fire({
              icon: 'success',
              title: 'User updated successfully'
            })

            Fire.$emit('load_user');

            $('#user').modal('hide');
          })
          .catch((error) => {
            console.log("Error.....");
            if (error.response.status == 422) {
              this.errors = error.response.data.errors;
            }
          })
    },
    deleteUser(id) {
      Swal.fire({
        title: 'Are you sure?',
        text: "You won't be able to revert this!",
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#3085d6',
        cancelButtonColor: '#d33',
        confirmButtonText: 'Yes, delete it!'
      }).then((result) => {

        if (result.value) {

          axios.delete('/users/' + id)
              .then((response) => {
                Swal.fire(
                    'Deleted!',
                    'User deleted successfully',
                    'success'
                )

                Fire.$emit('load_user');

              }).catch(() => {
            Swal.fire({
              icon: 'error',
              title: 'Oops...',
              text: 'Something went wrong!',
            })
          })
        }

      })
    },
    imageUp() {
      this.form.image = event.target.files[0];
    }
  },
  created() {

    this.getUsers();

    Fire.$on('load_user', () => {
      this.getUsers();
    });

  },

}
</script>

<style scoped>
.pagination {
  margin-top: 30px;
  float: right;
}

.validation_error {
  border: 1px solid red !important;
}
</style>

Define Routes

Now we need to put the below routes in web.php. Replace it with below codes.

routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::resource('users',\App\Http\Controllers\UserController::class)->except('update');
Route::post('users/{id}',[\App\Http\Controllers\UserController::class,'update']);

Setup blade File

Now we'll modify the default blade file named welcome.blade.php which comes with Laravel by default.

resources/views/welcome.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name', 'Laravel') }}</title>
    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
    <title>Laravel VUE SPA</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>

<div id="app">

    <users></users>
    <div class="py-4">
        @yield('content')
    </div>

</div>
<script src="{{ mix('js/app.js') }}" defer></script>
</body>
</html>

Output

And finally we're ready with our setup. It's time to check our output. Now go to http://127.0.0.1:8000/users, If everything goes well you'll find a below output.

Laravel 9 Vue Js CRUD SPA with Image

That's it for today. I hope you've enjoyed this tutorial. You can also download this tutorial from GitHub. Thanks for reading. ๐Ÿ™‚