docs: Add HTML Invoker Commands and Lazy Loading sections
- Document HTML Invoker Commands API (commandfor/command) - Document lazy loading pattern for web components - Include CDN comparison (esm.sh ?bundle vs others) - Add CSP configuration notes - Performance metrics before/after
This commit is contained in:
+261
-1
@@ -14,7 +14,9 @@ This document explains HTMX patterns used in this CV website project, with pract
|
||||
4. [Toggle Patterns](#toggle-patterns)
|
||||
5. [Contact Form Pattern](#contact-form-pattern)
|
||||
6. [Skeleton Loaders](#skeleton-loaders)
|
||||
7. [Common Attributes Reference](#common-attributes-reference)
|
||||
7. [HTML Invoker Commands API](#html-invoker-commands-api)
|
||||
8. [Lazy Loading Web Components](#lazy-loading-web-components)
|
||||
9. [Common Attributes Reference](#common-attributes-reference)
|
||||
|
||||
---
|
||||
|
||||
@@ -378,6 +380,264 @@ _="on htmx:afterSettle wait 100ms then remove .loading from me"
|
||||
|
||||
---
|
||||
|
||||
## HTML Invoker Commands API
|
||||
|
||||
**Browser Support**: Chrome/Edge 135+, Firefox Nightly, Safari TP
|
||||
|
||||
### The Problem
|
||||
|
||||
Opening and closing `<dialog>` elements traditionally requires JavaScript:
|
||||
|
||||
```html
|
||||
<!-- Old way - onclick handlers everywhere -->
|
||||
<button onclick="document.getElementById('my-modal').showModal()">
|
||||
Open Modal
|
||||
</button>
|
||||
|
||||
<dialog id="my-modal">
|
||||
<button onclick="document.getElementById('my-modal').close()">
|
||||
Close
|
||||
</button>
|
||||
</dialog>
|
||||
```
|
||||
|
||||
This is verbose, error-prone, and mixes behavior with markup.
|
||||
|
||||
### The Solution: `commandfor` + `command`
|
||||
|
||||
The new HTML Invoker Commands API provides declarative modal control:
|
||||
|
||||
```html
|
||||
<!-- New way - pure HTML attributes -->
|
||||
<button commandfor="my-modal" command="show-modal">
|
||||
Open Modal
|
||||
</button>
|
||||
|
||||
<dialog id="my-modal">
|
||||
<button commandfor="my-modal" command="close">
|
||||
Close
|
||||
</button>
|
||||
</dialog>
|
||||
```
|
||||
|
||||
### Command Values
|
||||
|
||||
| Command | Effect | Target Element |
|
||||
|---------|--------|----------------|
|
||||
| `show-modal` | Opens dialog as modal | `<dialog>` |
|
||||
| `close` | Closes dialog | `<dialog>` |
|
||||
| `show-popover` | Shows popover | `[popover]` |
|
||||
| `hide-popover` | Hides popover | `[popover]` |
|
||||
| `toggle-popover` | Toggles popover | `[popover]` |
|
||||
|
||||
### Project Implementation
|
||||
|
||||
**Files**: `templates/partials/widgets/*.html`, `templates/partials/modals/*.html`
|
||||
|
||||
```html
|
||||
<!-- Info button opens info modal -->
|
||||
<button id="info-button"
|
||||
commandfor="info-modal"
|
||||
command="show-modal"
|
||||
aria-label="Show information">
|
||||
<iconify-icon icon="mdi:information-outline"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<!-- Modal with close button -->
|
||||
<dialog id="info-modal" class="info-modal">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close"
|
||||
commandfor="info-modal"
|
||||
command="close"
|
||||
aria-label="Close">
|
||||
<iconify-icon icon="mdi:close"></iconify-icon>
|
||||
</button>
|
||||
<!-- Modal content -->
|
||||
</div>
|
||||
</dialog>
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **No JavaScript** - Pure HTML declarative syntax
|
||||
2. **Accessibility** - Built-in keyboard and screen reader support
|
||||
3. **Reduced Errors** - No typos in element IDs within JavaScript
|
||||
4. **Cleaner Templates** - Removes onclick clutter
|
||||
5. **Progressive Enhancement** - Graceful degradation in older browsers
|
||||
|
||||
### Fallback for Older Browsers
|
||||
|
||||
If you need to support browsers without Invoker Commands:
|
||||
|
||||
```html
|
||||
<button commandfor="my-modal"
|
||||
command="show-modal"
|
||||
onclick="this.commandfor || document.getElementById('my-modal').showModal()">
|
||||
Open Modal
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lazy Loading Web Components
|
||||
|
||||
### The Problem
|
||||
|
||||
Heavy web components (like ninja-keys command palette) add significant initial load time even when users may never use them:
|
||||
|
||||
```
|
||||
Initial Load: 81 module requests, ~300KB, 2+ seconds
|
||||
```
|
||||
|
||||
### The Solution: Dynamic Import on Demand
|
||||
|
||||
Only load the component when the user actually needs it:
|
||||
|
||||
```javascript
|
||||
// Don't import at top of file
|
||||
// import 'ninja-keys'; // ❌ Loads immediately
|
||||
|
||||
// Instead, lazy load on first use
|
||||
let loaded = false;
|
||||
|
||||
async function loadNinjaKeys() {
|
||||
if (loaded) return;
|
||||
|
||||
// Dynamic import - only fetches when called
|
||||
await import('https://esm.sh/ninja-keys@1.2.2?bundle');
|
||||
|
||||
// Create element after module loads
|
||||
const container = document.getElementById('cmd-k-container');
|
||||
const ninjaKeys = document.createElement('ninja-keys');
|
||||
ninjaKeys.id = 'cmd-k-bar';
|
||||
container.appendChild(ninjaKeys);
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
// Trigger on user action
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
loadNinjaKeys();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Project Implementation
|
||||
|
||||
**File**: `templates/partials/layout/body-scripts.html`
|
||||
|
||||
```html
|
||||
<!-- Placeholder container (always present, empty) -->
|
||||
<div id="cmd-k-container"></div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let ninjaLoaded = false;
|
||||
let ninjaLoading = false;
|
||||
|
||||
async function loadNinjaKeys() {
|
||||
if (ninjaLoaded || ninjaLoading) return;
|
||||
ninjaLoading = true;
|
||||
|
||||
// Use esm.sh with ?bundle for single-file delivery
|
||||
await import('https://esm.sh/ninja-keys@1.2.2?bundle');
|
||||
|
||||
// Create element
|
||||
const container = document.getElementById('cmd-k-container');
|
||||
const ninjaKeys = document.createElement('ninja-keys');
|
||||
ninjaKeys.id = 'cmd-k-bar';
|
||||
ninjaKeys.placeholder = 'Type a command or search...';
|
||||
ninjaKeys.hideBreadcrumbs = true;
|
||||
container.appendChild(ninjaKeys);
|
||||
|
||||
// Load initialization script
|
||||
const script = document.createElement('script');
|
||||
script.src = '/static/js/ninja-keys-init.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
ninjaLoaded = true;
|
||||
ninjaLoading = false;
|
||||
|
||||
// Open after brief initialization delay
|
||||
setTimeout(() => ninjaKeys.open(), 100);
|
||||
}
|
||||
|
||||
function openNinjaKeys() {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
if (nk && typeof nk.open === 'function') {
|
||||
nk.open();
|
||||
}
|
||||
}
|
||||
|
||||
// CMD+K / Ctrl+K keyboard shortcut
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
ninjaLoaded ? openNinjaKeys() : loadNinjaKeys();
|
||||
}
|
||||
});
|
||||
|
||||
// Button click trigger
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#cmd-k-button, .cmd-k-trigger')) {
|
||||
e.preventDefault();
|
||||
ninjaLoaded ? openNinjaKeys() : loadNinjaKeys();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
### CDN Choice: esm.sh with ?bundle
|
||||
|
||||
| CDN | Requests | Why |
|
||||
|-----|----------|-----|
|
||||
| unpkg.com | 80+ (redirect chains) | ❌ Follows all peer deps |
|
||||
| esm.sh | 80+ (without bundle) | ❌ Resolves all imports |
|
||||
| esm.sh?bundle | 2-3 | ✅ Pre-bundled single file |
|
||||
| jsdelivr | 1 | ✅ Also good option |
|
||||
|
||||
```javascript
|
||||
// ❌ Triggers 80+ module requests
|
||||
await import('https://esm.sh/ninja-keys@1.2.2');
|
||||
|
||||
// ✅ Single bundled file
|
||||
await import('https://esm.sh/ninja-keys@1.2.2?bundle');
|
||||
```
|
||||
|
||||
### CSP Configuration
|
||||
|
||||
Remember to add your CDN to Content Security Policy:
|
||||
|
||||
```go
|
||||
// internal/middleware/security.go
|
||||
csp := "default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' https://esm.sh https://cdn.jsdelivr.net; " +
|
||||
// ...
|
||||
```
|
||||
|
||||
### Performance Results
|
||||
|
||||
| Metric | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Initial requests | 81 | 0 |
|
||||
| Initial load time | +2.1s | 0ms |
|
||||
| On CMD+K | 0 | 3 requests |
|
||||
| Subsequent uses | 0 | 0 (cached) |
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use Placeholder Containers** - Empty div ready for component injection
|
||||
2. **Prevent Double Loading** - Track loading state with flags
|
||||
3. **Bundle Dependencies** - Use `?bundle` parameter on esm.sh
|
||||
4. **Cache First Load** - Browser caches subsequent uses automatically
|
||||
5. **Multiple Triggers** - Support keyboard AND button triggers
|
||||
6. **Initialization Delay** - Wait briefly after element creation for setup
|
||||
|
||||
---
|
||||
|
||||
## Common Attributes Reference
|
||||
|
||||
### Request Attributes
|
||||
|
||||
Reference in New Issue
Block a user