Async Handler, Sleep, and Sanitize
Quick Start (30 seconds)
// asyncHandler — auto-manages loading state on button click
button.addEventListener('click', AsyncHelpers.asyncHandler(async (e) => {
await AsyncHelpers.sleep(500); // wait half a second
await saveData();
}, { loadingClass: 'saving' }));
// sleep — pause async execution
async function init() {
showSplashScreen();
await AsyncHelpers.sleep(2000); // show for 2 seconds
hideSplashScreen();
}
// sanitize — clean user input to prevent XSS
const safeHtml = AsyncHelpers.sanitize(userInput, {
allowedTags: ['b', 'i', 'em', 'strong']
});asyncHandler() — Async Event Listeners with Loading States
What Is It?
asyncHandler wraps an async event listener so that while the async work is running, the event's target element automatically receives a loading CSS class and a data attribute. Both are removed when the work finishes — whether it succeeded or failed.
The Problem Without asyncHandler
button.addEventListener('click', async (e) => {
// Add loading state
button.classList.add('loading');
button.setAttribute('data-loading', 'true');
try {
await saveData();
} catch (error) {
console.error(error);
} finally {
// Remove loading state — must remember to do this in finally!
button.classList.remove('loading');
button.removeAttribute('data-loading');
}
});Problems: ❌ Boilerplate for every async event handler ❌ Easy to forget the finally block — loading state gets stuck ❌ Must manually reference the element
With asyncHandler
button.addEventListener('click', AsyncHelpers.asyncHandler(async (e) => {
await saveData();
// Loading state is removed automatically, even if this throws
}));✅ Loading class and attribute managed automatically ✅ Always cleaned up in finally — never gets stuck ✅ Clean, focused handler code
Syntax
AsyncHelpers.asyncHandler(handler, options)handler— an async function(event, ...args) => Promiseoptions.loadingClass— CSS class to add to the element (default:'loading')options.loadingAttribute— data attribute to set (default:'data-loading')options.errorHandler— function called if the handler throws(error, event) => void
Examples
Basic Usage
document.getElementById('saveBtn').addEventListener('click',
AsyncHelpers.asyncHandler(async (e) => {
await AsyncHelpers.fetchJSON('/api/save', {
method: 'POST',
body: Forms.myForm.values
});
console.log('Saved!');
})
);The button element gets class="loading" and data-loading="true" while saving, then both are removed automatically.
Custom Loading Class
submitBtn.addEventListener('click',
AsyncHelpers.asyncHandler(async (e) => {
await processOrder();
}, {
loadingClass: 'btn-submitting',
loadingAttribute: 'data-submitting'
})
);With Error Handling
deleteBtn.addEventListener('click',
AsyncHelpers.asyncHandler(async (e) => {
if (!confirm('Are you sure?')) return;
await deleteItem(itemId);
item.remove();
}, {
errorHandler: (error, event) => {
console.error('Delete failed:', error.message);
showErrorBanner('Could not delete. Please try again.');
}
})
);CSS for the Loading State
/* Style the button while its async handler is running */
.loading {
opacity: 0.7;
cursor: not-allowed;
pointer-events: none;
}
/* Or with attribute selector */
[data-loading="true"] {
position: relative;
}
[data-loading="true"]::after {
content: ' ⏳';
}sleep() — Promise-Based Delay
What Is It?
sleep(ms) pauses async execution for a specified number of milliseconds. It's simply a Promise that resolves after ms milliseconds — the async-friendly equivalent of "wait a moment."
Syntax
await AsyncHelpers.sleep(milliseconds);Examples
// Wait 1 second
await AsyncHelpers.sleep(1000);
// Show a success message for 2 seconds, then redirect
async function handleSuccess() {
showSuccessMessage('Account created!');
await AsyncHelpers.sleep(2000);
window.location.href = '/onboarding';
}
// Artificial delay between animations
async function animateSteps() {
await Elements.step1.fadeIn();
await AsyncHelpers.sleep(500); // pause between steps
await Elements.step2.fadeIn();
await AsyncHelpers.sleep(500);
await Elements.step3.fadeIn();
}Inside Animation Chains
sleep is already built into the animation chain as .delay(), but you can use it inside .next() callbacks for more complex logic:
await Elements.notification
.animate()
.slideDown()
.next(async () => {
await AsyncHelpers.sleep(500); // extra pause for complex logic
updateNotificationContent();
})
.slideUp()
.play();sanitize() — XSS-Safe Input Cleaning
What Is It?
sanitize(input, options) cleans a string to remove malicious content — specifically, XSS (Cross-Site Scripting) attack vectors like <script> tags, javascript: URLs, and inline event handlers (onclick, onerror, etc.).
Why this matters: If your app displays user-provided content in the DOM, unsanitized input could allow an attacker to inject JavaScript that runs in your users' browsers.
Syntax
const safe = AsyncHelpers.sanitize(input, options);Options
AsyncHelpers.sanitize(input, {
allowedTags: [], // HTML tags to keep; all others are stripped (default: none)
removeScripts: true, // strip <script> tags and javascript: URLs (default: true)
removeEvents: true, // strip inline event handlers like onclick (default: true)
removeStyles: false // strip style attributes (default: false)
});Examples
Strip Everything (Default — No HTML Allowed)
const userInput = '<script>alert("XSS!")</script> Hello!';
const safe = AsyncHelpers.sanitize(userInput);
// → '<script>alert("XSS!")</script> Hello!'
// All HTML is entity-encoded — safe to display as textAllow Only Specific Tags
const content = '<b>Hello</b> <script>alert("bad")</script> <i>World</i>';
const safe = AsyncHelpers.sanitize(content, {
allowedTags: ['b', 'i', 'em', 'strong', 'br']
});
// → '<b>Hello</b> <i>World</i>'
// <script> is removed; <b> and <i> are keptRich Text (Formatting Allowed)
const comment = '<p>Great post! <a href="javascript:steal()">Click</a></p>';
const safe = AsyncHelpers.sanitize(comment, {
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'li'],
removeScripts: true,
removeEvents: true
});
// → '<p>Great post! </p>'
// javascript: href is removed along with the link textRemove Inline Styles
const styled = '<div style="background:url(javascript:...)">...</div>';
const safe = AsyncHelpers.sanitize(styled, {
allowedTags: ['div'],
removeStyles: true
});
// → '<div>...</div>'Form Integration — Sanitize Fields In-Place
When the Form module is loaded, forms get sanitizeField and sanitizeAll methods:
// Sanitize a single field value in-place
Forms.commentForm.sanitizeField('content', {
allowedTags: ['b', 'i', 'em', 'strong']
});
// The field's value is replaced with the sanitized version
// Sanitize all text/textarea fields in the form
Forms.profileForm.sanitizeAll();
// Every text input and textarea gets sanitized
// Sanitize with custom options
Forms.postForm.sanitizeAll({
allowedTags: ['b', 'i', 'p', 'br'],
removeScripts: true,
removeEvents: true
});When to use sanitizeAll: Before displaying form values in the DOM, especially for content that users can see (comments, profile bios, post bodies).
What sanitize Does NOT Do
sanitize is a first-line defense for common XSS patterns. It handles:
<script>tagsjavascript:URLs- Inline event handlers (
onclick,onerror, etc.)
For a very permissive allowedTags list or defense-in-depth security, consider a dedicated HTML sanitization library like DOMPurify for production security-critical applications.
Summary
asyncHandler
- Wraps an async event listener to auto-manage loading CSS class and data attribute
- Loading state is always removed in
finally— never gets stuck - Options:
loadingClass,loadingAttribute,errorHandler
sleep
await AsyncHelpers.sleep(ms)— pauses async execution formsmilliseconds- Use it for delays between steps, minimum display times, or animation pauses
sanitize
AsyncHelpers.sanitize(input, opts)— removes XSS attack vectors from strings- Default: entity-encode all HTML (safest, no HTML rendered)
allowedTags— permit specific tags while stripping the rest- Form integration:
Forms.myForm.sanitizeField(name)/sanitizeAll()