ypsilon-event-handler
Version:
A production-ready event handling system for web applications with memory leak prevention, and method chaining support
407 lines (381 loc) • 16.3 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YpsilonEventHandler Declarative Delegation Example</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<link rel="stylesheet" type="text/css" href="./assets/main.css">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f3f4f6;
margin: 0;
overflow-y: scroll;
}
.content {
max-width: 800px;
margin: 0 auto;
padding: 10px;
}
nav ul {
list-style: none;
padding: 0;
display: flex;
gap: 10px;
}
nav a {
color: #2563eb;
text-decoration: none;
}
.nav-link.active {
font-weight: bold;
color: #1e40af;
}
#main-content {
background-color: white;
padding: 20px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
#main-content h1 {
font-size: 1.25rem;
font-weight: bold;
margin-top: 5px;
margin-bottom: 10px;
}
.y-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
margin-right: 6px;
}
#add-item {
background-color: #22c55e;
color: white;
}
#trigger-custom {
background-color: #9333ea;
color: white;
}
#toggle-effect {
background-color: #f59e0b;
color: white;
}
#item-list {
list-style: disc;
padding-left: 20px;
margin: 14px 0;
}
.dynamic-item {
color: #2563eb;
cursor: pointer;
}
#main-content.highlight {
background-color: #fef08a;
/* padding: 2px 4px; */
border-radius: 4px;
}
#notification {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
}
.toast {
margin-bottom: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background: #28a745;
color: white;
border-radius: 4px;
cursor: default;
transition: all .4s ease-in-out;
}
.toast span {
padding: 0 15px;
display: block;
max-width: 95%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toast button {
margin: 0;
padding: 10px 15px;
background: #237e38;
border: none;
color: white;
cursor: pointer;
font-weight: bold;
font-size: 22px;
}
.toast:not(:last-of-type) {
border-bottom: 1px solid #c3c3c3;
}
.y-nulled {
display: block;
}
.y-fade-in {
opacity: 1;
}
.credit {
font-size: 0.8rem;
color: #4b5563;
margin: 0;
padding: 1.2rem;
text-align: center;
}
.remarks {
margin: 12px 0 0;
padding: 6px 12px;
background-color: #f9fafb;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
border-radius: 4px;
font-size: 0.9rem;
color: #374151;
}
.remarks h2 {
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 8px;
}
.remarks ul {
list-style: disc;
padding-left: 20px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="content">
<nav>
<ul>
<li><a href="#home" class="nav-link y-btn" data-action="handleNavClick">Home</a></li>
<li><a href="#about" class="nav-link y-btn" data-action="handleNavClick">About</a></li>
<li><a href="#contact" class="nav-link y-btn" data-action="handleNavClick">Contact</a></li>
</ul>
</nav>
<div id="main-content">
<h1>Welcome to the Home Page</h1>
<p>This is a simple SPA using YpsilonEventHandler's declarative delegation.</p>
<button id="add-item" class="y-btn" data-action="handleAddItem">Add Dynamic Item</button>
<button id="trigger-custom" class="y-btn" data-action="handleTriggerCustom">Trigger Custom Event</button>
<button id="toggle-effect" class="y-btn" data-action="handleToggleEffect">Toggle Highlight Effect</button>
<ul id="item-list"></ul>
<div class="remarks">
<h2>About This Example</h2>
<p>This example, generated by Grok 3 (xAI), showcases YpsilonEventHandler's <code>handleEvent</code> pattern in a lightweight SPA. As one user said, "The marks are great!" Here's how it works and why it's powerful:</p>
<ul>
<li><strong>Single Listener</strong>: A <code>body</code> click listener routes all interactions via <code>data-action</code> (e.g., navigation, toast removal).</li>
<li><strong>Dynamic Elements</strong>: Adds list items with <code>data-action="handleItemClick"</code> without new listeners.</li>
<li><strong>Custom Events</strong>: Dispatches <code>app:custom</code> events, shown in toasts.</li>
<li><strong>Notifications</strong>: Toasts stack with 3-second timeouts and close buttons, using <code>showNotification</code> and <code>removeToast</code>.</li>
<li><strong>Navigation</strong>: Hash-based for <code>file://</code> compatibility, with active nav highlighting.</li>
<li><strong>Performance</strong>: <code>y-btn</code> validation optimizes handling.</li>
</ul>
<p><strong>Usage Tips</strong>: Use <code>data-action</code> for dynamic UIs, extend with <code>input</code> or <code>change</code> events, and call <code>handler.destroy()</code> for cleanup. For scroll events, add a listener on a persistent element (e.g., <code>#main-content</code>) with <code>.scroll-area { height: 300px; overflow-y: scroll; }</code> and delegate to <code>.scroll-area</code>. Try it to see YpsilonEventHandler's ~370-line magic!</p>
</div>
</div>
<footer style="margin-bottom: 4.5rem;">
<p class="credit">Generated by Grok 3 (xAI). Feedback: "The marks are great!"</p>
</footer>
<!-- Navigation to other examples -->
<nav style="box-shadow: 0 0 10px #6a8cd7; width: 100%; text-align: center; margin: 2rem 0 1rem; padding: 1rem; background: #f7f7f7; border-radius: 0; display: flex; flex-wrap: wrap; width: 100%; justify-content: center; width: 100%; position: fixed; bottom: 0; left: 0;">
<a href="./basic-example.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #2563eb; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Basic Examples</a>
<a href="./spa.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #16a34a; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">SPA Demo</a>
<a href="./reactive-y.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #dc2626; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Reactive Demo</a>
<a href="./single-listener-multiple-actions.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #7c3aed; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Single Listener</a>
<a href="./ai-reviews.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #ea580c; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">AI Reviews</a>
<a href="https://github.com/eypsilon/YpsilonEventHandler" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #374151; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">GitHub</a>
</nav>
<div id="notification"></div>
</div>
<!-- YpsilonEventHandler CDN -->
<script src="https://cdn.jsdelivr.net/npm/ypsilon-event-handler@1.5.0/ypsilon-event-handler.min.js"></script>
<script>
class AppHandler extends YpsilonEventHandler {
constructor() {
super({
// General click delegation for all buttons with y-btn class
'body': [
{ type: 'click' } // Falls back to handleClick, routes to data-action handlers
],
// Popstate for SPA navigation (hash-based)
'window': [
{ type: 'popstate', handler: 'handlePopstate' }
],
// Custom event
'document': [
{ type: 'app:custom', handler: 'handleCustomEvent' }
]
});
this.itemCount = 0;
this.initialContent = '';
// Initial content load based on hash
const currentPath = window.location.hash || '#home';
this.updateContent(currentPath);
// Also set active nav item for initial load
this.setActiveNavItem(currentPath);
}
// General click router: Uses handleEvent to route clicks to handlers via data-action
handleClick(event, target) {
// Pre-validate with y-btn class for performance
if (!(target instanceof Element) || !target.classList.contains('y-btn')) return;
// Delegate based on data-action
const action = target.dataset.action;
if (!action) return;
// Call the specified handler if it exists
if (typeof this[action] === 'function') {
return this[action](event, target);
}
}
handleNavClick(event, target) {
event.preventDefault();
const path = target.getAttribute('href');
// Remove active class from all nav links
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
// Add active class to clicked link
target.classList.add('active');
window.location.hash = path;
this.updateContent(path);
}
handlePopstate(event, target) {
this.updateContent(window.location.hash || '#home');
}
handleAddItem(event, target) {
const list = document.getElementById('item-list');
const li = document.createElement('li');
li.className = 'dynamic-item y-btn';
li.textContent = `Dynamic Item ${++this.itemCount}`;
li.dataset.action = 'handleItemClick';
list.appendChild(li);
}
handleItemClick(event, target) {
this.showNotification(`Clicked item: ${target.textContent}`);
}
handleTriggerCustom(event, target) {
this.dispatch('app:custom', { message: `Hello from item ${this.itemCount}!` });
}
handleCustomEvent(event, target) {
const message = event.detail?.message || 'No message';
this.showNotification(`Custom event triggered: ${message}`);
}
handleToggleEffect(event, target) {
const mainContent = document.getElementById('main-content');
mainContent.classList.toggle('highlight');
this.showNotification(`Highlight effect ${mainContent.classList.contains('highlight') ? 'enabled' : 'disabled'}`);
}
handleRemoveToast(event, target) {
this.removeToast(event, target);
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
removeToast(event, target) {
// Guard against invalid elements
if (!(target instanceof Element)) return;
// If target is the close button, get the parent toast div
const actualToast = target.classList.contains('toast') ? target : target.closest('.toast');
if (!actualToast || !actualToast.parentNode) return;
// Clear the timer if it exists
if (actualToast.dataset.timerId) {
clearTimeout(parseInt(actualToast.dataset.timerId));
}
actualToast.remove();
const notification = document.getElementById('notification');
// If no toasts left, reset notification container
if (notification.children.length === 0) {
notification.classList.remove('show', 'y-nulled');
notification.textContent = this.initialContent || '';
}
}
showNotification(message) {
const notification = document.getElementById('notification');
if (!notification.classList.contains('y-nulled')) {
notification.classList.add('y-nulled');
if (!this.initialContent) {
this.initialContent = notification.innerHTML;
}
notification.textContent = '';
}
const note = document.createElement('div');
note.innerHTML = `
<span>${this.escapeHtml(message)}</span>
<button class="y-btn" data-action="handleRemoveToast">×</button>
`;
notification.classList.add('show');
note.className = 'toast y-fade-in';
notification.appendChild(note);
// Individual timer for this toast (3 seconds)
const toastTimer = setTimeout(() => {
this.removeToast(null, note);
}, 3000);
// Store timer on the element
note.dataset.timerId = toastTimer;
}
updateContent(path) {
const content = document.getElementById('main-content');
let title, text;
switch (path) {
case '#about':
title = 'About Us';
text = 'Learn more about our SPA powered by YpsilonEventHandler!';
break;
case '#contact':
title = 'Contact Us';
text = 'Reach out to us via this cool SPA interface!';
break;
default:
title = 'Welcome to the Home Page';
text = "This is a simple SPA using YpsilonEventHandler's declarative delegation.";
path = '#home'; // Ensure default path for active class
}
content.innerHTML = `
<h1>${title}</h1>
<p>${text}</p>
<button id="add-item" class="y-btn" data-action="handleAddItem">Add Dynamic Item</button>
<button id="trigger-custom" class="y-btn" data-action="handleTriggerCustom">Trigger Custom Event</button>
<button id="toggle-effect" class="y-btn" data-action="handleToggleEffect">Toggle Highlight Effect</button>
<ul id="item-list"></ul>
<div class="remarks">
<h2>About This Example</h2>
<p>This example, generated by Grok 3 (xAI), showcases YpsilonEventHandler's <code>handleEvent</code> pattern in a lightweight SPA. As one user said, "The marks are great!" Here's how it works and why it's powerful:</p>
<ul>
<li><strong>Single Listener</strong>: A <code>body</code> click listener routes all interactions via <code>data-action</code> (e.g., navigation, toast removal).</li>
<li><strong>Dynamic Elements</strong>: Adds list items with <code>data-action="handleItemClick"</code> without new listeners.</li>
<li><strong>Custom Events</strong>: Dispatches <code>app:custom</code> events, shown in toasts.</li>
<li><strong>Notifications</strong>: Toasts stack with 3-second timeouts and close buttons, using <code>showNotification</code> and <code>removeToast</code>.</li>
<li><strong>Navigation</strong>: Hash-based for <code>file://</code> compatibility, with active nav highlighting.</li>
<li><strong>Performance</strong>: <code>y-btn</code> validation optimizes handling.</li>
</ul>
<p><strong>Usage Tips</strong>: Use <code>data-action</code> for dynamic UIs, extend with <code>input</code> or <code>change</code> events, and call <code>handler.destroy()</code> for cleanup. For scroll events, add a listener on a persistent element (e.g., <code>#main-content</code>) with <code>.scroll-area { height: 300px; overflow-y: scroll; }</code> and delegate to <code>.scroll-area</code>. Try it to see YpsilonEventHandler's ~370-line magic!</p>
</div>
`;
this.setActiveNavItem(path);
this.itemCount = 0; // Reset item count on navigation
}
setActiveNavItem(path) {
// Set active class on nav link matching the current path
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === path) {
link.classList.add('active');
}
});
}
}
// Initialize the handler
const handler = new AppHandler();
</script>
</body>
</html>