├── .DS_Store ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── hx-optimistic.d.ts ├── hx-optimistic.js ├── hx-optimistic.min.js ├── package-lock.json ├── package.json ├── playwright-report └── index.html ├── src ├── config.js ├── constants.js ├── extension.js ├── index.js └── utils.js ├── test-results └── .last-run.json ├── tests ├── README.md ├── helpers │ ├── setup.js │ └── test-utils.js ├── integration │ ├── basic-updates.test.js │ ├── error-handling.test.js │ └── lifecycle-context.test.js └── unit │ ├── interpolation.test.js │ └── target-resolution.test.js └── vitest.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenseanstewart/hx-optimistic/fe2203fa8bc7b6ae574061a7a11f6ef56dbd7490/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main, development ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Run unit and integration tests 31 | run: npm run test:run 32 | 33 | - name: Generate coverage report 34 | if: matrix.node-version == '20.x' 35 | run: npm run test:coverage 36 | 37 | - name: Upload coverage to Codecov 38 | if: matrix.node-version == '20.x' 39 | uses: codecov/codecov-action@v3 40 | with: 41 | file: ./coverage/lcov.info 42 | flags: unittests 43 | name: codecov-umbrella 44 | fail_ci_if_error: false 45 | 46 | 47 | 48 | lint: 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - uses: actions/checkout@v3 53 | 54 | - name: Use Node.js 55 | uses: actions/setup-node@v3 56 | with: 57 | node-version: '20.x' 58 | cache: 'npm' 59 | 60 | - name: Install dependencies 61 | run: npm ci 62 | 63 | - name: Check code formatting 64 | run: | 65 | # Add linting commands if you have them 66 | echo "Linting checks would go here" 67 | 68 | - name: Type check 69 | run: | 70 | # TypeScript checking if applicable 71 | echo "Type checking would go here" 72 | 73 | build: 74 | runs-on: ubuntu-latest 75 | needs: [test, lint] 76 | 77 | steps: 78 | - uses: actions/checkout@v3 79 | 80 | - name: Use Node.js 81 | uses: actions/setup-node@v3 82 | with: 83 | node-version: '20.x' 84 | cache: 'npm' 85 | 86 | - name: Install dependencies 87 | run: npm ci 88 | 89 | - name: Build minified version 90 | run: npm run build 91 | 92 | - name: Check build output 93 | run: | 94 | test -f hx-optimistic.min.js 95 | 96 | - name: Upload build artifacts 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: build-output 100 | path: | 101 | hx-optimistic.js 102 | hx-optimistic.min.js 103 | 104 | publish: 105 | runs-on: ubuntu-latest 106 | needs: [build] 107 | if: startsWith(github.ref, 'refs/tags/v') 108 | steps: 109 | - uses: actions/checkout@v3 110 | - name: Use Node.js 111 | uses: actions/setup-node@v3 112 | with: 113 | node-version: '20.x' 114 | cache: 'npm' 115 | registry-url: 'https://registry.npmjs.org' 116 | - name: Install dependencies 117 | run: npm ci 118 | - name: Build 119 | run: npm run build 120 | - name: Publish to npm 121 | env: 122 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 123 | run: npm publish --access public 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .astro/ 4 | coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HTMX Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hx-optimistic 2 | 3 | An htmx extension for optimistic UI updates with automatic rollback on errors. Combine it with speculation rules (or the htmx preload extension) and the View Transitions API for truly app‑like experience with minimal JavaScript. We love JavaScript, but use it like a spice: a pinch delights, too much overwhelms. 4 | 5 | ## ✨ Features 6 | 7 | - 🎯 **Optimistic Updates** - Immediate UI feedback while requests are processing 8 | - 🔄 **Automatic Rollback** - Intelligent revert to original state on errors 9 | - 📝 **Input Interpolation** - Dynamic templates with `${this.value}`, `${textarea}`, `${data:key}` helpers 10 | - 🎨 **Template Support** - Rich HTML templates for loading and error states 11 | - ⚠️ **Developer Warnings** - Console warnings for unsupported patterns 12 | - 🚫 **No CSS Required** - You control all styling through provided class names 13 | - 📦 **Tiny** - Only **18.2KB** uncompressed, **8.4KB** minified, **3.0KB** gzipped 14 | - 🔧 **Highly Configurable** - Fine-tune behavior per element 15 | 16 | ## 🚀 Quick Start 17 | 18 | ### Installation 19 | 20 | **Via CDN (jsDelivr, pinned major):** 21 | ```html 22 | 23 | 24 | ``` 25 | 26 | Alternative (unpkg, latest v1): 27 | ```html 28 | 29 | 30 | ``` 31 | 32 | Compatibility: Works with `htmx` 1.9+ and 2.x. 33 | 34 | **Via NPM:** 35 | ```bash 36 | npm install hx-optimistic 37 | ``` 38 | 39 | ### Basic Usage 40 | 41 | Enable the extension and add optimistic behavior to any HTMX element: 42 | 43 | ```html 44 | 45 | 46 | 54 | 55 | ``` 56 | 57 | ## 🎯 Core Concepts 58 | 59 | - Use `data-optimistic` to declare behavior per element 60 | - Choose between `values` (simple property changes) and `template` (full HTML) 61 | - Automatic snapshot and revert: `innerHTML`, `className`, attributes, and `dataset` 62 | - Token-based concurrency prevents stale errors from overwriting newer states 63 | - Target resolution via `hx-target` or config `target` chains: `closest`, `find`, `next`, `previous` 64 | - Errors via `errorMessage` or `errorTemplate` with `errorMode` and `delay` 65 | - Interpolation supports safe patterns only; avoid arbitrary JS expressions 66 | 67 | ## 📦 Bundle Size 68 | 69 | | Artifact | Size | 70 | |---------|------| 71 | | Unminified (`hx-optimistic.js`) | 18.2 KB | 72 | | Minified (`hx-optimistic.min.js`) | 8.4 KB | 73 | | Minified + gzip | 3.0 KB | 74 | 75 | ### Values vs Templates 76 | 77 | **Values** - Perfect for simple optimistic updates: 78 | ```html 79 | 94 | ``` 95 | 96 | **Templates** - Ideal for complex optimistic UI changes: 97 | ```html 98 |
You: ${textarea}", 101 | "errorTemplate": "
❌ Comment failed to post
" 102 | }'> 103 | 104 | 105 |
106 | ``` 107 | 108 | ### Input Interpolation 109 | 110 | Dynamic content using `${...}` syntax with powerful helpers: 111 | 112 | ```html 113 |
115 | 116 | 117 |
118 | ``` 119 | 120 | ## 📖 Interpolation Reference 121 | 122 | All `${...}` patterns supported in templates and values: 123 | 124 | | Pattern | Description | Example | 125 | |---------|-------------|---------| 126 | | `${this.value}` | Element's input value | `"Saving: ${this.value}"` | 127 | | `${this.textContent}` | Element's text content | `"Was: ${this.textContent}"` | 128 | | `${this.dataset.key}` | Data attribute via dataset | `"ID: ${this.dataset.userId}"` | 129 | | `${textarea}` | First textarea in form | `"Comment: ${textarea}"` | 130 | | `${email}` | First email input | `"Email: ${email}"` | 131 | | `${data:key}` | Data attribute shorthand | `"Count: ${data:count}"` | 132 | | `${attr:name}` | Any HTML attribute | `"ID: ${attr:id}"` | 133 | | `${contextKey}` | Value from `config.context` (templates only) | `"Hello, ${username}"` | 134 | | `${status}` | HTTP status (errors only) | `"Error ${status}"` | 135 | | `${statusText}` | HTTP status text (errors only) | `"Error: ${statusText}"` | 136 | | `${error}` | Error message (errors only) | `"Failed: ${error}"` | 137 | 138 | **Form Field Helpers:** 139 | - `${textarea}`, `${email}`, `${password}`, `${text}`, `${url}`, `${tel}`, `${search}` 140 | - `${fieldName}` - Any field with `name="fieldName"` 141 | 142 | ## ⚙️ Configuration Options 143 | 144 | Complete configuration reference for `data-optimistic`: 145 | 146 | ### Snapshot Behavior 147 | `innerHTML`, `className`, all attributes, and the element `dataset` are automatically captured and restored on revert; no configuration is required. 148 | 149 | ### Optimistic Updates 150 | ```javascript 151 | { 152 | // Simple property updates 153 | "values": { 154 | "textContent": "Loading...", 155 | "className": "btn loading" 156 | }, 157 | 158 | // Rich HTML templates 159 | "template": "#loading-template", // Or inline HTML 160 | "target": "closest .card", // Different target for optimistic update 161 | "swap": "beforeend", // Append instead of replace 162 | "class": "my-optimistic" // Optional custom class applied during optimistic state 163 | } 164 | ``` 165 | 166 | `swap` supports `beforeend` and `afterbegin`. If omitted, content is replaced. 167 | 168 | ### Error Handling 169 | ```javascript 170 | { 171 | "errorMessage": "Request failed", 172 | "errorTemplate": "
Error ${status}: ${statusText}
", 173 | "errorMode": "append", // "replace" (default) or "append" 174 | "delay": 2000 // Auto-revert delay in ms 175 | } 176 | ``` 177 | 178 | ### Context Data 179 | Provide additional variables for template interpolation: 180 | 181 | ```json 182 | { 183 | "template": "
Hello, ${username}
", 184 | "context": { "username": "Alice" } 185 | } 186 | ``` 187 | 188 | Full example: 189 | 190 | ```html 191 |
192 |

Alex

193 |

Status: 🔴 Offline

194 | 203 |
204 | 205 | 211 | 212 | 215 | ``` 216 | 217 | ## 🎨 CSS Classes 218 | 219 | This library does not include any CSS. These classes are applied so you can style them as you wish: 220 | 221 | - `hx-optimistic`: applied during the optimistic update 222 | - `hx-optimistic-error`: applied when an error is shown 223 | - `hx-optimistic-reverting`: applied while reverting to the snapshot 224 | - `hx-optimistic-error-message`: wrapper added when errorMode is "append" 225 | - `hx-optimistic-pending`: may be applied to `