Appearance
Notification Components
WARNING
- Update Reference table at he bottom of this page.
NOTE
This chapter introduces a notification system (toasts and confirmation modals) primarily to enhance the user experience of our application. While implementing robust user feedback is a good practice, the specific techniques shown here are a practical example and not a mandatory part of the core curriculum.
Notifications are essential for providing feedback to users in web applications. They can signal the success or failure of actions, alert users to important information, or confirm potentially destructive operations like deleting data.
In this chapter, we introduce a reusable notification system built with Livewire and Alpine.js. This system provides two key features:
- Toast Notifications: Small, temporary messages that appear briefly on the screen (e.g., "Record updated successfully!").
- Confirmation Modals: Dialog boxes that prompt the user to confirm an action before proceeding (e.g., "Are you sure you want to delete this item?").
We will leverage a combination of a JavaScript helper, a Blade component, and a PHP Trait to implement this system efficiently.
Preparation
To implement the notification system, you will need to create the following three files in your project:
app/Traits/NotificationsTrait.php
(Create theTraits
directory insideapp
if it doesn't exist).resources/js/toast.js
.resources/views/components/itf/notifications.blade.php
.
The specific code for these files is provided below. You need to create these files and paste the corresponding code into them.
toast.js
This JavaScript file provides the client-side logic for handling toast notifications using Alpine.js. It manages the display, positioning, timing, and dismissal (including swipe-to-dismiss on touch devices) of toasts.
- First, ensure this script is loaded by importing it into your main application JavaScript file (
resources/views/js/app.js
):
javascript
import './toast.js';
1
- Next, create the file
resources/js/toast.js
and add the following code:
javascript
// Define the toast function globally or make it available to Alpine
window.toast = function() {
return {
groupedToasts: {},
defaultPosition: 'top',
init(defaultPosition) {
this.defaultPosition = defaultPosition;
this.groupedToasts = {
'top-right': [],
'top-left': [],
'bottom-right': [],
'bottom-left': [],
'top': [],
'bottom': []
};
// Optional: Touch events for swipe-to-dismiss
// Note: Accuracy depends on unique IDs or simple scenarios
let startX = 0;
let swipedToastElement = null;
document.addEventListener('touchstart', e => {
const toastEl = e.target.closest('.pointer-events-auto[x-show]');
if (toastEl) {
startX = e.changedTouches[0].screenX;
swipedToastElement = toastEl; // Remember which element started the swipe
} else {
startX = null;
swipedToastElement = null;
}
}, { passive: true });
document.addEventListener('touchend', e => {
if (startX === null || !swipedToastElement) return;
const endX = e.changedTouches[0].screenX;
const deltaX = endX - startX;
if (Math.abs(deltaX) > 50) { // Swipe threshold
// Find the position and index of the swiped toast data
// This is still a bit fragile without unique IDs linking DOM to data.
// It assumes the DOM order roughly matches the array order within a position group.
for (const position in this.groupedToasts) {
const group = this.groupedToasts[position];
let foundIndex = -1;
// Attempt to find the index based on the element reference
// This requires iterating through the DOM elements managed by this position's template
// Or making assumptions. Let's stick to a simpler assumption for now:
// Find the first visible toast in the group related to the swiped element's container
const container = swipedToastElement.closest('.fixed[class*="'+position+'"]');
if (container) {
foundIndex = group.findIndex(toast => toast.visible); // Find first visible in this group
}
if (foundIndex !== -1) {
this.remove(position, foundIndex);
break; // Assume only one swipe action processed
}
}
}
// Reset swipe state
startX = null;
swipedToastElement = null;
});
},
show(data) {
// data is dispatched as [ { ...toastData } ] from Livewire 3
const toastData = data[0] || {};
const validPositions = ['top-right', 'top-left', 'bottom-right', 'bottom-left', 'top', 'bottom'];
const position = validPositions.includes(toastData.position) ? toastData.position : this.defaultPosition;
// Determine icon visibility: Use provided value, else default based on variant
let showIcon = toastData.icon;
if (typeof showIcon !== 'boolean') {
showIcon = ['info', 'success', 'warning', 'danger'].includes(toastData.variant);
}
const toast = {
visible: true, // Start visible
variant: toastData.variant || 'default',
heading: toastData.heading || null,
text: toastData.text || '',
icon: showIcon || false,
// Add a unique ID for potentially more robust swipe removal later?
id: Date.now() + Math.random()
};
if (!this.groupedToasts[position]) {
this.groupedToasts[position] = [];
}
this.groupedToasts[position].push(toast);
const duration = typeof toastData.duration === 'number' ? toastData.duration : 4000; // Default 4s
if (duration > 0) {
setTimeout(() => {
// Find the toast again in case others were added/removed
const index = this.groupedToasts[position].indexOf(toast);
if (index !== -1) {
this.remove(position, index);
}
}, duration);
}
},
remove(position, index) {
// Check if the position and index are valid
if (!this.groupedToasts[position] || !this.groupedToasts[position][index]) {
return; // Toast might already be removed (e.g., rapid clicks/swipes)
}
// Set visibility to false to trigger leave transition
this.groupedToasts[position][index].visible = false;
// Use a timeout to allow the leave transition to complete before removing from array
setTimeout(() => {
// Check again in case things changed during the timeout
// Find the specific toast object to remove, checking visibility flag
const toastToRemove = this.groupedToasts[position].find(t => !t.visible);
if(toastToRemove) {
const actualIndex = this.groupedToasts[position].indexOf(toastToRemove);
if (actualIndex > -1) {
this.groupedToasts[position].splice(actualIndex, 1);
}
}
}, 300); // Match the leave transition duration (e.g., duration-200 + buffer)
}
};
}
// console.log('Toast function initialized.'); // For debugging
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
Explanation: This script defines an Alpine.js component (window.toast
) that listens for browser events (dispatched by Livewire) to show toasts. It manages different positions, variants (colors/icons), automatic dismissal after a duration, and manual closing via a button or swipe gesture.
notifications.blade.php
This Blade component provides the HTML structure and Alpine.js logic for both the toast notification container and the confirmation modal dialog. It listens for browser events dispatched by the NotificationsTrait
(covered next) to display the appropriate notification.
- Create the file
resources/views/components/itf/notifications.blade.php
and add the following code:
php
@props([
'toastPosition' => 'top-right', // Default position for toasts if not specified in the event data
])
<div>
{{-- Toast Container --}}
<div
x-data="toast()"
x-init="init('{{ $toastPosition }}')"
x-on:toast-show.window="show($event.detail)" {{-- Listen for Livewire 3 dispatch --}}
class="fixed z-[5000] pointer-events-none inset-0 flex flex-col"
aria-live="assertive"
>
<template x-for="(group, position) in groupedToasts" :key="position">
<div class="fixed z-[5000]"
:class="{
'top-4 right-4': position === 'top-right',
'top-4 left-4': position === 'top-left',
'bottom-4 right-4': position === 'bottom-right',
'bottom-4 left-4': position === 'bottom-left',
'top-4 inset-x-4 flex justify-center': position === 'top',
'bottom-4 inset-x-4 flex justify-center': position === 'bottom'
}">
<template x-for="(toast, index) in group" :key="index">
<div
x-show="toast.visible"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="pointer-events-auto mb-2 w-80 max-w-full transform rounded-lg shadow-lg overflow-hidden mx-4"
:class="{
'bg-zinc-50 dark:bg-zinc-700/50 border border-zinc-200 dark:border-zinc-900': toast.variant === 'default',
'bg-sky-50 dark:bg-sky-700/50 border border-sky-200 dark:border-sky-900': toast.variant === 'info',
'bg-green-50 dark:bg-green-700/50 border border-green-200 dark:border-green-900': toast.variant === 'success',
'bg-yellow-50 dark:bg-yellow-700/50 border border-yellow-200 dark:border-yellow-900': toast.variant === 'warning',
'bg-red-50 dark:bg-red-700/50 border border-red-200 dark:border-red-900': toast.variant === 'danger'
}"
>
<!-- Toast Content -->
<div class="p-4">
<div class="flex items-start">
<template x-if="toast.icon">
<div class="flex-shrink-0 mr-3">
{{-- Assuming flux:icons components exist --}}
<template x-if="toast.variant === 'success'">
<flux:icon.check-circle class="text-2xl text-green-700 dark:text-green-200"/>
</template>
<template x-if="toast.variant === 'danger'">
<flux:icon.exclamation-circle class="text-2xl text-red-700 dark:text-red-200"/>
</template>
<template x-if="toast.variant === 'warning'">
<flux:icon.exclamation-triangle class="text-2xl text-yellow-700 dark:text-yellow-200"/>
</template>
<template x-if="toast.variant === 'info'">
<flux:icon.information-circle class="text-2xl text-sky-700 dark:text-sky-200"/>
</template>
<template x-if="toast.variant === 'default'">
<flux:icon.bell class="text-2xl text-zinc-500 dark:text-zinc-400"/>
</template>
</div>
</template>
<div class="flex-1 pt-0.5">
<template x-if="toast.heading">
<h3
class="text-sm font-medium mb-1"
:class="{
'text-zinc-800 dark:text-white': toast.variant === 'default',
'text-sky-800 dark:text-sky-200': toast.variant === 'info',
'text-green-800 dark:text-green-200': toast.variant === 'success',
'text-yellow-800 dark:text-yellow-200': toast.variant === 'warning',
'text-red-800 dark:text-red-200': toast.variant === 'danger',
}"
x-text="toast.heading"
></h3>
</template>
<div
class="text-sm"
:class="{
'text-zinc-700 dark:text-zinc-300': toast.variant === 'default',
'text-sky-700 dark:text-sky-300': toast.variant === 'info',
'text-green-700 dark:text-green-300': toast.variant === 'success',
'text-yellow-700 dark:text-yellow-300': toast.variant === 'warning',
'text-red-700 dark:text-red-300': toast.variant === 'danger'
}"
x-html="toast.text"
></div>
</div>
<div class="ml-3 flex-shrink-0">
<button
type="button"
class="inline-flex rounded-md focus:outline-none"
:class="{
'bg-zinc-50 text-zinc-700 hover:text-zinc-800 dark:bg-zinc-800/50 dark:text-zinc-500 dark:hover:text-zinc-400': toast.variant === 'default',
'bg-sky-50 text-sky-700 hover:text-sky-800 dark:bg-sky-900/50 dark:text-sky-500 dark:hover:text-sky-400': toast.variant === 'info',
'bg-green-50 text-green-700 hover:text-green-800 dark:bg-green-900/50 dark:text-green-500 dark:hover:text-green-400': toast.variant === 'success',
'bg-yellow-50 text-yellow-700 hover:text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-500 dark:hover:text-yellow-400': toast.variant === 'warning',
'bg-red-50 text-red-700 hover:text-red-800 dark:bg-red-900/50 dark:text-red-500 dark:hover:text-red-400': toast.variant === 'danger'
}"
@click="remove(position, index)"
>
<span class="sr-only">Close</span>
{{-- Assuming flux:icons component exists --}}
<flux:icon.x-mark class="h-5 w-5"/>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
{{-- /Toast Container --}}
{{-- Confirm Container --}}
<div
x-cloak
x-data="{ open: false, params: [{}] }"
@keydown.escape.window="open = false"
@confirm-show.window="open = true; params = $event.detail" {{-- Listen for Livewire 3 dispatch --}}
role="dialog"
aria-modal="true"
x-show="open"
class="confirm-container"
{{-- Add aria-labelledby and aria-describedby if needed based on heading/text --}}
>
{{-- Overlay --}}
<div x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-[6000] bg-zinc-500/50 backdrop-blur-xs"></div>
{{-- Modal Panel --}}
<div class="fixed inset-0 z-[6000] overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4 text-center">
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
@click.away="open = false"
class="relative transform overflow-hidden rounded-lg bg-white dark:bg-zinc-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg p-6"
:class="params[0].extraClasses">
{{-- Modal Content --}}
<h3 x-show="params[0].heading" x-text="params[0].heading"
class="text-lg font-semibold leading-6 text-zinc-900 dark:text-white mb-2" id="modal-title"></h3>
<div class="mt-2">
<p x-html="params[0].text" class="text-sm text-zinc-600 dark:text-zinc-300" id="modal-description"></p>
</div>
{{-- Modal Footer/Actions --}}
<div class="mt-5 sm:mt-6 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-3 space-y-2 sm:space-y-0">
{{-- Assuming flux:button component exists --}}
<flux:button type="button" @click.stop="open = false">
<span x-text="params[0].cancelText"></span>
</flux:button>
<flux:button type="button" variant="primary" @click.stop="eval(params[0].nextEvent); open = false">
<span x-text="params[0].confirmText"></span>
</flux:button>
</div>
</div>
</div>
</div>
</div>
{{-- /Confirm Container --}}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
Explanation: This component sets up two Alpine.js contexts:
- One for toasts (
x-data="toast()"
), which uses thetoast.js
logic and listens fortoast-show
events. It renders toasts based on thegroupedToasts
data. - One for the confirmation modal (
x-data="{ open: false, params: [{}] }"
), which listens forconfirm-show
events. When triggered, it displays a modal using the data passed in the event detail (params
). The confirm button useseval()
to execute a JavaScript string (provided by the Trait) that typically dispatches another Livewire event to trigger the confirmed action.
NotificationsTrails.php
A PHP Trait allows you to reuse methods across different classes. This trait provides convenient helper methods within your Livewire components to dispatch the browser events needed to trigger the toast and confirmation notifications defined in the Blade component.
- Create the file
app/Traits/NotificationsTrait.php
and add the following code:
php
<?php
namespace App\Traits;
use Arr;
use Illuminate\Support\Js;
trait NotificationsTrait
{
/**
* Dispatch a toast notification (Livewire 3).
*
* @param string $variant The style variant ('default', 'info', 'success', 'warning', 'danger').
* @param string $text The main message text for the toast.
* @param array $options Additional options: heading, duration, position, icon.
* @return void
*/
protected function showToast(string $variant, string $text, array $options = []): void
{
$showIcon = Arr::get($options, 'icon');
if (!is_bool($showIcon)) {
$showIcon = in_array($variant, ['info', 'success', 'warning', 'danger']);
}
$payload = [ // Alpine component expects an array containing one object
'text' => $text,
'variant' => $variant,
'heading' => Arr::get($options, 'heading'),
'duration' => Arr::get($options, 'duration', 4000), // Default 4s
'position' => Arr::get($options, 'position'), // Alpine component handles null/default
'icon' => $showIcon,
];
// Livewire 3 dispatch to browser event listener
$this->dispatch('toast-show', $payload);
}
/** Show default toast. */
public function toast(string $text, array $options = []): void
{
$this->showToast('default', $text, $options);
}
/** Show info toast. */
public function toastInfo(string $text, array $options = []): void
{
$this->showToast('info', $text, $options);
}
/** Show success toast. */
public function toastSuccess(string $text, array $options = []): void
{
$this->showToast('success', $text, $options);
}
/** Show warning toast. */
public function toastWarning(string $text, array $options = []): void
{
// Default duration for warning might be longer
$options['duration'] = Arr::get($options, 'duration', 5000);
$this->showToast('warning', $text, $options);
}
/** Show danger/error toast. */
public function toastDanger(string $text, array $options = []): void
{
// Default duration for danger might be longer
$options['duration'] = Arr::get($options, 'duration', 6000);
$this->showToast('danger', $text, $options);
}
/** Alias for danger toast. */
public function toastError(string $text, array $options = []): void
{
$this->toastDanger($text, $options);
}
/**
* Dispatch a confirmation modal dialog (Livewire 3).
* Dispatches a Livewire event on confirm, suitable for Route Model Binding listeners.
*
* @param string $text The confirmation question/text.
* @param array $options Configuration: heading, confirmText, cancelText, class, next => [onEvent, params...].
* 'next' => [
* 'onEvent' => (string) Name of the Livewire event to dispatch.
* 'paramName1' => value1, // For Route Model Binding, use lowercase model name as key (e.g., 'genre' => $genre->id)
* 'paramName2' => value2, // Additional parameters
* // ...
* ]
* @return void
*/
public function confirm(string $text, array $options = []): void
{
$nextAction = Arr::get($options, 'next', []);
$onEvent = Arr::get($nextAction, 'onEvent');
if (!$onEvent) {
trigger_error('Confirmation modal requires a "next.onEvent" option.', E_USER_WARNING);
return; // Stop if no event name provided
}
// Extract parameters for the event, excluding 'onEvent' itself
$eventParams = Arr::except($nextAction, ['onEvent']);
// Prepare the JavaScript parameters object using Js::from
// Ensures proper encoding of different data types for JS
$jsEventParams = Js::from($eventParams);
// Construct the JavaScript string to dispatch the JavaScript event using $dispatch
// $dispatch('event-name', { key1: value1, key2: value2 })
// Note: $dispatch needs escaping in the PHP string to become literal $wire in JS
$nextEventJs = "\$dispatch('{$onEvent}', {$jsEventParams})";
// Prepare the payload for the Alpine confirm component
$payload = [ // Alpine component expects an array containing one object
'text' => $text,
'heading' => Arr::get($options, 'heading', 'Are you sure?'), // Default heading
'confirmText' => Arr::get($options, 'confirmText', 'Confirm'),
'cancelText' => Arr::get($options, 'cancelText', 'Cancel'),
'nextEvent' => $nextEventJs, // JS string: $wire.dispatch(...)
'extraClasses' => Arr::get($options, 'class'), // Map 'class' option to 'extraClasses' for Alpine
];
// Livewire 3 dispatch to browser event listener
$this->dispatch('confirm-show', $payload);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
Explanation: This trait provides simple methods (toastSuccess
, toastDanger
, confirm
, etc.) that encapsulate the logic for dispatching the correct Livewire browser events (toast-show
, confirm-show
) with the expected data payload. The confirm
method is particularly important as it constructs the JavaScript needed ($nextEventJs
) to trigger a subsequent Livewire action after the user clicks the confirm button in the modal.
Usage
Update the Layout
To make the notification component available on all pages, include it in your main layout file, typically near the end of the <body>
tag, before @fluxScripts
or other closing script tags.
- Open
resources/views/components/layouts/vinylshop.blade.php
and add the<x-itf.notifications />
component:
php
<!DOCTYPE html>
<html lang="{{ config('app.locale', 'en') }}">
<head>
{{-- ... --}}
</head>
<body class="bg-zinc-50 dark:bg-zinc-900">
{{-- ... --}}
<x-itf.notifications toastPosition="bottom-right" /> // Optionally set a default toast position like: <x-itf.notifications toastPosition="bottom-right" />
@fluxScripts
@stack('scripts')
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Update the Error Template
On, for example, the 404 error page, you still see parts of the Confirm container without any content. The container has a class confirm-container
, but it's empty and we need to remove it from all the error pages.
- Open
resources/views/errors/minimal.blade.php
and remove theconfirm-container
:
php
<x-layouts.vinylshop>
<div class="grid grid-rows-2 grid-flow-col gap-4">
<p class="row-span-2 text-red-700 dark:text-red-300 text-5xl text-right border-r-2 border-zinc-200 dark:border-b-zinc-700 pr-4">
@yield('code')
</p>
<p class="text-2xl font-light text-zinc-400 dark:text-zinc-300">
@yield('message')
</p>
<div>
<flux:button variant="primary" icon="home" href="{{ route('home') }}">Home</flux:button>
<flux:button variant="primary" href="#" icon="arrow-uturn-left" onclick="window.history.back();">Back</flux:button>
</div>
</div>
@push('scripts')
<script>
document.getElementById('toggle-mode').remove();
document.getElementById('auth-buttons').remove();
document.querySelector('.confirm-container').remove(); // Remove the confirm container
</script>
@endpush
</x-layouts.vinylshop>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Use in Livewire Components
Now you can use the NotificationsTrait
within your Livewire components to trigger toasts and confirmation modals.
- Import the Trait: Add
use NotificationsTrait;
inside your Livewire component class. - Call Trait Methods: Call methods like
$this->toastSuccess('message')
or$this->confirm('Are you sure?', [...options...])
from your Livewire actions.
Example Scenario: Deleting a genre with confirmation and a success toast.
(This example is for demonstration purposes; you don't need to implement it right now.)
php
<?php
namespace App\Livewire;
use App\Models\Genre;
use App\Traits\NotificationsTrait;
use Livewire\Attributes\On;
use Livewire\Component;
class NotificationTest extends Component
{
use NotificationsTrait;
// Action triggered by a button click in the view
public function confirmDelete(Genre $genre)
{
// Call the confirm method from the trait
$this->confirm(
"delete the genre <b>{$genre->name}</d>?", // Main text (accepts HTML)
[
// Options for the confirmation modal
'heading' => 'Confirm Deletion',
'class' => '!bg-orange-50', // Custom CSS class for the modal panel
'confirmText' => 'Yes, Delete It!',
'cancelText' => 'No, Cancel',
'next' => [ // Configuration for the action *after* confirmation
'onEvent' => 'delete-genre', // Livewire event to dispatch on confirm
'genre' => $genre->id // Parameter(s) for the event listener
]
]
);
}
// Listener for the event dispatched after confirmation
#[On('delete-genre')]
public function delete(Genre $genre) // Livewire automatically resolves the Genre model from the ID
{
// Perform the actual deletion
$genre->delete();
// Show a success toast notification
$this->toastDanger(
"Genre <b>{$genre->name}</b> deleted successfully!",
[
'duration' => 5000, // Show for 5 seconds
'position' => 'top-right', // Override default position if needed
]
);
}
public function render()
{
$genres = Genre::orderBy('name')->get();
return view('livewire.notification-test', compact('genres'));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Reference
<x-itf.notifications />
Component
This component should be placed once in your main layout file.
Attribute | Prop | Default | Description |
---|---|---|---|
toast-position | toastPosition | 'top-right' | Default position for toasts. Options: top-right , top-left , bottom-right , bottom-left , top , bottom . |
NotificationsTrait
Methods
Use these methods within your Livewire components after adding use NotificationsTrait;
.
Toast Methods
All toast methods share the same signature: methodName(string $text, array $options = [])
.
toast(string $text, ...)
: Shows a default styled toast.toastInfo(...)
: Shows an informational (blue) toast.toastSuccess(...)
: Shows a success (green) toast.toastWarning(...)
: Shows a warning (yellow) toast (default 5s duration).toastDanger(...)
: Shows a danger (red) toast (default 6s duration).toastError(...)
: An alias fortoastDanger
.
The following keys can be passed in the $options
array:
Key | Default | Description |
---|---|---|
heading | null | Optional title for the toast. |
duration | 4000 | Time in ms before auto-closing (0 = manual close). Varies for warning (5000) and danger (6000). |
position | null | Overrides the component's default position. E.g., 'top-right' , 'bottom-left' . |
icon | true for standard variants | true /false to force show/hide icon. Defaults to true for info/success/warning/danger. |
Confirmation Modal Method
The confirm
method shows a modal dialog to the user: confirm(string $text, array $options = [])
.
The following keys can be passed in the $options
array:
Key | Default | Description |
---|---|---|
heading | 'Are you sure?' | The title of the modal dialog. |
confirmText | 'Confirm' | Text for the confirmation button. |
cancelText | 'Cancel' | Text for the cancellation button. |
class | null | Additional CSS classes to apply to the modal panel (e.g., for custom backgrounds). |
next | [] | Required. An array defining the action to take upon confirmation. See details below. |
The next
array configures the Livewire event that is dispatched when the user clicks the confirm button.
Key in next | Description |
---|---|
onEvent | Required. The name of the Livewire event to dispatch (e.g., 'delete-genre' ). |
[paramKey] | Any additional key/value pairs to sen |