The Ultimate Guide For Creating Laravel CRUD AJAX Application using Laravel 5.8, Vue 2, Tailwindcss, Vue-router, and Axios.
- Installation and configurations.
- Database model, migration, and seeds.
- HTTP routes.
- HTTP Controllers.
- Form Requests and Validation.
- Views.
- Bundling assets.
- Tailwindcss.
- Vue and Vue-router.
- AJAX and Axios.
- Install laravel in a new project directory:
composer create-project --prefer-dist laravel/laravel [Your project name]
🔥 Pro Tip:
make sure you have Composer installed.
- Install NPM dependencies to compile
SASS
andJavaScript
:
npm install
🔥 Pro Tip:
make sure you have Nodejs installed.
- Create a new database. I am using
MySQL
:
# Login to MySQL
sudo mysql -u [MySQL Username] -p
# Create MySQL database
CREATE DATABASE [Your database name] DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- Configure Database connection and other settings in your
.env
file:
APP_NAME=CRUD
APP_ENV=local
APP_DEBUG=true
APP_URL=http://crud.test
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=[Your Database Name]
DB_USERNAME=[Your MySQL Username]
DB_PASSWORD=[Your MySQL Password]
📍.env
-
Since we don't need the authentication system, let's copy the auth migrations to another directory inside the
database
directory so we can use them later. Let's call ittrash
.
📍Copy files insidedatabase/migration
to new directorydatabase/trash
-
Let's create a migration, factory, and resource controller for our model
Item
:
php artisan make:model Item --all
- Let's add our table fields:
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->text('comment');
$table->timestamps();
});
}
📍database/migration/xxxx_xx_xx_xxxxxx_create_items_table.php
🔥 Pro Tip:
you can find all migration columns in the documentation.
- Make the database fields fillable in the
Item
model:
class Item extends Model
{
protected $fillable = [
'title', 'comment',
];
}
📍app/Item.php
- Let's add the
Item
factory for generating seeds later:
$factory->define(App\Item::class, function (Faker $faker) {
return [
'title' => $faker->sentence(4, true),
'comment' => $faker->realText(200, 2),
];
});
📍database/factories/ItemFactory.php
🔥 Pro Tip:
you can find all the available faker formatters in the Faker
project README.
- Create database seeder for
Item
table:
php artisan make:seeder ItemsTableSeeder
Let's add 6 rows for example at any single seed:
public function run()
{
factory(App\Item::class, 6)->create();
}
📍database/seeds/ItemsTableSeeder.php
- Add the
Item
seeder to the project seeder:
public function run()
{
$this->call(ItemsTableSeeder::class);
}
📍database/seeds/DatabaseSeeder.php
- Now we can seed to the database using the
seeder
command:
php artisan db:seed
- You should find that the data has been seeded successfully to your database.
- Let's create all of our routes on the
routes
directory:
// :: Home Route ::
// For showing home page
Route::get('/', 'HomeController@index')->name('index');
// this also works: Route::view('/', 'index');
// :: Items CRUD Routes ::
// For getting all the items
Route::get('/items', 'ItemController@index')->name('items.index');
// For saving an item
Route::post('/items', 'ItemController@store')->name('items.store');
// For getting an item
Route::get('/items/{item}', 'ItemController@show')->name('items.show');
// For updating an item
Route::patch('/items/{item}', 'ItemController@update')->name('items.update');
// For deleting an item
Route::delete('/items/{item}', 'ItemController@destroy')->name('items.destroy');
📍routes/web.php
🔥 Pro Tip:
you can find all the available router methods in the documentation.
🔥 Pro Tip:
after creating the routes then comes the controllers
and the views
.
🔥 Pro Tip:
you can also add middlewares to any route you have but, for the sake of simplisity we don't don't have to add any middleware to our current routes. More about middleware in the documentation.
- After creating all the routes we need, let's create the home controller. As you can remember, we already created the
Item
controller when we created theitems
table migration:
php artisan make:controller HomeController
- Let's add the
index
method for the home route and return the home pageview
that we will add later on:
class HomeController extends Controller
{
public function index() {
return view('index');
}
}
📍app/Http/Controllers/HomeController.php
- Let's add the
item
methods for each HTTP request. You can delete thecreate
andupdate
methods because we don't need them in our application:
class ItemController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$items = Item::orderBy('id','DESC')->get();
return response()->json([
'items' => $items
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$item = new Item;
$item->title = $request->title;
$item->comment = $request->comment;
$item->save();
return response()->json([
'item' => $item
]);
}
/**
* Display the specified resource.
*
* @param \App\Item $item
* @return \Illuminate\Http\Response
*/
public function show(Item $item)
{
return response()->json([
'item' => $item
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Item $item
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Item $item)
{
$item->title = $request->title;
$item->comment = $request->comment;
$item->save();
return response()->json([
'item' => $item
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Item $item
* @return \Illuminate\Http\Response
*/
public function destroy(Item $item)
{
$item->delete();
return response()->json([
'item' => $item
]);
}
}
📍app/Http/Controllers/ItemController.php
🔥 Pro Tip:
you can find all the available Laravel eloquent methods in the documentation.
- Some requests need to be validated before it can go to the database; Like the
store
and theupdate
routes. let's create two form request for both routes:
php artisan make:request StoreItemRequest
php artisan make:request UpdateItemRequest
class StoreItemRequest 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 [
'title' => 'required|string|min:6|max:255',
'comment' => 'required|string|min:6|max:1000',
];
}
}
📍app/Http/Requests/StoreItemRequest.php
class UpdateItemRequest 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 [
'title' => 'required|string|min:6|max:255',
'comment' => 'required|string|min:6|max:1000',
];
}
}
📍app/Http/Requests/UpdateItemRequest.php
- Then let's add the routes to our
store
andupdate
methods on theItemController
.
use App\Http\Requests\StoreItemRequest;
use App\Http\Requests\UpdateItemRequest;
class ItemController extends Controller
{
// ...
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\StoreItemRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreItemRequest $request)
{
// ...
}
// ...
/**
* Update the specified resource in storage.
*
* @param App\Http\Requests\UpdateItemRequest $request
* @param \App\Item $item
* @return \Illuminate\Http\Response
*/
public function update(UpdateItemRequest $request, Item $item)
{
// ...
}
// ...
}
📍app/Http/Controllers/ItemController.php
🔥 Pro Tip:
you can find all the available Laravel validation rules in the documentation.
- We also need to validate the route parameter
item
we used on the controller routes. The validation pattern should be added to theRouteServiceProvider
in theboot
method:
Route::get('/items/{item}', 'ItemController@show')->name('items.show');
Route::patch('/items/{item}', 'ItemController@update')->name('items.update');
Route::delete('/items/{item}', 'ItemController@destroy')->name('items.destroy');
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
Route::pattern('item', '[0-9]+');
parent::boot();
}
📍app/Providers/RouteServiceProvider.php
- We only need a single home page to show all the items. let's create a new
blade
view:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="description" content="A simple example of using Laravel 5.8 and Vue 2 for creating CRUD applications with AJAX functionality.">
<title>Laravel AJAX CRUD Application</title>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Source Sans Pro:400,600']
}
});
</script>
<link rel="stylesheet" type="text/css" href="{{ mix('css/app.css') }}">
</head>
<body>
<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>
📍resources/views/index.blade.php
🔥 Pro Tip:
you can remove the welcome.blade.php
view because we don't need it in our project.
- We need to prepare our
SASS
andJavaScript
bundling usinglaravel-mix
. the assets bundlinginput
andoutput
path are already configured inwebpack.mix.js
:
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
📍webpack.mix.js
- Let's remove the files inside the
resources/js
andresources/sass
directory. After that create empty files as follow;resources/js/app.js
andresources/sass/app.scss
.
- We will be using the power of
tailwindcss
to writeSASS
. let's installtailwindcss
using NPM:
npm i tailwindcss --save-dev
🔥 Pro Tip:
To find more about how to use tailwindcss
, check out the documentation.
- To use
tailwindcss
, we need to generate a newtailwind.js
configuration file:
npx tailwind init
- After that let's add the main style structure to our
app.scss
:
/**
* This injects Tailwind's base styles, which is a combination of
* Normalize.css and some additional base styles.
*/
@tailwind preflight;
/**
* This injects any component classes registered by plugins.
*/
@tailwind components;
/**
* Here you would add any of your custom component classes; stuff that you'd
* want loaded *before* the utilities so that the utilities could still
* override them.
*/
@import "components";
/**
* This injects all of Tailwind's utility classes, generated based on your
* config file.
*/
@tailwind utilities;
/**
* Here you would add any custom utilities you need that don't come out of the
* box with Tailwind.
*/
📍resources/sass/app.scss
- The last step is to add the tailwindcss plugin to the laravel-mix
post-css
:
const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.options({
processCssUrls: false,
postCss: [
tailwindcss('./tailwind.js'),
],
});
📍webpack.mix.js
- We are ready to start writing
SASS
by running the development script:
npm run watch
- Because this isn't a
SASS
tutorial, you can find theSASS
files on the project GitHub repository.
🔥 Pro Tip:
If you want to find more aboutSASS
language, check out the documentation.
- To create our single page application for the CRUD functionality, we need to install
vue
via NPM:
npm i vue --save
- We need to add the mounting point that
vue
needs to render our components:
<body>
-
+ <div id="app"></div>
<script src="{{ mix('js/app.js') }}"></script>
</body>
📍resources/views/index.blade.php
- We can start by creating a new
vue
instance in ourapp.js
file. We also gonna need to create 2 other files, One for the main application component calledApp.vue
inside directoryMain
. The other for importing all javascript libraries before we mount the app calledboorstrap.js
:
import Vue from 'vue';
import App from './Main/App';
require('./bootstrap');
const app = new Vue({
el: '#app',
render: h => h(App),
});
export default app;
📍resources/js/app.js
- First we start with the
App
component.
<template>
<div>
<Navbar />
<Content />
<Ads />
<Footer />
<Notifications />
</div>
</template>
<script>
import Navbar from '../Components/Navbar';
import Content from '../Components/Content';
import Ads from '../Components/Ads';
import Footer from '../Components/Footer';
import Notifications from '../Components/Notifications';
export default {
components: { Navbar, Content, Ads, Footer, Notifications, },
}
</script>
📍resources/js/Main/App.vue
- The site contain multiple components like
Navbar
,Content
,Ads
,Footer
, andNotifications
. You can find the code of these component on GitHub. Some of these components have dependancies and we need to install them and configure them in ourbootstrap.js
. We also need to createform.js
for notifications and form errors functions:
npm i -S @fortawesome/fontawesome-svg-core @fortawesome/vue-fontawesome @fortawesome/free-brands-svg-icons @fortawesome/free-solid-svg-icons axios vue-notification
import Vue from 'vue';
import { dom, config, library } from '@fortawesome/fontawesome-svg-core';
import { faGithub, faTwitter } from '@fortawesome/free-brands-svg-icons';
import { faStar, faSyncAlt, faArrowLeft, faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import axios from 'axios';
import Loading from './Components/Loading';
import Notifications from 'vue-notification';
import form from './form';
library.add(faGithub, faTwitter, faStar, faSyncAlt, faArrowLeft, faCircleNotch, faTimes);
Vue.component('Fa', FontAwesomeIcon);
config.searchPseudoElements = true;
dom.watch();
Vue.use(Notifications);
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if(error.response === undefined || error.request.status === 500) {
form.fail();
}
return Promise.reject(error);
});
Vue.component('Loading', Loading);
📍resources/js/bootstrap.js
import app from './app';
const form = {
note: (message, duration) => {
return app.$notify({
group: 'notes',
text: message,
duration: duration,
});
},
fail: () => {
return app.$notify({
group: 'notes',
text: 'Something went wrong while processing your request!',
duration: 10000,
});
},
errors: (err) => {
if(err.response.status === 422) {
return err.response.data.errors;
}
},
};
export default form;
📍resources/js/form.js
- A small change needs to be added to fix the
container
class alignment. Currently, thecontainer
is aligned to the left and we need to have it centered. To do so, we will edit thetailwindcss
configuration:
plugins: [
require('tailwindcss/plugins/container')({
- // center: true,
+ center: true,
- // padding: '1rem',
+ padding: '1rem',
}),
],
📍tailwind.js
- As you will notice, the images will show up on the index page. you will need to get the images from GitHub. Copy all the images to
resources/images
and setup bundling inside ofwebpack.mix.js
:
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
+ .copyDirectory('resources/images', 'public/images')
.options({
processCssUrls: false,
postCss: [
tailwindcss('./tailwind.js'),
],
});
📍webpack.mix.js
- Now, we are going to focus on the
CRUD
component located inside of theContent
component. We will be usingvue-router
to switch between the CRUD components. We need to installvue-router
and create a configuration file calledrouter.js
, so it can be added to ourvue
instance inapp.js
. TheCRUD
component will be our router entry:
npm i vue-router --save
import Vue from 'vue';
import VueRouter from 'vue-router';
import Index from './Components/Items/Index';
import Create from './Components/Items/Create';
import Show from './Components/Items/Show';
import Edit from './Components/Items/Edit';
import Delete from './Components/Items/Delete';
import NotFound from './Components/NotFound';
Vue.use(VueRouter);
const routes = [
{ path: '/', component: Index, name: 'items.index', },
{ path: '/create', component: Create, name: 'items.create', },
{ path: '/:id', component: Show, name: 'items.show', },
{ path: '/:id/edit', component: Edit, name: 'items.edit', },
{ path: '/:id/delete', component: Delete, name: 'items.delete', },
{ path: '*', component: NotFound, name: '404', },
];
const router = new VueRouter({
routes
});
export default router;
📍resources/js/router.js
import Vue from 'vue';
import App from './Main/App';
+import router from './router';
require('./bootstrap');
const app = new Vue({
el: '#app',
render: h => h(App),
+ router,
});
export default app;
📍resources/js/app.js
<template>
<div class="crud">
<transition name="slide-left" mode="out-in">
<router-view></router-view>
</transition>
</div>
</template>
<style scoped>
.slide-left-enter-active, .slide-left-leave-active {
transition: all 250ms;
}
.slide-left-enter, .slide-left-leave-to {
opacity: 0;
transform: translateX(-.25rem);
}
</style>
📍resources/js/Components/CRUD.vue
- To make AJAX call we need the URL of every HTTP request to submit the form. We will be using a package called
Ziggy
to use theroute()
function inside our javascript:
composer require tightenco/ziggy
<body>
<div id="app"></div>
+ @routes
<script src="{{ mix('js/app.js') }}"></script>
</body>
📍resources/views/index.blade.php
- let's create all the crud components files in new
Items
directory:
<template>
<div class="crud-listing">
<div class="crud-header">
<button
class="button success"
@click="$router.push({ name: 'items.create' })"
:disabled="fetching"
>
Create
</button>
<button
class="button ml-1"
title="Reload"
:disabled="fetching"
@click="reload()"
>
<Fa :icon="['fas', 'sync-alt']" />
</button>
</div>
<Loading v-if="loading" />
<div v-else class="crud-content mb-0">
<table class="table" :class="[ items.length === 0 ? '' : 'mb-5']">
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-if="items.length === 0">
<td colspan="3" class="text-center"><em>The table is empty.</em></td>
</tr>
<template v-else>
<ItemRow v-for="item in items" :key="item.id" :item="item" />
</template>
</tbody>
</table>
<p class="text-right text-sm text-grey-dark" v-if="items.length !== 0">Number of rows: {{ items.length }}</p>
</div>
</div>
</template>
<script>
import axios from 'axios';
import ItemRow from '../ItemRow';
import form from '../../form';
export default {
data() {
return {
items: [],
loading: true,
fetching: true,
}
},
mounted() {
let self = this;
axios.get(route('items.index'))
.then(res => {
self.items = res.data.items;
})
.catch(err => {
})
.then(() => {
self.fetching = false;
self.loading = false;
});
},
methods: {
reload() {
let self = this;
this.fetching = true;
this.loading = true;
axios.get(route('items.index'))
.then(res => {
self.items = res.data.items;
})
.catch(err => {
})
.then(() => {
self.loading = false;
self.fetching = false;
});
}
},
components: { ItemRow },
}
</script>
📍resources/js/Components/Items/Index.vue
<template>
<div class="crud-create">
<div class="crud-header">
<button
class="button"
@click="$router.push({ name: 'items.index' })"
:disabled="fetching"
>
<Fa :icon="[ 'fas', 'arrow-left' ]" /> Go back
</button>
</div>
<form @submit.prevent="create()">
<div class="crud-content">
<div class="input">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2">
<label for="create_title" class="mt-2">Title</label>
</div>
<div class="w-full md:w-2/3 px-2">
<input type="text" id="create_title" v-model="form.title" @focus="errors.title = []">
<span v-if="errors.title" v-text="errors.title[0]" class="input-error"></span>
</div>
</div>
</div>
<div class="input">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2">
<label for="create_comment" class="mt-2">Comment</label>
</div>
<div class="w-full md:w-2/3 px-2">
<textarea id="create_comment" v-model="form.comment" cols="30" rows="10" @focus="errors.comment = []"></textarea>
<div class="flex flex-wrap">
<div class="w-1/2">
<span v-if="errors.comment" v-text="errors.comment[0]" class="input-error"></span>
</div>
<div class="w-1/2">
<span class="input-help">Maximum of 1000 letters.</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="crud-footer">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2"></div>
<div class="w-full md:w-2/3 px-2">
<button
type="submit"
class="button loading"
:class="fetching ? 'loading-show' : ''"
:disabled="fetching"
>
Submit
</button>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
import axios from 'axios';
import form from '../../form';
export default {
data() {
return {
fetching: false,
form: {
title: '',
comment: '',
},
errors: {},
}
},
methods: {
create() {
let self = this;
self.fetching = true;
axios.post(route('items.store'),{
title: self.form.title,
comment: self.form.comment,
})
.then(res => {
self.clear();
form.note('Create request succeeded.', 8000);
self.errors = {};
self.$router.push({ name: 'items.index' });
})
.catch(err => {
self.errors = form.errors(err);
})
.then(() => {
self.fetching = false;
});
},
clear() {
this.form.title = '';
this.form.comment = '';
}
}
}
</script>
📍resources/js/Components/Items/Create.vue
<template>
<div class="crud-show">
<div class="crud-header">
<button
class="button"
@click="$router.push({ name: 'items.index' })"
:disabled="fetching"
>
<Fa :icon="[ 'fas', 'arrow-left' ]" /> Go back
</button>
</div>
<Loading v-if="loading" />
<template v-else>
<div class="crud-content">
<h2 class="mb-4" v-text="item.title"></h2>
<p class="text-content" v-text="item.comment"></p>
</div>
<div class="crud-meta">
<p class="mb-1">Written at: {{ item.created_at }} ({{ item.written_at }}).</p>
<p v-if="item.created_date != item.updated_date">Last updated: {{ item.updated_at }} ({{ item.modified_at }}).</p>
</div>
</template>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
loading: true,
fetching: true,
item: {},
}
},
mounted() {
let self = this;
axios.get(route('items.show', { item: self.$route.params.id }))
.then(res => {
self.modify(res.data.item);
})
.catch(err => {
})
.then(() => {
self.fetching = false;
self.loading = false;
});
},
methods: {
modify(item) {
this.item = item;
}
}
}
</script>
📍resources/js/Components/Items/Show.vue
<template>
<div class="crud-edit">
<div class="crud-header">
<button
class="button"
@click="$router.push({ name: 'items.index' })"
:disabled="fetching"
>
<Fa :icon="[ 'fas', 'arrow-left' ]" /> Go back
</button>
</div>
<Loading v-if="loading" />
<template v-else>
<form @submit.prevent="edit()">
<div class="crud-content">
<div class="input">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2">
<label for="edit_title" class="mt-2">Title</label>
</div>
<div class="w-full md:w-2/3 px-2">
<input type="text" v-model="form.title" id="edit_title" @focus="errors.title = []">
<span v-if="errors.title" v-text="errors.title[0]" class="input-error"></span>
</div>
</div>
</div>
<div class="input">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2">
<label for="edit_comment" class="mt-2">Comment</label>
</div>
<div class="w-full md:w-2/3 px-2">
<textarea id="edit_comment" v-model="form.comment" cols="30" rows="10" @focus="errors.comment = []"></textarea>
<div class="flex flex-wrap">
<div class="w-1/2">
<span v-if="errors.comment" v-text="errors.comment[0]" class="input-error"></span>
</div>
<div class="w-1/2">
<span class="input-help">Maximum of 1000 letters.</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="crud-footer">
<div class="flex flex-wrap -mx-2">
<div class="w-full md:w-1/3 px-2"></div>
<div class="w-full md:w-2/3 px-2">
<button
type="submit"
class="button loading"
:class="fetching ? 'loading-show' : ''"
:disabled="fetching"
>
Submit
</button>
</div>
</div>
</div>
</form>
</template>
</div>
</template>
<script>
import axios from 'axios';
import form from '../../form';
export default {
data() {
return {
loading: true,
fetching: true,
form: {
title: '',
comment: '',
},
errors: {},
}
},
mounted() {
let self = this;
axios.get(route('items.show', { item: self.$route.params.id }))
.then(res => {
self.modify(res.data.item);
})
.catch(err => {
})
.then(() => {
self.fetching = false;
self.loading = false;
});
},
methods: {
edit() {
let self = this;
self.fetching = true;
axios.patch(route('items.update', { item: self.$route.params.id }),{
title: self.form.title,
comment: self.form.comment,
})
.then(res => {
form.note('Edit request succeeded.', 8000);
self.errors = {};
self.$router.push({ name: 'items.index' });
})
.catch(err => {
self.errors = form.errors(err);
})
.then(() => {
self.fetching = false;
});
},
modify(item) {
this.form.title = item.title;
this.form.comment = item.comment;
},
}
}
</script>
📍resources/js/Components/Items/Edit.vue
<template>
<div class="crud-edit">
<div class="crud-header">
<button
class="button"
@click="$router.push({ name: 'items.index' })"
:disabled="fetching"
>
<Fa :icon="[ 'fas', 'arrow-left' ]" /> Go back
</button>
</div>
<div class="crud-content text-center">
<img src="images/thinking-emoji.png" class="mb-5" alt="Thinking emoji" />
<h2>Pretty sure you want to delete this?</h2>
</div>
<div class="crud-footer text-center">
<form @submit.prevent="remove()">
<button
type="submit"
class="button danger loading"
:class="fetching ? 'loading-show' : ''"
:disabled="fetching"
>
Delete
</button>
<button
type="button"
class="button ml-1"
@click="$router.push({ name: 'items.index' })"
:disabled="fetching"
>
Cancel
</button>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios';
import form from '../../form';
export default {
data() {
return {
fetching: false,
}
},
methods: {
remove() {
let self = this;
self.fetching = true;
axios.delete(route('items.destroy', { item: self.$route.params.id }))
.then(res => {
form.note('Delete request succeeded.', 8000);
self.$router.push({ name: 'items.index' });
})
.catch(err => {
})
.then(() => {
self.fetching = false;
});
},
}
}
</script>
📍resources/js/Components/Items/Delete.vue
🔥 Pro Tip:
I need to mention that you should be careful about redirecting after the creation and the edition requests in case user need to do that multiple times. It's so annoying for the user to go back over and over to submit the form again but it's not an issue in our case. Thanks to @DesTheDev suggestion in this part - #1.
- To show the correct date, we need to configure the date fields inside of our
Items
model:
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class Item extends Model
{
protected $fillable = [
'title', 'comment',
];
protected $appends = ['written_at', 'modified_at', 'created_date', 'updated_date'];
protected $casts = [
'created_at' => 'date:Y-m-d',
'updated_at' => 'date:Y-m-d',
];
public function getCreatedDateAttribute()
{
return Carbon::parse($this->created_at)->format('Y-m-d H:i:s');
}
public function getUpdatedDateAttribute()
{
return Carbon::parse($this->updated_at)->format('Y-m-d H:i:s');
}
public function getWrittenAtAttribute()
{
return Carbon::parse($this->created_at)->diffForHumans();
}
public function getModifiedAtAttribute()
{
return Carbon::parse($this->updated_at)->diffForHumans();
}
}
📍app/Item.php
- You can version your assets when compiling to production using the if statement inside
webpack.mix.js
:
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.copyDirectory('resources/images', 'public/images')
.options({
processCssUrls: false,
postCss: [
tailwindcss('./tailwind.js'),
],
});
+ if (mix.inProduction()) {
+ mix.version();
+ }
📍webpack.mix.js
- you'r Done!