├── 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 |
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 |
--------------------------------------------------------------------------------