In this tutorial, we’ll show you step-by-step how to build a full CRUD system using Alpine.js, Tailwind CSS, and jQuery DataTable, complete with modals for a smooth user experience.You'll also learn how to integrate modals for better user experience while managing FAQs.
Why Alpine.js, Tailwind CSS, and jQuery DataTable?
- Alpine.js: A lightweight JavaScript framework that makes it easy to add interactivity to your web pages without the complexity of larger frameworks like Vue or React.
- Tailwind CSS: A utility-first CSS framework that allows you to design responsive and modern UIs quickly.
- jQuery DataTable: A powerful jQuery plugin that enhances HTML tables with features like sorting, pagination, and searching.
Step 1: Setting Up the Table with jQuery DataTable
Add these links in the head section of your layout.
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.js"></script>
<script src="{{ asset('tailwind/dt-tailwind.js') }}"></script>
<table class="is-hoverable w-full text-left" id="faqDataTable">
<thead>
<tr>
<th class="whitespace-nowrap rounded-tl-lg bg-slate-200 px-4 py-3 font-semibold uppercase text-slate-800 dark:bg-navy-800 dark:text-navy-100 lg:px-5">#</th>
<th class="whitespace-nowrap bg-slate-200 px-4 py-3 font-semibold uppercase text-slate-800 dark:bg-navy-800 dark:text-navy-100 lg:px-5">FAQ</th>
<th class="whitespace-nowrap rounded-tr-lg bg-slate-200 px-4 py-3 font-semibold uppercase text-slate-800 dark:bg-navy-800 dark:text-navy-100 lg:px-5">Action</th>
</tr>
</thead>
<tbody>
@foreach ($faqs as $faq)
<tr class="border-y border-transparent border-b-slate-200 dark:border-b-navy-500">
<td class="whitespace-nowrap px-4 py-3 sm:px-5">{{ $faq->id }}</td>
<td class="px-4 py-3 sm:px-5 text-sm">
<div class="font-semibold text-slate-800 dark:text-slate-200">
<strong>Question</strong> : {{ $faq->question }}
</div>
<div class="mt-1 text-slate-600 dark:text-slate-400">
<strong>Answer</strong> {{ $faq->answer }}
</div>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm sm:px-5">
<div class="flex space-x-2">
<button class="btn h-8 w-8 p-0 text-info hover:bg-info/20 focus:bg-info/20 active:bg-info/25 edit-post-button"
data-post-id="{{ $faq->id }}"
data-question="{{ $faq->question }}"
data-answer="{{ $faq->answer }}">
<i class="fa fa-edit"></i>
</button>
<button class="btn h-8 w-8 p-0 text-error hover:bg-error/20 focus:bg-error/20 active:bg-error/25 delete-post-button"
data-post-id="{{ $faq->id }}">
<i class="fa fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
In this table, we have assigned an id so that we can make it DataTable using jQuery and after that we will use Modal using Alpine for edit and delete with confirmation.
Step 2: Adding Modals with Alpine.js
Modals are a great way to handle CRUD operations without redirecting users to a new page. We’ll use Alpine.js to manage the state of our modals.
Delete Modal
The delete modal allows users to confirm before deleting an FAQ.
// Store for delete modal
Alpine.store('modal', {
visible: false,
actionUrl: null,
openModal(actionUrl) {
this.actionUrl = actionUrl;
this.visible = true;
},
closeModal() {
this.visible = false;
this.actionUrl = null;
}
});
When the delete button is clicked, the modal opens with the appropriate action URL for deletion
Edit Modal
The edit modal allows users to update FAQs. It pre-fills the form with the selected FAQ’s data.
// Store for edit modal
Alpine.store('editStore', {
visible: false,
faq: {
id: null,
question: '',
answer: ''
},
openEditModal(data) {
this.faq = data; // Set the FAQ data
this.visible = true; // Show the modal
},
closeEditModal() {
this.visible = false; // Hide the modal
this.faq = {
id: null,
question: '',
answer: ''
}; // Reset the FAQ data
}
});
When the edit button is clicked, the modal opens with old data inside fields if exists
$(document).ready(function() {
let table = $("#faqDataTable").DataTable();
// Delete button handler
$('#faqDataTable').on('click', '.delete-post-button', function(e) {
e.preventDefault();
let postId = $(this).data('post-id');
let actionUrl = "{{ route('faqs.destroy', '__ID__') }}".replace('__ID__', postId);
Alpine.store('modal').openModal(actionUrl);
});
// Edit button handler
$('#faqDataTable').on('click', '.edit-post-button', function(e) {
e.preventDefault();
// Retrieve data from the button's data-* attributes
let postId = $(this).data('post-id');
let question = $(this).data('question');
let answer = $(this).data('answer');
// Prepare the FAQ data object
let faqData = {
id: postId,
question: question,
answer: answer
};
// Open the edit modal using the store
Alpine.store('editStore').openEditModal(faqData);
});
});
This is how our edit modal looks like, which we are pre-fllling with old data.
<template x-teleport="#x-teleport-target">
<div class="fixed inset-0 z-[100] flex flex-col items-center justify-center px-4 py-6 sm:px-5"
x-show="$store.editStore.visible" role="dialog" @keydown.window.escape="$store.editStore.closeEditModal()">
<!-- Modal Overlay -->
<div class="absolute inset-0 bg-slate-900/60 transition-opacity duration-300"
@click="$store.editStore.closeEditModal()" x-show="$store.editStore.visible" x-transition:enter="ease-out"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
</div>
<!-- Modal Content -->
<div class="relative w-full max-w-2xl bg-white rounded-lg shadow-lg dark:bg-navy-700"
x-show="$store.editStore.visible" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
<!-- Modal Header -->
<div class="flex justify-between items-center bg-slate-200 px-4 py-3 dark:bg-navy-800 sm:px-5 rounded-t-lg">
<h3 class="text-base font-medium text-slate-700 dark:text-navy-100">
Edit FAQ
</h3>
<button @click="$store.editStore.closeEditModal()"
class="btn -mr-1.5 h-7 w-7 rounded-full p-0 hover:bg-slate-300/20 focus:bg-slate-300/20 active:bg-slate-300/25 dark:hover:bg-navy-300/20 dark:focus:bg-navy-300/20 dark:active:bg-navy-300/25">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-5">
<form x-bind:action="'{{ route('faqs.update', '') }}/' + $store.editStore.faq.id" method="POST" id="edit-faq-form">
@csrf
@method('PUT')
<input type="hidden" name="id" :value="$store.editStore.faq.id">
<!-- Question Field -->
<div class="mb-4">
<label class="block">
<span class="mb-2 text-slate-700 dark:text-slate-300">Question</span>
<input name="question" type="text" required
x-model="$store.editStore.faq.question"
class="form-input mt-1.5 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-700 placeholder:text-slate-400/70 hover:border-slate-400 focus:border-primary focus:ring-1 focus:ring-primary dark:border-navy-450 dark:bg-navy-900 dark:text-slate-300 dark:hover:border-navy-400 dark:focus:border-accent dark:focus:ring-accent"
placeholder="Enter your question" autofocus>
</label>
</div>
<!-- Answer Field -->
<div class="mb-4">
<label class="block">
<span class="mb-2 text-slate-700 dark:text-slate-300">Answer</span>
<textarea name="answer" rows="4" required
x-model="$store.editStore.faq.answer"
class="form-textarea mt-1.5 w-full resize-none rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-700 placeholder:text-slate-400/70 hover:border-slate-400 focus:border-primary focus:ring-1 focus:ring-primary dark:border-navy-450 dark:bg-navy-900 dark:text-slate-300 dark:hover:border-navy-400 dark:focus:border-accent dark:focus:ring-accent"
placeholder="Enter your answer"></textarea>
</label>
</div>
<!-- Modal Footer -->
<div class="flex justify-end gap-4">
<button type="button" @click="$store.editStore.closeEditModal()"
class="btn bg-gray-500 text-white hover:bg-gray-600">Cancel</button>
<button type="submit"
class="btn bg-warning font-medium text-white hover:bg-warning-focus focus:bg-warning-focus active:bg-warning-focus/90">
Save
</button>
</div>
</form>
</div>
</div>
</div>
</template>
This is how our delete modal looks like, which we are using for delete confirmation.
<!-- Modal (outside the table) -->
<div id="x-teleport-target"></div>
<template x-teleport="#x-teleport-target">
<div class="fixed inset-0 z-[100] flex flex-col items-center justify-center px-4 py-6 sm:px-5"
x-show="$store.modal.visible" role="dialog" @keydown.window.escape="$store.modal.closeModal()"
id="deletePostModal">
<!-- Modal Overlay -->
<div class="absolute inset-0 bg-slate-900/60 transition-opacity duration-300"
@click="$store.modal.closeModal()" x-show="$store.modal.visible" x-transition:enter="ease-out"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
</div>
<!-- Modal Content -->
<div class="relative w-full max-w-2xl bg-white rounded-lg shadow-lg dark:bg-navy-700"
x-show="$store.modal.visible" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
<!-- Modal Header -->
<div class="flex justify-between items-center bg-slate-200 px-4 py-3 dark:bg-navy-800 sm:px-5 rounded-t-lg">
<h3 class="text-base font-medium text-slate-700 dark:text-navy-100">
DELETE POST
</h3>
<button @click="$store.modal.closeModal()"
class="btn -mr-1.5 h-7 w-7 rounded-full p-0 hover:bg-slate-300/20 focus:bg-slate-300/20 active:bg-slate-300/25 dark:hover:bg-navy-300/20 dark:focus:bg-navy-300/20 dark:active:bg-navy-300/25">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Modal Body -->
<div class="p-5">
<p class="mt-4 text-sm dark:text-white-200 mb-4">
Are you sure you want to delete this record? This action cannot be undone.
</p>
<!-- Modal Footer -->
<div class="flex justify-end gap-4">
<button type="button" @click="$store.modal.closeModal()"
class="btn bg-gray-500 text-white hover:bg-gray-600">Cancel</button>
<form :action="$store.modal.actionUrl" method="POST" id="delete-form">
@csrf
@method('DELETE')
</form>
<button type="button"
class="btn bg-error font-medium text-white hover:bg-error-focus focus:bg-error-focus active:bg-error-focus/90"
@click="document.getElementById('delete-form').submit()">Delete</button>
</div>
</div>
</div>
</div>
</template>