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:
juanatsap
2025-12-02 08:33:18 +00:00
parent 2d3d3de8cd
commit db642c7cc2
+261 -1
View File
@@ -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