Skip to content

Commit

Permalink
merge 1.8 into master
Browse files Browse the repository at this point in the history
  • Loading branch information
1cg committed Jul 12, 2022
2 parents 299a9ba + 71fe07b commit f784e72
Show file tree
Hide file tree
Showing 223 changed files with 119,415 additions and 1,489 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## [1.8.0] - 2022-12-7

* **NOTE**: This release involved some changes to toughy code (e.g. history support) so please test thoroughly and let
us know if you see any issues
* Boosted forms now will automatically push URLs into history as with links. The [response URL](https://caniuse.com/mdn-api_xmlhttprequest_responseurl)
detection API support is good enough that we feel comfortable making this the default now.
* If you do not want this behavior you can add `hx-push-url='false'` to your boosted forms
* The [`hx-replace-url`](https://htmx.org/attributes/hx-replace-url) attribute was introduced, allowing you to replace
the current URL in history (to complement `hx-push-url`)
* Bug fix - if htmx is included in a page more than once, we do not process elements multiple times
* Bug fix - When localStorage is not available we do not attempt to save history in it
* [Bug fix](https://github.com/bigskysoftware/htmx/issues/908) - `hx-boost` respects the `enctype` attribute
* `m` is now a valid timing modifier (e.g. `hx-trigger="every 2m"`)
* `next` and `previous` are now valid extended query selector modifiers, e.g. `hx-target="next div"` will target the
next div from the current element
* Bug fix - `hx-boost` will boost anchor tags with a `_self` target
* The `load` event now properly supports event filters
* The websocket extension has had many improvements: (A huge thank you to Denis Palashevskii, our newest committer on the project!)
* Implement proper `hx-trigger` support
* Expose trigger handling API to extensions
* Implement safe message sending with sending queue
* Fix `ws-send` attributes connecting in new elements
* Fix OOB swapping of multiple elements in response
* The `HX-Location` response header now implements client-side redirects entirely within htmx
* The `HX-Reswap` response header allows you to change the swap behavior of htmx
* 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
* The new [`hx-replace-url`](/attributes/hx-replace-url) attribute can be used to replace the current URL in the location
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.
* htmx now properly handles anchors in both boosted links, as well as in `hx-get`, etc. attributes

## [1.7.0] - 2022-02-2

* The new [`hx-sync`](/attributes/hx-sync) attribute allows you to synchronize multiple element requests on a single
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ By removing these arbitrary constraints htmx completes HTML as a
## quick start

```html
<script src="https://unpkg.com/htmx.org@1.7.0"></script>
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
Expand Down
16 changes: 16 additions & 0 deletions dist/ext/disable-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

// Disable Submit Button
htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
let elt = evt.detail.elt;
let target = elt.getAttribute("hx-disable-element");
let targetElement = (target == "self") ? elt : document.querySelector(target);

if (name === "htmx:beforeRequest" && targetElement) {
targetElement.disabled = true;
} else if (name == "htmx:afterRequest" && targetElement) {
targetElement.disabled = false;
}
}
});
16 changes: 15 additions & 1 deletion dist/ext/loading-states.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy',
]

let loadingStateEltsByType = {}
Expand All @@ -77,7 +78,7 @@
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.path
evt.detail.pathInfo.requestPath
)
})

Expand Down Expand Up @@ -153,6 +154,19 @@
})
}
)

loadingStateEltsByType['data-loading-aria-busy'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.setAttribute("aria-busy", "true")),
() => (targetElt.removeAttribute("aria-busy"))
)
})
}
)
}

if (name === 'htmx:afterOnLoad') {
Expand Down
170 changes: 115 additions & 55 deletions dist/ext/ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f

/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function(apiRef) {

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

/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function(name, evt) {

Expand All @@ -50,14 +50,17 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
internalData.webSocket.close();
}
return;

// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
var parent = evt.target;

forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function(child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
Expand All @@ -81,14 +84,14 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f

/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} elt
* @param {number=} retryCount
* @returns
* the element's "ws-connect" attribute.
* @param {HTMLElement} elt
* @param {number=} retryCount
* @returns
*/
function ensureWebSocket(elt, retryCount) {

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

var messageQueue = [];

socket.onopen = function (e) {
retryCount = 0;
handleQueuedMessages(messageQueue, socket);
}

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

if (fragment.children.length) {
for (var i = 0; i < fragment.children.length; i++) {
api.oobSwap(api.getAttributeValue(fragment.children[i], "hx-swap-oob") || "true", fragment.children[i], settleInfo);
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}

api.settleImmediately(settleInfo.tasks);
});

// Re-connect any ws-send commands as well.
forEach(queryAttributeOnThisOrChildren(elt, "ws-send"), function(child) {
var legacyAttribute = api.getAttributeValue(child, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
processWebSocketSend(elt, child);
});

// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(elt).webSocket = socket;
api.getInternalData(elt).webSocketMessageQueue = messageQueue;
}

/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}

var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}

/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}

/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} parent
* @param {HTMLElement} child
*/
*/
function processWebSocketSend(parent, child) {
child.addEventListener(api.getTriggerSpecs(child)[0].trigger, function (evt) {
var webSocket = api.getInternalData(parent).webSocket;
var headers = api.getHeaders(child, parent);
var results = api.getInputValues(child, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(child);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, child);
filteredParameters['HEADERS'] = headers;
if (errors && errors.length > 0) {
api.triggerEvent(child, 'htmx:validation:halted', errors);
return;
}
webSocket.send(JSON.stringify(filteredParameters));
if(api.shouldCancel(child)){
evt.preventDefault();
}
var nodeData = api.getInternalData(child);
let triggerSpecs = api.getTriggerSpecs(child);
triggerSpecs.forEach(function(ts) {
api.addTriggerHandler(child, ts, nodeData, function (evt) {
var webSocket = api.getInternalData(parent).webSocket;
var messageQueue = api.getInternalData(parent).webSocketMessageQueue;
var headers = api.getHeaders(child, parent);
var results = api.getInputValues(child, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(child);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, child);
filteredParameters['HEADERS'] = headers;
if (errors && errors.length > 0) {
api.triggerEvent(child, 'htmx:validation:halted', errors);
return;
}
webSocketSend(webSocket, JSON.stringify(filteredParameters), messageQueue);
if(api.shouldCancel(evt, child)){
evt.preventDefault();
}
});
});
}


/**
* webSocketSend provides a safe way to send messages through a WebSocket.
* It checks that the socket is in OPEN state and, otherwise, awaits for it.
* @param {WebSocket} socket
* @param {string} message
* @param {string[]} messageQueue
* @return {boolean}
*/
function webSocketSend(socket, message, messageQueue) {
if (socket.readyState != socket.OPEN) {
messageQueue.push(message);
} else {
socket.send(message);
}
}

/**
* handleQueuedMessages sends messages awaiting in the message queue
*/
function handleQueuedMessages(messageQueue, socket) {
while (messageQueue.length > 0) {
var message = messageQueue[0]
if (socket.readyState == socket.OPEN) {
socket.send(message);
messageQueue.shift()
} else {
break;
}
}
}

/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
Expand All @@ -230,12 +289,12 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f

/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
Expand All @@ -248,8 +307,8 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url){
Expand All @@ -258,9 +317,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f

/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {

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

/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
Expand All @@ -292,4 +351,5 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
}
}

})();
})();

Loading

0 comments on commit f784e72

Please sign in to comment.