Skip to content

Commit f784e72

Browse files
committed
merge 1.8 into master
2 parents 299a9ba + 71fe07b commit f784e72

223 files changed

Lines changed: 119415 additions & 1489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## [1.8.0] - 2022-12-7
4+
5+
* **NOTE**: This release involved some changes to toughy code (e.g. history support) so please test thoroughly and let
6+
us know if you see any issues
7+
* Boosted forms now will automatically push URLs into history as with links. The [response URL](https://caniuse.com/mdn-api_xmlhttprequest_responseurl)
8+
detection API support is good enough that we feel comfortable making this the default now.
9+
* If you do not want this behavior you can add `hx-push-url='false'` to your boosted forms
10+
* The [`hx-replace-url`](https://htmx.org/attributes/hx-replace-url) attribute was introduced, allowing you to replace
11+
the current URL in history (to complement `hx-push-url`)
12+
* Bug fix - if htmx is included in a page more than once, we do not process elements multiple times
13+
* Bug fix - When localStorage is not available we do not attempt to save history in it
14+
* [Bug fix](https://github.com/bigskysoftware/htmx/issues/908) - `hx-boost` respects the `enctype` attribute
15+
* `m` is now a valid timing modifier (e.g. `hx-trigger="every 2m"`)
16+
* `next` and `previous` are now valid extended query selector modifiers, e.g. `hx-target="next div"` will target the
17+
next div from the current element
18+
* Bug fix - `hx-boost` will boost anchor tags with a `_self` target
19+
* The `load` event now properly supports event filters
20+
* The websocket extension has had many improvements: (A huge thank you to Denis Palashevskii, our newest committer on the project!)
21+
* Implement proper `hx-trigger` support
22+
* Expose trigger handling API to extensions
23+
* Implement safe message sending with sending queue
24+
* Fix `ws-send` attributes connecting in new elements
25+
* Fix OOB swapping of multiple elements in response
26+
* The `HX-Location` response header now implements client-side redirects entirely within htmx
27+
* The `HX-Reswap` response header allows you to change the swap behavior of htmx
28+
* The new [`hx-select-oob`](/attributes/hx-select-oob) attribute selects one or more elements from a server response to swap in via an out of band swap
29+
* The new [`hx-replace-url`](/attributes/hx-replace-url) attribute can be used to replace the current URL in the location
30+
bar (very similar to `hx-push-url` but no new history entry is created). The corresponding `HX-Replace-Url` response header can be used as well.
31+
* htmx now properly handles anchors in both boosted links, as well as in `hx-get`, etc. attributes
32+
333
## [1.7.0] - 2022-02-2
434

535
* The new [`hx-sync`](/attributes/hx-sync) attribute allows you to synchronize multiple element requests on a single

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ By removing these arbitrary constraints htmx completes HTML as a
3434
## quick start
3535

3636
```html
37-
<script src="https://unpkg.com/htmx.org@1.7.0"></script>
37+
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
3838
<!-- have a button POST a click via AJAX -->
3939
<button hx-post="/clicked" hx-swap="outerHTML">
4040
Click Me

dist/ext/disable-element.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use strict";
2+
3+
// Disable Submit Button
4+
htmx.defineExtension('disable-element', {
5+
onEvent: function (name, evt) {
6+
let elt = evt.detail.elt;
7+
let target = elt.getAttribute("hx-disable-element");
8+
let targetElement = (target == "self") ? elt : document.querySelector(target);
9+
10+
if (name === "htmx:beforeRequest" && targetElement) {
11+
targetElement.disabled = true;
12+
} else if (name == "htmx:afterRequest" && targetElement) {
13+
targetElement.disabled = false;
14+
}
15+
}
16+
});

dist/ext/loading-states.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
'data-loading-class',
7070
'data-loading-class-remove',
7171
'data-loading-disable',
72+
'data-loading-aria-busy',
7273
]
7374

7475
let loadingStateEltsByType = {}
@@ -77,7 +78,7 @@
7778
loadingStateEltsByType[type] = getLoadingStateElts(
7879
container,
7980
type,
80-
evt.detail.pathInfo.path
81+
evt.detail.pathInfo.requestPath
8182
)
8283
})
8384

@@ -153,6 +154,19 @@
153154
})
154155
}
155156
)
157+
158+
loadingStateEltsByType['data-loading-aria-busy'].forEach(
159+
(sourceElt) => {
160+
getLoadingTarget(sourceElt).forEach((targetElt) => {
161+
queueLoadingState(
162+
sourceElt,
163+
targetElt,
164+
() => (targetElt.setAttribute("aria-busy", "true")),
165+
() => (targetElt.removeAttribute("aria-busy"))
166+
)
167+
})
168+
}
169+
)
156170
}
157171

158172
if (name === 'htmx:afterOnLoad') {

dist/ext/ws.js

Lines changed: 115 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
1313

1414
/**
1515
* init is called once, when this extension is first registered.
16-
* @param {import("../htmx").HtmxInternalApi} apiRef
16+
* @param {import("../htmx").HtmxInternalApi} apiRef
1717
*/
1818
init: function(apiRef) {
1919

@@ -33,9 +33,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
3333

3434
/**
3535
* onEvent handles all events passed to this extension.
36-
*
37-
* @param {string} name
38-
* @param {Event} evt
36+
*
37+
* @param {string} name
38+
* @param {Event} evt
3939
*/
4040
onEvent: function(name, evt) {
4141

@@ -50,14 +50,17 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
5050
internalData.webSocket.close();
5151
}
5252
return;
53-
53+
5454
// Try to create EventSources when elements are processed
5555
case "htmx:afterProcessNode":
5656
var parent = evt.target;
5757

5858
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function(child) {
5959
ensureWebSocket(child)
6060
});
61+
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
62+
ensureWebSocketSend(child)
63+
});
6164
}
6265
}
6366
});
@@ -81,14 +84,14 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
8184

8285
/**
8386
* ensureWebSocket creates a new WebSocket on the designated element, using
84-
* the element's "ws-connect" attribute.
85-
* @param {HTMLElement} elt
86-
* @param {number=} retryCount
87-
* @returns
87+
* the element's "ws-connect" attribute.
88+
* @param {HTMLElement} elt
89+
* @param {number=} retryCount
90+
* @returns
8891
*/
8992
function ensureWebSocket(elt, retryCount) {
9093

91-
// If the element containing the WebSocket connection no longer exists, then
94+
// If the element containing the WebSocket connection no longer exists, then
9295
// do not connect/reconnect the WebSocket.
9396
if (!api.bodyContains(elt)) {
9497
return;
@@ -125,16 +128,19 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
125128
/** @type {WebSocket} */
126129
var socket = htmx.createWebSocket(wssSource);
127130

131+
var messageQueue = [];
132+
128133
socket.onopen = function (e) {
129134
retryCount = 0;
135+
handleQueuedMessages(messageQueue, socket);
130136
}
131137

132138
socket.onclose = function (e) {
133139
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
134-
if ([1006, 1012, 1013].indexOf(e.code) >= 0) {
140+
if ([1006, 1012, 1013].indexOf(e.code) >= 0) {
135141
var delay = getWebSocketReconnectDelay(retryCount);
136142
setTimeout(function() {
137-
ensureWebSocket(elt, retryCount+1);
143+
ensureWebSocket(elt, retryCount+1);
138144
}, delay);
139145
}
140146
};
@@ -158,55 +164,108 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
158164
var fragment = api.makeFragment(response);
159165

160166
if (fragment.children.length) {
161-
for (var i = 0; i < fragment.children.length; i++) {
162-
api.oobSwap(api.getAttributeValue(fragment.children[i], "hx-swap-oob") || "true", fragment.children[i], settleInfo);
167+
var children = Array.from(fragment.children);
168+
for (var i = 0; i < children.length; i++) {
169+
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
163170
}
164171
}
165172

166173
api.settleImmediately(settleInfo.tasks);
167174
});
168175

169-
// Re-connect any ws-send commands as well.
170-
forEach(queryAttributeOnThisOrChildren(elt, "ws-send"), function(child) {
171-
var legacyAttribute = api.getAttributeValue(child, "hx-ws");
172-
if (legacyAttribute && legacyAttribute !== 'send') {
173-
return;
174-
}
175-
processWebSocketSend(elt, child);
176-
});
177-
178176
// Put the WebSocket into the HTML Element's custom data.
179177
api.getInternalData(elt).webSocket = socket;
178+
api.getInternalData(elt).webSocketMessageQueue = messageQueue;
179+
}
180+
181+
/**
182+
* ensureWebSocketSend attaches trigger handles to elements with
183+
* "ws-send" attribute
184+
* @param {HTMLElement} elt
185+
*/
186+
function ensureWebSocketSend(elt) {
187+
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
188+
if (legacyAttribute && legacyAttribute !== 'send') {
189+
return;
190+
}
191+
192+
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
193+
processWebSocketSend(webSocketParent, elt);
194+
}
195+
196+
/**
197+
* hasWebSocket function checks if a node has webSocket instance attached
198+
* @param {HTMLElement} node
199+
* @returns {boolean}
200+
*/
201+
function hasWebSocket(node) {
202+
return api.getInternalData(node).webSocket != null;
180203
}
181204

182205
/**
183206
* processWebSocketSend adds event listeners to the <form> element so that
184207
* messages can be sent to the WebSocket server when the form is submitted.
185208
* @param {HTMLElement} parent
186209
* @param {HTMLElement} child
187-
*/
210+
*/
188211
function processWebSocketSend(parent, child) {
189-
child.addEventListener(api.getTriggerSpecs(child)[0].trigger, function (evt) {
190-
var webSocket = api.getInternalData(parent).webSocket;
191-
var headers = api.getHeaders(child, parent);
192-
var results = api.getInputValues(child, 'post');
193-
var errors = results.errors;
194-
var rawParameters = results.values;
195-
var expressionVars = api.getExpressionVars(child);
196-
var allParameters = api.mergeObjects(rawParameters, expressionVars);
197-
var filteredParameters = api.filterValues(allParameters, child);
198-
filteredParameters['HEADERS'] = headers;
199-
if (errors && errors.length > 0) {
200-
api.triggerEvent(child, 'htmx:validation:halted', errors);
201-
return;
202-
}
203-
webSocket.send(JSON.stringify(filteredParameters));
204-
if(api.shouldCancel(child)){
205-
evt.preventDefault();
206-
}
212+
var nodeData = api.getInternalData(child);
213+
let triggerSpecs = api.getTriggerSpecs(child);
214+
triggerSpecs.forEach(function(ts) {
215+
api.addTriggerHandler(child, ts, nodeData, function (evt) {
216+
var webSocket = api.getInternalData(parent).webSocket;
217+
var messageQueue = api.getInternalData(parent).webSocketMessageQueue;
218+
var headers = api.getHeaders(child, parent);
219+
var results = api.getInputValues(child, 'post');
220+
var errors = results.errors;
221+
var rawParameters = results.values;
222+
var expressionVars = api.getExpressionVars(child);
223+
var allParameters = api.mergeObjects(rawParameters, expressionVars);
224+
var filteredParameters = api.filterValues(allParameters, child);
225+
filteredParameters['HEADERS'] = headers;
226+
if (errors && errors.length > 0) {
227+
api.triggerEvent(child, 'htmx:validation:halted', errors);
228+
return;
229+
}
230+
webSocketSend(webSocket, JSON.stringify(filteredParameters), messageQueue);
231+
if(api.shouldCancel(evt, child)){
232+
evt.preventDefault();
233+
}
234+
});
207235
});
208236
}
209-
237+
238+
/**
239+
* webSocketSend provides a safe way to send messages through a WebSocket.
240+
* It checks that the socket is in OPEN state and, otherwise, awaits for it.
241+
* @param {WebSocket} socket
242+
* @param {string} message
243+
* @param {string[]} messageQueue
244+
* @return {boolean}
245+
*/
246+
function webSocketSend(socket, message, messageQueue) {
247+
if (socket.readyState != socket.OPEN) {
248+
messageQueue.push(message);
249+
} else {
250+
socket.send(message);
251+
}
252+
}
253+
254+
/**
255+
* handleQueuedMessages sends messages awaiting in the message queue
256+
*/
257+
function handleQueuedMessages(messageQueue, socket) {
258+
while (messageQueue.length > 0) {
259+
var message = messageQueue[0]
260+
if (socket.readyState == socket.OPEN) {
261+
socket.send(message);
262+
messageQueue.shift()
263+
} else {
264+
break;
265+
}
266+
}
267+
}
268+
210269
/**
211270
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
212271
* @param {number} retryCount // The number of retries that have already taken place
@@ -230,12 +289,12 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
230289

231290
/**
232291
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
233-
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
292+
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
234293
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
235294
* returns FALSE.
236-
*
237-
* @param {*} elt
238-
* @returns
295+
*
296+
* @param {*} elt
297+
* @returns
239298
*/
240299
function maybeCloseWebSocketSource(elt) {
241300
if (!api.bodyContains(elt)) {
@@ -248,8 +307,8 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
248307
/**
249308
* createWebSocket is the default method for creating new WebSocket objects.
250309
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
251-
*
252-
* @param {string} url
310+
*
311+
* @param {string} url
253312
* @returns WebSocket
254313
*/
255314
function createWebSocket(url){
@@ -258,9 +317,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
258317

259318
/**
260319
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
261-
*
262-
* @param {HTMLElement} elt
263-
* @param {string} attributeName
320+
*
321+
* @param {HTMLElement} elt
322+
* @param {string} attributeName
264323
*/
265324
function queryAttributeOnThisOrChildren(elt, attributeName) {
266325

@@ -281,8 +340,8 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
281340

282341
/**
283342
* @template T
284-
* @param {T[]} arr
285-
* @param {(T) => void} func
343+
* @param {T[]} arr
344+
* @param {(T) => void} func
286345
*/
287346
function forEach(arr, func) {
288347
if (arr) {
@@ -292,4 +351,5 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
292351
}
293352
}
294353

295-
})();
354+
})();
355+

0 commit comments

Comments
 (0)