├── LICENSE ├── README.md └── htmx-load.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Make Startups, Inc. 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 | # htmx-load 2 | Handling third party libraries inside an htmx application can be challenging as 3 | you attempt to manage the initialization of each lib. 4 | 5 | This repo is an MIT-licensed agnostic framework for managing resource 6 | loading in a view/URL-specific method. It's more of a recipe than a library 7 | and you're encouraged to adapt or modify bits and pieces for your use case. 8 | 9 | ## Usage 10 | This library is designed to work nicely with https://htmx.org. 11 | ```html 12 | 13 | 14 | 15 | 33 | ``` 34 | ## htmx partial-page specific handlers 35 | Assuming you have an htmx project that has specific partial html snippets 36 | loaded in your pages, you can use the htmx-load functionality from those 37 | pages as your code is loaded. 38 | ### Output of `/blog/23/edit` 39 | ```html 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 55 | ``` 56 | ## Overview 57 | This "library" was designed to allow you to have the tools to easily solve 58 | common collisions and problems using third-party javascript libraries in your 59 | htmx applications. This library was not designed to solve all of your problems 60 | as many of those will be unique to you and your specific needs. 61 | 62 | Please feel free to modify this library to work with your specific use-case. 63 | 64 | If you have any questions, please create a GitHub Issue or message 65 | [Eric Harrison on X](https://x.com/blister). 66 | -------------------------------------------------------------------------------- /htmx-load.js: -------------------------------------------------------------------------------- 1 | // htmx-load.js, MIT Copyright 2024 Make Startups, Inc and Eric Harrison 2 | // 3 | // See README.md for usage examples and recipes. 4 | 5 | // HtmxLoad is a top-level object to make it easier to track and manage 6 | // lazy-loaded and view-specific scripts inside an htmx-powered web 7 | // application. 8 | // 9 | // As an example, if an htmx partial contains a script tag in 10 | // the return, this code will be re-executed when a page is loaded a second 11 | // time and will throw errors like `Error: const varname cannot be redefined.`. 12 | // 13 | // To fix this type of error, the HtmxLoad library provides one solution 14 | // for maanaging JS resources and library usage inside an htmx-style app. 15 | const HtmxLoad = { 16 | 'DEBUG': false, // debug mode, when on, logs to console. 17 | 18 | // scripts is a storage object to track view-specific JS files have 19 | // been executed. We can use this object to see if a specific JS file has 20 | // already been added to the global context and potentially skip it 21 | // on future page loads. 22 | 'scripts': {}, 23 | 24 | // The current window.location.pathname will be stored here and will 25 | // serve as a key for determining which code to execute. 26 | 'current_route': null, 27 | 28 | // storage location for view specific load callbacks. 29 | 'route_handlers': {}, 30 | 31 | // The 'data' object will provide view-specific storage and will persist 32 | // across page loads over multiple pages. 33 | // 34 | // https://server.com/view_name/whatever = HtmxLoad['view_name'] = {}; 35 | 'data': {}, 36 | 37 | // TEMPORARY view specific storage. Completely wiped out any time 38 | // `current_route` is changed. 39 | 'temp': {}, 40 | 41 | // function and callback storage. 42 | '__registered': {}, 43 | }; 44 | 45 | /** This function allows you to define a function that will be called every time 46 | * a view is loaded via the route parameter. 47 | * 48 | * WARNING: YOU are responsible for making sure that your callback function can 49 | * survive being called multiple times in a single session. 50 | * 51 | * @param {string} route The route argument specifies what pages will run 52 | * this handler on load. 53 | * @param {jsViewHandler} callback A function that will be called when a page 54 | * is loaded for the first time. You are responsible for making sure that your 55 | * callback function can handle being called multiple times in a single session. 56 | * 57 | * @returns {number} handlers_defined A count of all the registered handlers for 58 | * this route. 59 | */ 60 | HtmxLoad.register = function(route, callback) { 61 | // strip slashes on the route before using it as a key 62 | route = route.replace(/^\//, '').replace(/\/$/, ''); 63 | 64 | if ( ! (route in HtmxLoad.route_handlers) ) { 65 | HtmxLoad.route_handlers[route] = []; 66 | HtmxLoad.__registered[route] = []; 67 | } 68 | 69 | // Convert the entire callback to a string and store it in this route. 70 | // Prevent duplicate functions from being registered. 71 | const cb_string = callback.toString(); 72 | if ( ! HtmxLoad.__registered[route].includes(cb_string) ) { 73 | HtmxLoad.DEBUG && console.log(`${route} callback added for the first time.`); 74 | HtmxLoad.route_handlers[route].push(callback); 75 | HtmxLoad.__registered[route].push(cb_string); 76 | } else { 77 | HtmxLoad.DEBUG && console.warn(`${route} callback has already been registered.`); 78 | } 79 | 80 | return HtmxLoad.route_handlers[route].length; 81 | }; 82 | 83 | /** This function takes a route as it's only parameter and then runs: 84 | * 1. Any "global" route handler that has been defined for all pages. 85 | * 2. All route handler functions specific to this route. 86 | * 87 | * @param {string} route A URL path (after hostname). Ex: `/blog/34/Cool-Title` 88 | * 1. Run all route handlers defined for `blog` 89 | * 2. Run all route handlers defined for `blog/3` 90 | * 3. Run all route handlers defined for `blog/3/Cool-Title` 91 | * 92 | * @param {Event} ev The event object from whichever eventListener 93 | * function that called HtmxLoad.run 94 | */ 95 | HtmxLoad.run = function(route, ev) { 96 | HtmxLoad.DEBUG && console.log(`RUNNING route handlers for ("${route}")`); 97 | 98 | if ( ! route ) { 99 | route = ''; 100 | } 101 | 102 | if ( route !== HtmxLoad.current_route ) { 103 | HtmxLoad.current_route = route; 104 | 105 | // If your application has pages with a lot of vertical scrolling, 106 | // you may want to uncomment the timeout function below to smoothely 107 | // scroll to the top of the page whenever your user is attempting to 108 | // go to a new URL. 109 | /* 110 | setTimeout(function() { 111 | window.scrollTo({ top: 0, behavior: 'instant'}); 112 | }, 50); 113 | */ 114 | } else { 115 | // In our application, we choose to rerun callback functions 116 | // if the user clicks a link that would reload the page. 117 | // 118 | // You may want to have a different mechanism, so returning from this 119 | // function here may be more appropriate for your use-case. 120 | HtmxLoad.DEBUG && console.log('Duplicate route execution "${route}"'); 121 | // return true; 122 | } 123 | 124 | // Clear out any temp data. 125 | delete HtmxLoad.temp; 126 | HtmxLoad.temp = {}; 127 | 128 | route = route.replace(/^\//, ''); // remove leading slash 129 | 130 | // get the first URL path part and use that to generate view-specific 131 | // storage. 132 | const view_name = route.split('/')[0]; 133 | if ( ! (view_name in HtmxLoad.data) ) { 134 | HtmxLoad.data[view_name] = {}; 135 | } 136 | 137 | // always run our initial empty path in case there are application 138 | // "global" handlers defined. Any callback added without a URL path 139 | // will be treated as a global initialization handler and run on every 140 | // single page load. 141 | const path_parts = route.split('/'); 142 | let path = ''; 143 | HtmxLoad.__run_if_exists(path, ev); 144 | 145 | while ( paths.length ) { 146 | path += paths.shift(); 147 | HtmxLoad.__run_if_exists(path, ev); 148 | path += '/'; 149 | } 150 | }; 151 | 152 | /** This function executes every function in an array stored by route_part. 153 | * 154 | * @param {string} route A URL path (after hostname). Ex: `/blog/34/Cool-Title` 155 | * 156 | * @param {Event} ev The event object from whichever eventListener 157 | * function that called HtmxLoad.run 158 | */ 159 | HtmxLoad.__run_if_exists = function(route_part, ev) { 160 | if ( path in HtmxLoad.route_handlers ) { 161 | const handlers = HtmxLoad.route_handlers[path]; 162 | HtmxLoad.DEBUG && console.log(' ', `running => "${path}"`, handlers.length); 163 | for ( let i = 0; i < handlers.length; ++i ) { 164 | HtmxLoad.DEBUG && console.log(' ', `${i} => `, handlers[i]); 165 | 166 | // run the stored callback function 167 | handlers[i](ev); 168 | } 169 | } 170 | }; 171 | 172 | // Official Event handler callback function for every event we're watching. 173 | // 174 | function htmx_load_event_handler(ev) { 175 | HtmxLoad.run(window.location.pathname, ev); 176 | } 177 | 178 | // Officially register callback function for all possible page load/ready 179 | // events in the browser. 180 | document.addEventListener('DOMContentLoaded', htmx_load_event_handler); 181 | document.addEventListener('htmx:afterSettle', htmx_load_event_handler); 182 | 183 | // popstate is the Window event that fires if you use your back button 184 | // to navigate to a previous page. With htmx, you haven't left the actual 185 | // page, but navigating backwards will attempt to load that view. You need 186 | // to be able to re-run your registered callback functions. 187 | // 188 | // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event 189 | window.addEventListener('popstate', htmx_load_event_handler); 190 | 191 | // htmx-load.js, MIT Copyright 2024 Make Startups, Inc and Eric Harrison 192 | --------------------------------------------------------------------------------