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)
|
4. [Toggle Patterns](#toggle-patterns)
|
||||||
5. [Contact Form Pattern](#contact-form-pattern)
|
5. [Contact Form Pattern](#contact-form-pattern)
|
||||||
6. [Skeleton Loaders](#skeleton-loaders)
|
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
|
## Common Attributes Reference
|
||||||
|
|
||||||
### Request Attributes
|
### Request Attributes
|
||||||
|
|||||||
Reference in New Issue
Block a user