Why This Topic Is Useful
Main Problem Developers Face
Reuse component using alpine js store
What Is a Child Component?
What Is a Parent Component?
What Is Alpine.store?
Why Use Alpine.store for Shared Modal?
coding example to reuse modal using alpine js store
Why This Topic Is Useful
Each parent needs the same popup/modal.
Instead of creating separate modals again and again, you create one child modal component and control it globally using Alpine.store().
This is very practical for Laravel Blade projects because many pages need common popups like:
| Use Case | Example |
|---|---|
| Delete confirmation | “Are you sure you want to delete this doctor?” |
| View details modal | Show doctor, hospital, booking, or user details |
| Edit popup | Open same edit modal with different data |
| Image preview | Open image preview from different cards |
| Booking action | Confirm, cancel, approve, reject booking |
| Alert modal | Show success/error message from different sections |
Main Problem Developers Face
Without a shared Alpine store, developers often write duplicate modal code inside every parent component.
Example problem:
<!-- Parent 1 modal -->
<div x-data="{ open: false }">
...
</div>
<!-- Parent 2 modal -->
<div x-data="{ open: false }">
...
</div>
<!-- Parent 3 modal -->
<div x-data="{ open: false }">
...
</div>
This becomes hard to maintain.
If you change modal design, you must update many places.
Better Solution
Main Problem Developers Face
Use one global store:
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('modal', {
open: false,
title: '',
message: '',
data: null,
show(title, message, data = null) {
this.title = title;
this.message = message;
this.data = data;
this.open = true;
},
close() {
this.open = false;
this.title = '';
this.message = '';
this.data = null;
}
});
});
</script>
Now any parent can open the same modal.
Example Parent Component 1
<button
@click="$store.modal.show(
'Delete Doctor',
'Are you sure you want to delete this doctor?',
{ id: 101, type: 'doctor' }
)"
class="px-4 py-2 bg-red-600 text-white rounded-lg">
Delete Doctor
</button>
Example Parent Component 2
<button
@click="$store.modal.show(
'Cancel Booking',
'Do you want to cancel this booking?',
{ id: 55, type: 'booking' }
)"
class="px-4 py-2 bg-yellow-600 text-white rounded-lg">
Cancel Booking
</button>
Single Reusable Child Modal
<div
x-data
x-show="$store.modal.open"
class="fixed inset-0 flex items-center justify-center bg-black/50 z-50"
x-cloak>
<div class="bg-white rounded-xl shadow-lg w-full max-w-md p-6">
<h2 class="text-xl font-bold mb-3" x-text="$store.modal.title"></h2>
<p class="text-gray-600 mb-5" x-text="$store.modal.message"></p>
<div class="flex justify-end gap-3">
<button
@click="$store.modal.close()"
class="px-4 py-2 border rounded-lg">
Cancel
</button>
<button
@click="console.log($store.modal.data)"
class="px-4 py-2 bg-blue-600 text-white rounded-lg">
Confirm
</button>
</div>
</div>
</div>
Explain that in Laravel Blade or normal HTML pages, developers often repeat modal code many times. Alpine.store helps manage shared frontend state globally.
What Is a Child Component?
A child component is a reusable UI block, like:
Modal
Popup
Alert box
Confirm dialog
Image preview
Toast notification
What Is a Parent Component?
A parent component is any section that triggers the child component.
Example:
Doctor card opens modal
Booking row opens modal
User table opens modal
Hospital profile opens modal
What Is Alpine.store?**
Alpine.store() is used to create global state in Alpine.js. This means multiple components can read and update the same data.
5.
Why Use Alpine.store for Shared Modal?
Because it gives:
| Advantage | Benefit |
|---|---|
| One modal code | No duplicate popup HTML |
| Global access | Any button can open modal |
| Cleaner Blade files | Less repeated frontend code |
| Easy maintenance | Update design in one place |
| Dynamic data support | Send title, message, ID, type, status |
| Better structure | Parent handles trigger, child handles UI |
Real Laravel Use Case
Example:
@foreach($doctors as $doctor)
<button
@click="$store.modal.show(
'View Doctor',
'Doctor: {{ $doctor->name }}',
{ id: '{{ $doctor->id }}', type: 'doctor' }
)">
View
</button>
@endforeach
One modal can display data from many doctors.
Best Practices
Use Alpine.store when the same component is needed by many sections. Keep modal UI separate. Pass only required data like ID, title, message, type, and action. Avoid putting heavy business logic directly inside Alpine. For Laravel actions like delete/update, call backend routes or APIs safely.
Parent component imports JS
Child modal component stays separate
Global state is managed through Alpine.store()
Same child component can be reused from multiple parent components
coding example to reuse modal using alpine js store
Goal
You want this flow:
Parent Component 1
↓
opens same child modal
Parent Component 2
↓
opens same child modal
Parent Component 3
↓
opens same child modal
Reusable Child Modal Component
↓
controlled by Alpine.store()
This is best when you have common UI like:
Delete confirmation modal
View details modal
Status update modal
Image preview modal
Booking cancel modal
Doctor approve/reject modal
Recommended Folder Structure
resources/
├── views/
│ ├── layouts/
│ │ └── app.blade.php
│ │
│ ├── pages/
│ │ └── doctors/
│ │ └── index.blade.php
│ │
│ └── components/
│ └── modals/
│ └── reusable-action-modal.blade.php
│
├── js/
│ ├── app.js
│ │
│ ├── stores/
│ │ └── modal-store.js
│ │
│ └── components/
│ └── doctor-actions.js
Install Required Frontend Packages
If Tailwind and Alpine are already installed, skip this.
npm install alpinejs
npm install -D tailwindcss postcss autoprefixer
Optional component library:
npm install flowbite
or
npm install daisyui
Create Alpine Store File
Create this file:
resources/js/stores/modal-store.js
export default function registerModalStore(Alpine) {
Alpine.store('actionModal', {
isOpen: false,
title: '',
message: '',
confirmText: 'Confirm',
cancelText: 'Cancel',
actionType: null,
payload: null,
open(config = {}) {
this.title = config.title || 'Confirm Action';
this.message = config.message || 'Are you sure you want to continue?';
this.confirmText = config.confirmText || 'Confirm';
this.cancelText = config.cancelText || 'Cancel';
this.actionType = config.actionType || null;
this.payload = config.payload || null;
this.isOpen = true;
},
close() {
this.isOpen = false;
this.title = '';
this.message = '';
this.confirmText = 'Confirm';
this.cancelText = 'Cancel';
this.actionType = null;
this.payload = null;
},
confirm() {
window.dispatchEvent(
new CustomEvent('action-modal:confirmed', {
detail: {
actionType: this.actionType,
payload: this.payload,
},
})
);
this.close();
},
});
}
What this file does
This creates a global modal store named:
$store.actionModal
Now any Blade file, parent component, table row, card, or button can open the same modal.
- Register Store in app.js
Open:
resources/js/app.js
Add this:
import Alpine from 'alpinejs';
import registerModalStore from './stores/modal-store';
import './components/doctor-actions';
window.Alpine = Alpine;
document.addEventListener('alpine:init', () => {
registerModalStore(Alpine);
});
Alpine.start();
Why this is important
This is the main entry file. It imports:
modal-store.js
doctor-actions.js
So your parent logic and child modal store stay clean and separate.
- Create Parent Action JS File
Create:
resources/js/components/doctor-actions.js
window.addEventListener('action-modal:confirmed', function (event) {
const { actionType, payload } = event.detail;
if (!actionType || !payload) {
return;
}
if (actionType === 'delete-doctor') {
deleteDoctor(payload);
}
if (actionType === 'block-doctor') {
blockDoctor(payload);
}
if (actionType === 'approve-doctor') {
approveDoctor(payload);
}
});
function deleteDoctor(payload) {
const form = document.getElementById(`delete-doctor-form-${payload.id}`);
if (form) {
form.submit();
}
}
function blockDoctor(payload) {
const form = document.getElementById(`block-doctor-form-${payload.id}`);
if (form) {
form.submit();
}
}
function approveDoctor(payload) {
const form = document.getElementById(`approve-doctor-form-${payload.id}`);
if (form) {
form.submit();
}
}
What this file does
This listens when the child modal confirms an action.
Example:
User clicks Delete Doctor
↓
Parent opens modal
↓
User clicks Confirm
↓
Modal dispatches event
↓
doctor-actions.js catches event
↓
Correct form submits
This keeps business action logic separate from modal UI.
- Create Reusable Child Modal Component
Create:
resources/views/components/modals/reusable-action-modal.blade.php
<div
x-data
x-show="$store.actionModal.isOpen"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4"
@keydown.escape.window="$store.actionModal.close()"
>
<div
class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl"
@click.outside="$store.actionModal.close()"
>
<div class="mb-4">
<h2
class="text-xl font-semibold text-gray-900"
x-text="$store.actionModal.title"
></h2>
<p
class="mt-2 text-sm text-gray-600"
x-text="$store.actionModal.message"
></p>
</div>
<div class="flex justify-end gap-3">
<button
type="button"
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
@click="$store.actionModal.close()"
x-text="$store.actionModal.cancelText"
></button>
<button
type="button"
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
@click="$store.actionModal.confirm()"
x-text="$store.actionModal.confirmText"
></button>
</div>
</div>
</div>
Important
Add this CSS once to hide Alpine content before load:
[x-cloak] {
display: none !important;
}
Usually add it in:
resources/css/app.css
- Include Child Modal in Main Layout
Open:
resources/views/layouts/app.blade.php
Example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Panel</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-100">
<main>
@yield('content')
</main>
{{-- Global reusable modal --}}
<x-modals.reusable-action-modal />
</body>
</html>
Why include it in layout?
Because this modal becomes globally available on every page.
So any parent component can open it.
- Parent Component Example: Doctor List Page
Create or open:
resources/views/pages/doctors/index.blade.php
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-6xl p-6">
<h1 class="mb-6 text-2xl font-bold text-gray-900">
Doctors List
</h1>
<div class="overflow-hidden rounded-xl bg-white shadow">
<table class="w-full border-collapse">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-700">
Name
</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-700">
Email
</th>
<th class="px-4 py-3 text-right text-sm font-semibold text-gray-700">
Action
</th>
</tr>
</thead>
<tbody>
@foreach($doctors as $doctor)
<tr class="border-t">
<td class="px-4 py-3 text-sm text-gray-800">
{{ $doctor->name }}
</td>
<td class="px-4 py-3 text-sm text-gray-600">
{{ $doctor->email }}
</td>
<td class="px-4 py-3 text-right">
<button
type="button"
class="rounded-lg bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700"
@click="$store.actionModal.open({
title: 'Delete Doctor',
message: 'Are you sure you want to delete {{ $doctor->name }}?',
confirmText: 'Yes, Delete',
cancelText: 'No, Cancel',
actionType: 'delete-doctor',
payload: {
id: {{ $doctor->id }}
}
})"
>
Delete
</button>
<form
id="delete-doctor-form-{{ $doctor->id }}"
action="{{ route('doctors.destroy', $doctor->id) }}"
method="POST"
class="hidden"
>
@csrf
@method('DELETE')
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endsection
- Another Parent Component: Block Doctor Button
Same child modal can be used here also.
<button
type="button"
class="rounded-lg bg-yellow-500 px-4 py-2 text-sm text-white hover:bg-yellow-600"
@click="$store.actionModal.open({
title: 'Block Doctor',
message: 'Are you sure you want to block this doctor account?',
confirmText: 'Yes, Block',
cancelText: 'Cancel',
actionType: 'block-doctor',
payload: {
id: {{ $doctor->id }}
}
})"
>
Block
</button>
<form
id="block-doctor-form-{{ $doctor->id }}"
action="{{ route('doctors.block', $doctor->id) }}"
method="POST"
class="hidden"
>
@csrf
@method('PATCH')
</form>
- Another Parent Component: Approve Doctor Button
<button
type="button"
class="rounded-lg bg-green-600 px-4 py-2 text-sm text-white hover:bg-green-700"
@click="$store.actionModal.open({
title: 'Approve Doctor',
message: 'Do you want to approve this doctor profile?',
confirmText: 'Yes, Approve',
cancelText: 'Cancel',
actionType: 'approve-doctor',
payload: {
id: {{ $doctor->id }}
}
})"
>
Approve
</button>
<form
id="approve-doctor-form-{{ $doctor->id }}"
action="{{ route('doctors.approve', $doctor->id) }}"
method="POST"
class="hidden"
>
@csrf
@method('PATCH')
</form>
- Example Routes
use App\Http\Controllers\DoctorController;
Route::delete('/doctors/{doctor}', [DoctorController::class, 'destroy'])
->name('doctors.destroy');
Route::patch('/doctors/{doctor}/block', [DoctorController::class, 'block'])
->name('doctors.block');
Route::patch('/doctors/{doctor}/approve', [DoctorController::class, 'approve'])
->name('doctors.approve');
- Example Controller
namespace App\Http\Controllers;
use App\Models\Doctor;
use Illuminate\Http\RedirectResponse;
class DoctorController extends Controller
{
public function destroy(Doctor $doctor): RedirectResponse
{
$doctor->delete();
return back()->with('success', 'Doctor deleted successfully.');
}
public function block(Doctor $doctor): RedirectResponse
{
$doctor->update([
'status' => 'blocked',
]);
return back()->with('success', 'Doctor blocked successfully.');
}
public function approve(Doctor $doctor): RedirectResponse
{
$doctor->update([
'status' => 'approved',
]);
return back()->with('success', 'Doctor approved successfully.');
}
}
- Build Frontend Assets
Run:
npm run dev
For production:
npm run build
Final Practical Flow
resources/js/stores/modal-store.js
↓
creates global Alpine modal store
resources/views/components/modals/reusable-action-modal.blade.php
↓
child modal reads store data
resources/views/pages/doctors/index.blade.php
↓
parent buttons open modal with different data
resources/js/components/doctor-actions.js
↓
handles confirm event and submits correct form
Best Practice
Use this approach when the child component is common and reusable, like:
Confirm modal
Alert modal
Image preview modal
Status update modal
Delete popup
Approval popup
Top comments (0)