In this tutorial we’ll create Laravel 9 CRUD app with classic views utilizing Bootstrap 5 along with JQuery (the good ol’ days!). Laravel 9.x requires a minimum PHP version of 8.0. This tutorial assumes that you are using Linux, MacOS or WSL on Windows and Node.js, Composer and PHP 8 along with required modules i.e bcmatch, sqlite, mbstring, xml, zip, gd, mcrypt are properly installed. Also make sure that SQLite is installed as we’ll be using SQLite to keep things simple and quick; you may use MySQL or PostgreSQL as you like, however make sure the corresponding PHP module is installed too.
Using Composer, enter following command in terminal to install Laravel 9;
composer create-project laravel/laravel app 9.*
Next, setup database and Laravel environment file. Here, I’m configuring SQLite database for simplicity purposes. Being inside parent directory (app), execute following command to create new SQLite database file;
touch database/database.sqlite
Then find .env config file in the parent directory (app), open it and change DB_CONNECTION line as follows;
DB_CONNECTION = sqlite
You can remove DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env when using SQLite database.
Since, initial setup of Laravel app is complete, we can move towards MVC (Model, View, Controller). First enter the following command in parent directory to create Post model along with the migration:
php artisan make:model Post -m
Open newly created migration file from database/migrations/ which is named something like …create_posts_table.php and then insert the table fields as follows;
<?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('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
};
Enter following command in parent directory to migrate the above migration;
php artisan migrate
Since our database part is done, let’s move onto Model and Controller. First, open app/Models/Post.php model file and insert protected fillable array as follows;
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title', 'body'
];
}
Having done with the Model part of our app, we move onto Controller. Execute following command in parent directory to create PostController.php;
php artisan make:controller PostController
Go ahead and open this controller file which is located at app/Http/Controllers/PostController.php and amend it as follows in order to define basic CRUD (Create, Read, Update, Delete) functions;
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use App\Models\Post;
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return view('posts.index', compact('posts'));
}
public function create()
{
return view('posts.create');
}
public function store()
{
Post::create(
Request::validate([
'title' => ['required', 'max:50'],
'body' => ['required'],
])
);
return Redirect::route('posts')->with('success', 'Post created.');
}
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Post $post)
{
$post->update(
Request::validate([
'title' => ['required', 'max:50'],
'body' => ['required'],
])
);
return Redirect::route('posts')->with('success', 'Post updated.');
}
public function destroy(Post $post)
{
$post->delete();
return Redirect::back()->with('success', 'Post deleted.');
}
}
In order to define routes, open routes/web.php and put the following line at the upper side of it;
use App\Http\Controllers\PostController;
And then insert following CRUD routes into this routes/web.php just after welcome route;
Route::get('posts', [PostController::class, 'index'])
->name('posts');
Route::get('posts/create', [PostController::class, 'create'])
->name('posts.create');
Route::post('posts', [PostController::class, 'store'])
->name('posts.store');
Route::get('posts/{post}/edit', [PostController::class, 'edit'])
->name('posts.edit');
Route::put('posts/{post}', [PostController::class, 'update'])
->name('posts.update');
Route::delete('posts/{post}', [PostController::class, 'destroy'])
->name('posts.destroy');
We’re done with the Model and Controller parts of the app. Now let’s create Views using Blade templates.
First, create directories named layouts and posts inside resources/views directory. Remaining in parent directory, you may execute following command to make these directories;
mkdir resources/views/layouts resources/views/posts
Now create app.blade.php inside resources/views/layouts and following three files inside resources/views/posts directory:
- index.blade.php
- create.blade.php
- edit.blade.php
You can execute following command inside parent directory to create these files in one go;
touch resources/views/layouts/app.blade.php resources/views/posts/{index,create,edit}.blade.php
Insert following code in resources/views/layouts/app.blade.php;
<!DOCTYPE html>
<html>
<head>
<title>Laravel 9 CRUD with Bootstrap 5</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
@yield('content')
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</body>
</html>
Go inside resources/views/posts folder and insert following code into index.blade.php;
@extends('layouts.app')
@section('content')
<div class="row">
<div class="m-2">
<div class="p-2">
<h2>Laravel 9 CRUD with Bootstrap 5</h2>
</div>
<div class="p-2">
<a class="btn btn-primary" href="{{ route('posts.create') }}">Create New Post</a>
</div>
</div>
</div>
@if ($message = session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ $message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<table class="table table-hover">
<tr>
<th width="10%">S.no.</th>
<th>Title</th>
<th>Body</th>
<th width="20%">Actions</th>
</tr>
<?php $i = 0; ?>
@foreach ($posts as $post)
<tr>
<td>{{ ++$i }}</td>
<td>{{ $post->title }}</td>
<td>{{ $post->body }}</td>
<td class="d-flex justify-content-around">
<a class="btn btn-secondary" href="{{ route('posts.edit', $post->id) }}">Edit</a>
<form action="{{ route('posts.destroy', $post->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</td>
</tr>
@endforeach
</table>
@endsection
Insert following code into create.blade.php;
@extends('layouts.app')
@section('content')
<div class="card m-4">
<div class="card-header">
<h4>Create Post</h4>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="post" action="{{ route('posts.store') }}">
@csrf
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" name="title"/>
</div>
<div class="form-group">
<label for="body">Body</label>
<textarea class="form-control" name="body"></textarea>
</div>
<button type="submit" class="btn btn-secondary m-2">Add Post</button>
</form>
</div>
</div>
@endsection
Insert following code into edit.blade.php;
@extends('layouts.app')
@section('content')
<div class="card m-4">
<div class="card-header">
<h4>Edit Post</h4>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('posts.update', $post->id) }}" method="POST">
@csrf
@method('PUT')
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" name="title" value="{{ $post->title }}"/>
</div>
<div class="form-group">
<label for="body">Body</label>
<textarea class="form-control" name="body">{{ $post->body }}</textarea>
</div>
<button type="submit" class="btn btn-secondary m-2">Submit</button>
</form>
</div>
</div>
@endsection
Finally, enter following command to start PHP development server;
php artisan serve
Enter following URL in the browser and test CRUD funtions;
http://localhost:8000/posts
That’s it for CRUD part in Laravel 9 using Bootstrap 5; you can stop reading here if you came for CRUD. However, since we’ve also fetched JQuery into the app.blade.php layout, it’s fitting that we give this old buddy some respect (although Bootstrap has done away with JQuery since version 5 and uses vanilla JS). Down below we’ll fortify our forms with couple of tricks using JQuery. These steps are quintessential for making HTML forms behave like good kids.
There are couple of inherent problems with the HTML forms as coded in both create.blade.php and edit.blade.php. First, if you happen to press Enter key anywhere on form while typing in data, the form will be submitted immediately – that’s unpleasant experience since majority of people impulsively press Enter when they are done typing in a field. So, as developer you have to stop submitting the form on every Enter – although you have to allow Enter key presses on submit button and in textarea fields. Second, a lot of people impulsively double-click submit button which causes double submission of form data and sometimes duplicate data is entered in the database. So you have to stop double submission of data by disabling submit button once it has been pressed.
So, let’s utilize JQuery in order to address the above issues. Put following code at the end of both create.blade.php and edit.blade.php in resources/views/posts folder;
@section('script')
<script>
$(document).ready(function() {
$(document).on("keydown", ":input:not(textarea):not(:submit)", function(event) {
if(event.keyCode == 13) {
event.preventDefault();
return false;
}
});
$('.prevent-multi').on('submit', function(){
$('.prevent-multi-submit').attr('disabled','true');
return true;
});
});
</script>
@endsection
Now assign prevent-multi class to <form> tag in both create.blade.php and edit.blade.php, e.g.;
<form method="post" action="{{ route('posts.store') }}" class="prevent-multi">
Similarly, assign prevent-multi-submit class to submit button in both create.blade.php and edit.blade.php, e.g.;
<button type="submit" class="btn btn-secondary m-2 prevent-multi-submit">Add Post</button>
Finally, put following code (right after <script> tags) inside resources/views/layouts/app.blade.php;
@yield('script')
Save all files, start PHP development server if not running already, load localhost:8000/posts/create in browser and try to press Enter inside Title field; it won’t work. Again try Enter in Body field; it’ll work since it’s textarea type field. Now, after filling data in form fields, try to double-click speedily on submit button; it’ll submit form only once and then immediately disable submit button restricting the possibility of double submission.
Our final view files are as follows;
resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>
<head>
<title>Laravel 9 CRUD with Bootstrap 5</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
@yield('content')
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
@yield('script')
</body>
</html>
resources/views/posts/create.blade.php
@extends('layouts.app')
@section('content')
<div class="card m-4">
<div class="card-header">
<h4>Create Post</h4>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="post" action="{{ route('posts.store') }}" class="prevent-multi">
@csrf
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" name="title"/>
</div>
<div class="form-group">
<label for="body">Body</label>
<textarea class="form-control" name="body"></textarea>
</div>
<button type="submit" class="btn btn-secondary m-2 prevent-multi-submit">Add Post</button>
</form>
</div>
</div>
@endsection
@section('script')
<script>
$(document).ready(function() {
$(document).on("keydown", ":input:not(textarea):not(:submit)", function(event) {
if(event.keyCode == 13) {
event.preventDefault();
return false;
}
});
$('.prevent-multi').on('submit', function(){
$('.prevent-multi-submit').attr('disabled','true');
return true;
});
});
</script>
@endsection
resources/views/posts/edit.blade.php
@extends('layouts.app')
@section('content')
<div class="card m-4">
<div class="card-header">
<h4>Edit Post</h4>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('posts.update', $post->id) }}" method="POST" class="prevent-multi">
@csrf
@method('PUT')
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" name="title" value="{{ $post->title }}"/>
</div>
<div class="form-group">
<label for="body">Body</label>
<textarea class="form-control" name="body">{{ $post->body }}</textarea>
</div>
<button type="submit" class="btn btn-secondary m-2 prevent-multi-submit">Submit</button>
</form>
</div>
</div>
@endsection
@section('script')
<script>
$(document).ready(function() {
$(document).on("keydown", ":input:not(textarea):not(:submit)", function(event) {
if(event.keyCode == 13) {
event.preventDefault();
return false;
}
});
$('.prevent-multi').on('submit', function(){
$('.prevent-multi-submit').attr('disabled','true');
return true;
});
});
</script>
@endsection