Tools for debugging htmx behavior, utility functions for common operations, and development helpers for building htmx applications.
Control htmx's built-in logging system for debugging and development.
/**
* Enables logging of all htmx events (useful for debugging)
*/
function logAll(): void;
/**
* Disables htmx event logging
*/
function logNone(): void;
/**
* Custom logger for htmx events
* Set to null to use console.log, or provide custom logging function
*/
let logger: any;Usage Examples:
// Enable all logging
htmx.logAll();
// Disable logging
htmx.logNone();
// Custom logger implementation
htmx.logger = function(elt, event, data) {
// Custom logging format
console.group(`🔧 HTMX: ${event}`);
console.log('Element:', elt);
console.log('Data:', data);
console.groupEnd();
// Send to analytics service
if (window.analytics && event.startsWith('htmx:')) {
analytics.track('htmx_event', {
event: event,
element: elt.tagName,
timestamp: Date.now()
});
}
};
// Conditional logging based on environment
if (process.env.NODE_ENV === 'development') {
htmx.logAll();
} else {
htmx.logNone();
}
// Log only specific events
const originalLogger = htmx.logger;
htmx.logger = function(elt, event, data) {
// Only log errors and important events
if (event.includes('Error') || event.includes('failed') || event === 'htmx:responseError') {
originalLogger.call(this, elt, event, data);
}
};Helper functions for common operations and data processing.
/**
* Parses an interval string consistent with htmx timing specifications
* @param str - Timing string (e.g., "1s", "500ms", "2.5s")
* @returns Number in milliseconds or undefined if invalid
*/
function parseInterval(str: string): number | undefined;Usage Examples:
// Parse timing strings
const delay1 = htmx.parseInterval('1s'); // 1000
const delay2 = htmx.parseInterval('500ms'); // 500
const delay3 = htmx.parseInterval('2.5s'); // 2500
const delay4 = htmx.parseInterval('invalid'); // undefined
// Use in dynamic timing
function setDynamicDelay(element, delayString) {
const delay = htmx.parseInterval(delayString);
if (delay !== undefined) {
element.style.animationDelay = `${delay}ms`;
}
}
// Parse timing from HTML attributes
document.querySelectorAll('[data-delay]').forEach(el => {
const delayString = el.getAttribute('data-delay');
const delay = htmx.parseInterval(delayString);
if (delay !== undefined) {
setTimeout(() => {
el.classList.add('delayed-action');
}, delay);
}
});
// Validate timing configurations
function validateTimingConfig(config) {
const validatedConfig = {};
for (const [key, value] of Object.entries(config)) {
if (typeof value === 'string') {
const parsed = htmx.parseInterval(value);
validatedConfig[key] = parsed !== undefined ? parsed : 0;
} else {
validatedConfig[key] = value;
}
}
return validatedConfig;
}
const config = validateTimingConfig({
swapDelay: '100ms',
settleDelay: '20ms',
timeout: '30s'
});Access to htmx version information for compatibility checks and debugging.
/**
* Current htmx version string
*/
const version: string; // "2.0.6"Usage Examples:
// Check htmx version
console.log('HTMX Version:', htmx.version);
// Version comparison utility
function compareVersions(version1, version2) {
const v1parts = version1.split('.').map(Number);
const v2parts = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const v1part = v1parts[i] || 0;
const v2part = v2parts[i] || 0;
if (v1part > v2part) return 1;
if (v1part < v2part) return -1;
}
return 0;
}
// Feature detection based on version
function supportsFeature(feature) {
const currentVersion = htmx.version;
const featureVersions = {
'view-transitions': '2.0.0',
'websockets': '1.6.0',
'extensions': '1.0.0'
};
const requiredVersion = featureVersions[feature];
if (!requiredVersion) return false;
return compareVersions(currentVersion, requiredVersion) >= 0;
}
// Conditional feature usage
if (supportsFeature('view-transitions')) {
htmx.config.globalViewTransitions = true;
}
// Report version info for support
function getDebugInfo() {
return {
htmxVersion: htmx.version,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
config: htmx.config
};
}Access to location-related utilities for navigation and URL handling.
/**
* Proxy of window.location used for page reload functions
*/
const location: Location;Usage Examples:
// Access current location
console.log('Current URL:', htmx.location.href);
console.log('Current path:', htmx.location.pathname);
// Navigation utilities
function navigateTo(url) {
htmx.location.href = url;
}
function reloadPage() {
htmx.location.reload();
}
// URL manipulation
function updateURL(path, replaceState = false) {
if (replaceState) {
history.replaceState(null, '', path);
} else {
history.pushState(null, '', path);
}
}
// Check if URL is same origin
function isSameOrigin(url) {
try {
const urlObj = new URL(url, htmx.location.origin);
return urlObj.origin === htmx.location.origin;
} catch (e) {
return false;
}
}Create a debug panel for development and testing.
// Create debug panel
function createDebugPanel() {
const panel = document.createElement('div');
panel.id = 'htmx-debug-panel';
panel.style.cssText = `
position: fixed;
bottom: 10px;
right: 10px;
width: 300px;
max-height: 400px;
background: #333;
color: #fff;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
overflow-y: auto;
z-index: 10000;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
`;
const header = document.createElement('h3');
header.textContent = `HTMX Debug (v${htmx.version})`;
header.style.margin = '0 0 10px 0';
panel.appendChild(header);
const logContainer = document.createElement('div');
logContainer.id = 'htmx-debug-log';
panel.appendChild(logContainer);
const controls = document.createElement('div');
controls.style.marginTop = '10px';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.onclick = () => logContainer.innerHTML = '';
controls.appendChild(clearBtn);
const toggleBtn = document.createElement('button');
toggleBtn.textContent = 'Toggle Logging';
toggleBtn.onclick = toggleLogging;
controls.appendChild(toggleBtn);
panel.appendChild(controls);
document.body.appendChild(panel);
return { panel, logContainer };
}
let debugPanel = null;
let loggingEnabled = false;
function toggleLogging() {
if (loggingEnabled) {
htmx.logNone();
loggingEnabled = false;
} else {
htmx.logAll();
loggingEnabled = true;
}
}
// Initialize debug panel
if (process.env.NODE_ENV === 'development') {
document.addEventListener('DOMContentLoaded', () => {
debugPanel = createDebugPanel();
// Custom logger for debug panel
const originalLogger = console.log;
htmx.logger = function(elt, event, data) {
if (debugPanel) {
const logEntry = document.createElement('div');
logEntry.style.borderBottom = '1px solid #555';
logEntry.style.padding = '5px 0';
logEntry.innerHTML = `
<strong>${event}</strong><br>
Element: ${elt.tagName}${elt.id ? '#' + elt.id : ''}<br>
Time: ${new Date().toLocaleTimeString()}
`;
debugPanel.logContainer.appendChild(logEntry);
debugPanel.logContainer.scrollTop = debugPanel.logContainer.scrollHeight;
}
originalLogger.call(console, 'HTMX:', event, elt, data);
};
});
}Monitor htmx performance and request timing.
// Performance monitoring
const HtmxPerformanceMonitor = {
requests: [],
startTimes: new Map(),
init() {
htmx.on('htmx:beforeRequest', (evt) => {
const requestId = this.generateRequestId(evt);
this.startTimes.set(requestId, performance.now());
});
htmx.on('htmx:afterRequest', (evt) => {
const requestId = this.generateRequestId(evt);
const startTime = this.startTimes.get(requestId);
if (startTime) {
const duration = performance.now() - startTime;
this.recordRequest({
url: evt.detail.requestConfig.path,
method: evt.detail.requestConfig.verb,
duration: duration,
status: evt.detail.xhr.status,
size: evt.detail.xhr.responseText.length,
timestamp: Date.now(),
successful: evt.detail.successful
});
this.startTimes.delete(requestId);
}
});
},
generateRequestId(evt) {
return `${evt.detail.requestConfig.verb}-${evt.detail.requestConfig.path}-${Date.now()}`;
},
recordRequest(requestInfo) {
this.requests.push(requestInfo);
// Keep only last 100 requests
if (this.requests.length > 100) {
this.requests.shift();
}
// Log slow requests
if (requestInfo.duration > 2000) {
console.warn('Slow HTMX request:', requestInfo);
}
},
getStats() {
if (this.requests.length === 0) return null;
const durations = this.requests.map(r => r.duration);
const sizes = this.requests.map(r => r.size);
return {
totalRequests: this.requests.length,
averageDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
maxDuration: Math.max(...durations),
minDuration: Math.min(...durations),
averageSize: sizes.reduce((a, b) => a + b, 0) / sizes.length,
errorRate: this.requests.filter(r => !r.successful).length / this.requests.length,
slowRequests: this.requests.filter(r => r.duration > 2000).length
};
},
exportData() {
return {
stats: this.getStats(),
requests: this.requests,
config: htmx.config,
version: htmx.version,
timestamp: new Date().toISOString()
};
}
};
// Initialize monitoring in development
if (process.env.NODE_ENV === 'development') {
HtmxPerformanceMonitor.init();
// Global access for debugging
window.htmxStats = HtmxPerformanceMonitor;
}Utilities for testing htmx applications.
// Testing utilities
const HtmxTestUtils = {
// Wait for htmx requests to complete
async waitForRequests(timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
// Check if any requests are in flight
const activeRequests = document.querySelectorAll('.htmx-request');
if (activeRequests.length === 0) {
return true;
}
await new Promise(resolve => setTimeout(resolve, 10));
}
throw new Error('Timeout waiting for HTMX requests to complete');
},
// Trigger htmx event and wait for completion
async triggerAndWait(element, event, detail = {}) {
const promise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for htmx event'));
}, 5000);
const handler = () => {
clearTimeout(timeout);
htmx.off('htmx:afterRequest', handler);
resolve();
};
htmx.on('htmx:afterRequest', handler);
});
htmx.trigger(element, event, detail);
return promise;
},
// Mock htmx responses for testing
mockResponse(url, responseText, status = 200) {
const originalAjax = htmx.ajax;
htmx.ajax = function(method, path, context) {
if (path === url) {
// Create mock response
setTimeout(() => {
const mockEvent = new CustomEvent('htmx:afterRequest', {
detail: {
xhr: {
status: status,
responseText: responseText,
getResponseHeader: () => 'text/html'
},
successful: status < 400,
target: typeof context === 'string' ? document.querySelector(context) : context
}
});
document.dispatchEvent(mockEvent);
}, 10);
return Promise.resolve();
}
return originalAjax.call(this, method, path, context);
};
// Return cleanup function
return () => {
htmx.ajax = originalAjax;
};
},
// Get current htmx state
getState() {
return {
activeRequests: document.querySelectorAll('.htmx-request').length,
indicators: document.querySelectorAll('.htmx-indicator').length,
config: htmx.config,
version: htmx.version
};
}
};
// Example test usage
/*
// In Jest or similar testing framework
test('htmx request updates content', async () => {
const cleanup = HtmxTestUtils.mockResponse('/api/data', '<div>New Content</div>');
const button = document.querySelector('#load-button');
await HtmxTestUtils.triggerAndWait(button, 'click');
expect(document.querySelector('#content').textContent).toBe('New Content');
cleanup();
});
*/Helpful configurations for development environments.
// Development-specific configuration
function setupDevelopmentEnvironment() {
// Enable comprehensive logging
htmx.logAll();
// Add request timing
htmx.on('htmx:beforeRequest', (evt) => {
evt.detail.requestStart = performance.now();
});
htmx.on('htmx:afterRequest', (evt) => {
if (evt.detail.requestStart) {
const duration = performance.now() - evt.detail.requestStart;
console.log(`Request to ${evt.detail.requestConfig.path} took ${duration.toFixed(2)}ms`);
}
});
// Highlight htmx elements
const style = document.createElement('style');
style.textContent = `
[hx-get], [hx-post], [hx-put], [hx-patch], [hx-delete] {
outline: 1px dashed #007bff !important;
outline-offset: 2px !important;
}
.htmx-request {
background-color: rgba(255, 193, 7, 0.2) !important;
}
.htmx-indicator {
border: 1px solid #dc3545 !important;
}
`;
document.head.appendChild(style);
// Global error handler
window.addEventListener('error', (evt) => {
console.error('Global error:', evt.error);
});
// Expose utilities globally
window.htmxTestUtils = HtmxTestUtils;
console.log('🔧 HTMX development environment initialized');
}
// Auto-initialize in development
if (process.env.NODE_ENV === 'development' ||
window.location.hostname === 'localhost' ||
window.location.hostname.endsWith('.local')) {
document.addEventListener('DOMContentLoaded', setupDevelopmentEnvironment);
}