├── LICENSE ├── README.md └── hx-offline.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Spiro Floropoulos 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | Currently HTMX does not support offline mode. Connectivity is required to process (but not execute) htmx actions on any given page where it is implemented. 4 | 5 | To achieve offline mode, we are going to implement at least a few different features 6 | 7 | * Detect when we're offline 8 | * During offline mode, catch/trap/interject any hx requests going out 9 | * Capture state of request 10 | * - Element source (which element triggered the request) 11 | - Parameters of request 12 | - Success / fail operations of request 13 | * Store request (maybe using localStorage OR sqlocal?) 14 | * Wait for connectivity 15 | * On connectivity, ship requests either in queue or async 16 | * Handle results of requests appropriately 17 | 18 | ## Technologies Required 19 | 20 | * Local storage (so localStorage OR sqlocal?) 21 | * HTMX 22 | * WebWorkers (y though?) 23 | * - (link to review)[https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Offline_Service_workers] 24 | 25 | ## Where does WASM fit in? 26 | 27 | WASM might, potentially, serve as back-end router to ship back offline responses when htmx continues to make requests. 28 | 29 | However, this might not be necessary if we have some light front-end driven interactions while offline. 30 | 31 | [Work in Progress] 32 | 33 | ### Dumping Ground 34 | 35 | * Possibility to use replicache. Lean on localStorage and then a background (worker?) process deals with CRDTs. Ultimately you'd end up patching the DB... 36 | * Optimistic updates 37 | * To capture htmx requests 38 | * - https://github.com/bigskysoftware/htmx/blob/master/src/htmx.js#L2217 39 | - Possibly do something like document.addEventListener('htmx:beforeRequest') -> get event data somehow 40 | 41 | ### TODO 42 | 43 | - [x] How do we capture HTMX requests (outgoing)? 44 | - [x] Can we tell if we're online/offline 45 | - [x] How do we convert capture HTMX requests to data we can store? 46 | - [x] Where do we store said data (refer to Dumping Ground for awesome Chris ideas) 47 | - [x] How do we store required success/fail operations when we finally ship the requests? 48 | - [ ] How do we deal with requests interactivity while offline? 49 | - [x] How do we fire off requests from DB once we go back online? 50 | - [x] Add optional "ship to server" parameter - ALREADY DONE WITH "ignore:extension_name" via HTMX 51 | - [ ] Dedupe or ignore duplicate requests 52 | -------------------------------------------------------------------------------- /hx-offline.js: -------------------------------------------------------------------------------- 1 | let QUEUE = localStorage.getItem('htmx-offline-queue'); 2 | if (QUEUE === null) { 3 | QUEUE = []; 4 | localStorage.setItem('htmx-offline-queue', []); 5 | } 6 | 7 | htmx.defineExtension('hx-offline', { 8 | onEvent: function (name, evt) { 9 | // console.log('Fired event', {name, evt}); 10 | if (name === 'htmx:beforeRequest') { 11 | if (!navigator.onLine) { 12 | QUEUE.push({trigger: generateTriggerSpecs(evt.srcElement), element: evt.srcElement}); 13 | localStorage.setItem('htmx-offline-queue', QUEUE); 14 | } 15 | } 16 | } 17 | }); 18 | 19 | function matches(elt, selector) { 20 | let matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; 21 | return matchesFunction && matchesFunction.call(elt, selector); 22 | } 23 | 24 | let INPUT_SELECTOR = 'input, textarea, select'; 25 | function generateTriggerSpecs(elt) { 26 | let explicit_trigger = elt.getAttribute('hx-trigger'); 27 | if (explicit_trigger) { 28 | return explicit_trigger; 29 | } 30 | 31 | let trigger_specs = []; 32 | if (trigger_specs.length > 0) { 33 | return trigger_specs; 34 | } else if (matches(elt, 'form')) { 35 | return 'submit'; 36 | } else if (matches(elt, 'input[type="button"], input[type="submit"]')){ 37 | return 'click'; 38 | } else if (matches(elt, INPUT_SELECTOR)) { 39 | return 'change'; 40 | } else { 41 | return 'click'; 42 | } 43 | } 44 | 45 | window.addEventListener('online', function (evt) { 46 | console.log('Online'); 47 | console.log(evt); 48 | console.log(navigator.onLine); 49 | 50 | if (navigator.onLine) { 51 | // Release the Kraken 52 | for (let i = 0; i < QUEUE.length; ++i) { 53 | // execute QUEUE[i].trigger via QUEUE[i].elt 54 | // htmx.trigger(trigger, elt, return) 55 | htmx.trigger(QUEUE[i].element, QUEUE[i].trigger, function (e) { 56 | console.log('QUEUE TRIGGER', e); 57 | }); 58 | } 59 | QUEUE = []; 60 | localStorage.setItem('htmx-offline-queue', []); 61 | } 62 | }); 63 | window.addEventListener('offline', function (evt) { 64 | console.log('Offline'); 65 | console.log(evt); 66 | console.log(navigator.onLine); 67 | }); 68 | --------------------------------------------------------------------------------