mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 13:01:03 +00:00
response-targets extension with documentation, exposed 3 functions in internal API (#1436)
* findThisElement, getClosestAttributeValue, querySelectorExt exposed in internal API * Added response-targets extension. * Extra check for empty element in response-targets.js * Added test/ext/response-targets.js * Linked response-targets.md * Added htmx.config.{responseTargetPrefersExisting, responseTargetPrefersRetargetHeader} configuration options * Typo fixed (not impacting logic)
This commit is contained in:
parent
9bd3e3b1e3
commit
7fc5ac5074
110
src/ext/response-targets.js
Normal file
110
src/ext/response-targets.js
Normal file
@ -0,0 +1,110 @@
|
||||
(function(){
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
const targetAttrPrefix = 'hx-target-';
|
||||
const targetAttrMinLen = targetAttrPrefix.length - 1;
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} respCode
|
||||
* @returns {HTMLElement | null}
|
||||
*/
|
||||
function getRespCodeTarget(elt, respCode) {
|
||||
if (!elt || !respCode) return null;
|
||||
|
||||
var targetAttr = targetAttrPrefix + respCode;
|
||||
var targetStr = api.getClosestAttributeValue(elt, targetAttr);
|
||||
|
||||
if (targetStr) {
|
||||
if (targetStr === "this") {
|
||||
return api.findThisElement(elt, targetAttr);
|
||||
} else {
|
||||
return api.querySelectorExt(elt, targetStr);
|
||||
}
|
||||
} else {
|
||||
for (let l = targetAttr.length - 1; l > targetAttrMinLen; l--) {
|
||||
targetAttr = targetAttr.substring(0, l) + '*';
|
||||
targetStr = api.getClosestAttributeValue(elt, targetAttr);
|
||||
if (targetStr) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetStr) {
|
||||
if (targetStr === "this") {
|
||||
return api.findThisElement(elt, targetAttr);
|
||||
} else {
|
||||
return api.querySelectorExt(elt, targetStr);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Event} evt */
|
||||
function handleErrorFlag(evt) {
|
||||
if (evt.detail.isError) {
|
||||
if (htmx.config.responseTargetUnsetsError) {
|
||||
evt.detail.isError = false;
|
||||
}
|
||||
} else if (htmx.config.responseTargetSetsError) {
|
||||
evt.detail.isError = true;
|
||||
}
|
||||
}
|
||||
|
||||
htmx.defineExtension('response-targets', {
|
||||
|
||||
/** @param {import("../htmx").HtmxInternalApi} apiRef */
|
||||
init: function (apiRef) {
|
||||
api = apiRef;
|
||||
|
||||
if (htmx.config.responseTargetUnsetsError === undefined) {
|
||||
htmx.config.responseTargetUnsetsError = true;
|
||||
}
|
||||
if (htmx.config.responseTargetSetsError === undefined) {
|
||||
htmx.config.responseTargetSetsError = false;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersExisting === undefined) {
|
||||
htmx.config.responseTargetPrefersExisting = false;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
|
||||
htmx.config.responseTargetPrefersRetargetHeader = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
*/
|
||||
onEvent: function (name, evt) {
|
||||
if (name === "htmx:beforeSwap" &&
|
||||
evt.detail.xhr &&
|
||||
evt.detail.xhr.status !== 200) {
|
||||
if (evt.detail.target) {
|
||||
if (htmx.config.responseTargetPrefersExisting) {
|
||||
evt.detail.shoudSwap = true;
|
||||
handleErrorFlag(evt);
|
||||
return true;
|
||||
}
|
||||
if (htmx.config.responseTargetPrefersRetargetHeader &&
|
||||
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
|
||||
evt.detail.shouldSwap = true;
|
||||
handleErrorFlag(evt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!evt.detail.requestConfig) {
|
||||
return true;
|
||||
}
|
||||
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
|
||||
if (target) {
|
||||
handleErrorFlag(evt);
|
||||
evt.detail.shouldSwap = true;
|
||||
evt.detail.target = target;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
@ -90,9 +90,11 @@ return (function () {
|
||||
addTriggerHandler: addTriggerHandler,
|
||||
bodyContains: bodyContains,
|
||||
canAccessLocalStorage: canAccessLocalStorage,
|
||||
findThisElement: findThisElement,
|
||||
filterValues: filterValues,
|
||||
hasAttribute: hasAttribute,
|
||||
getAttributeValue: getAttributeValue,
|
||||
getClosestAttributeValue: getClosestAttributeValue,
|
||||
getClosestMatch: getClosestMatch,
|
||||
getExpressionVars: getExpressionVars,
|
||||
getHeaders: getHeaders,
|
||||
@ -105,6 +107,7 @@ return (function () {
|
||||
mergeObjects: mergeObjects,
|
||||
makeSettleInfo: makeSettleInfo,
|
||||
oobSwap: oobSwap,
|
||||
querySelectorExt: querySelectorExt,
|
||||
selectAndSwap: selectAndSwap,
|
||||
settleImmediately: settleImmediately,
|
||||
shouldCancel: shouldCancel,
|
||||
|
224
test/ext/response-targets.js
Normal file
224
test/ext/response-targets.js
Normal file
@ -0,0 +1,224 @@
|
||||
describe("response-targets extension", function() {
|
||||
beforeEach(function() {
|
||||
this.server = sinon.fakeServer.create();
|
||||
clearWorkArea();
|
||||
});
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('targets an adjacent element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" hx-target-404="#d1" hx-get="/test">Click Me!</button>')
|
||||
var div1 = make('<div id="d1"></div>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets an adjacent element properly with wildcard', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" hx-target-4*="#d1" hx-get="/test">Click Me!</button>')
|
||||
var div1 = make('<div id="d1"></div>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a parent element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets" id="d1"><button id="b1" hx-target-404="#d1" hx-get="/test">Click Me!</button></div>')
|
||||
var btn = byId("b1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a parent element properly with wildcard', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets" id="d1"><button id="b1" hx-target-*="#d1" hx-get="/test">Click Me!</button></div>')
|
||||
var btn = byId("b1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `this` element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets" hx-target-404="this"><button id="b1" hx-get="/test">Click Me!</button></div>')
|
||||
var btn = byId("b1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `closest` element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets"><p><i><button id="b1" hx-target-404="closest div" hx-get="/test">Click Me!</button></i></p></div>')
|
||||
var btn = byId("b1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `closest` element properly w/ hyperscript syntax', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets"><p><i><button id="b1" hx-target-404="closest <div/>" hx-get="/test">Click Me!</button></i></p></div>')
|
||||
var btn = byId("b1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `find` element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets" hx-target-404="find span" hx-get="/test">Click Me! <div><span id="s1"></span><span id="s2"></span></div></div>')
|
||||
div1.click();
|
||||
this.server.respond();
|
||||
var span1 = byId("s1")
|
||||
var span2 = byId("s2")
|
||||
span1.innerHTML.should.equal("Not found!");
|
||||
span2.innerHTML.should.equal("");
|
||||
});
|
||||
|
||||
it('targets a `find` element properly w/ hyperscript syntax', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var div1 = make('<div hx-ext="response-targets" hx-target-404="find <span/>" hx-get="/test">Click Me! <div><span id="s1"></span><span id="s2"></span></div></div>')
|
||||
div1.click();
|
||||
this.server.respond();
|
||||
var span1 = byId("s1")
|
||||
var span2 = byId("s2")
|
||||
span1.innerHTML.should.equal("Not found!");
|
||||
span2.innerHTML.should.equal("");
|
||||
});
|
||||
|
||||
it('targets an inner element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" hx-target-404="#d1" hx-get="/test">Click Me!<div id="d1"></div></button>')
|
||||
var div1 = byId("d1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets an inner element properly w/ hyperscript syntax', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" hx-target-404="<#d1/>" hx-get="/test">Click Me!<div id="d1"></div></button>')
|
||||
var div1 = byId("d1")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('handles bad target gracefully', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" hx-target-404="bad" hx-get="/test">Click Me!</button>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Click Me!");
|
||||
});
|
||||
|
||||
|
||||
it('targets an adjacent element properly w/ data-* prefix', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
var btn = make('<button hx-ext="response-targets" data-hx-target-404="#d1" data-hx-get="/test">Click Me!</button>')
|
||||
var div1 = make('<div id="d1"></div>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `next` element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
make('<div hx-ext="response-targets">' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target-404="next div" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("");
|
||||
});
|
||||
|
||||
it('targets a `next` element properly w/ hyperscript syntax', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
make('<div hx-ext="response-targets">' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target-404="next <div/>" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Not found!");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("");
|
||||
});
|
||||
|
||||
it('targets a `previous` element properly', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
make('<div hx-ext="response-targets">' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target-404="previous div" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
|
||||
it('targets a `previous` element properly w/ hyperscript syntax', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", [404, {}, "Not found!"]);
|
||||
make('<div hx-ext="response-targets">' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target-404="previous <div/>" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("Not found!");
|
||||
});
|
||||
});
|
@ -143,6 +143,9 @@
|
||||
<script src="../src/ext/ws.js"></script>
|
||||
<script src="ext/ws.js"></script>
|
||||
|
||||
<script src="../src/ext/response-targets.js"></script>
|
||||
<script src="ext/response-targets.js"></script>
|
||||
|
||||
<!-- events last so they don't screw up other tests -->
|
||||
<script src="core/events.js"></script>
|
||||
|
||||
|
@ -79,6 +79,7 @@ See the individual extension documentation for more details.
|
||||
| [`path-deps`](@/extensions/path-deps.md) | an extension for expressing path-based dependencies [similar to intercoolerjs](http://intercoolerjs.org/docs.html#dependencies)
|
||||
| [`preload`](@/extensions/preload.md) | preloads selected `href` and `hx-get` targets based on rules you control.
|
||||
| [`remove-me`](@/extensions/remove-me.md) | allows you to remove an element after a given amount of time
|
||||
| [`response-targets`](@/extensions/response-targets.md) | allows to specify different target elements to be swapped when different HTTP response codes are received
|
||||
| [`restored`](@/extensions/restored.md) | allows you to trigger events when the back button has been pressed
|
||||
| [`server-sent-events`](@/extensions/server-sent-events.md) | uni-directional server push messaging via [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
|
||||
| [`web-sockets`](@/extensions/web-sockets.md) | bi-directional connection to WebSocket servers
|
||||
|
117
www/content/extensions/response-targets.md
Normal file
117
www/content/extensions/response-targets.md
Normal file
@ -0,0 +1,117 @@
|
||||
+++
|
||||
title = "response-targets"
|
||||
+++
|
||||
|
||||
This extension allows to specify different target elements to be swapped when
|
||||
different HTTP response codes are received.
|
||||
|
||||
It uses attribute names in a form of ``hx-target-[CODE]`` where `[CODE]` is a numeric
|
||||
HTTP response code with the optional wildcard character at its end.
|
||||
|
||||
The value of each attribute can be:
|
||||
|
||||
* A CSS query selector of the element to target.
|
||||
* `this` which indicates that the element that the `hx-target` attribute is on is the target.
|
||||
* `closest <CSS selector>` which will find the closest parent ancestor that matches the given CSS selector
|
||||
(e.g. `closest tr` will target the closest table row to the element).
|
||||
* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.
|
||||
* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.
|
||||
(e.g. `next .error` will target the closest following sibling element with `error` class)
|
||||
* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.
|
||||
(e.g `previous .error` will target the closest previous sibling with `error` class)
|
||||
|
||||
## Install
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/htmx.org/dist/ext/response-targets.js"></script>
|
||||
```
|
||||
|
||||
## Configure (optional)
|
||||
|
||||
* When `HX-Retarget` response header is received it disables any lookup that would be
|
||||
performed by this extension but any responses with error status codes will be
|
||||
swapped (normally they would not be, even with target set via header) and internal
|
||||
error flag (`isError`) will be modified. You may change this and choose to ignore
|
||||
`HX-Retarget` header when `hx-target-…` is in place by setting a configuration flag
|
||||
`htmx.config.responseTargetPrefersRetargetHeader` to `false` (default is
|
||||
`true`). Note that this extension only performs a simple check whether the header
|
||||
is set and target exists. It is not extracting target's value from the header but
|
||||
trusts it was set by HTMX core logic.
|
||||
|
||||
* Normally, any target which is already established by HTMX built-in functions or
|
||||
extensions called before will be overwritten if a matching `hx-target-…` tag is
|
||||
found. You may change it by using a configuration flag
|
||||
`htmx.config.responseTargetPrefersExisting` to `true` (default is `false`). This is
|
||||
kinky and risky option. It has a real-life applications similar to a skilled,
|
||||
full-stack tardigrade eating parentheses when no one is watching.
|
||||
|
||||
* `isError` flag on the `detail` member of an event associated with swapping the
|
||||
content with `hx-target-[CODE]` will be set to `false` when error response code is
|
||||
received. This is different from the default behavior. You may change this by
|
||||
setting a configuration flag `htmx.config.responseTargetUnsetsError` to `false`
|
||||
(default is `true`).
|
||||
|
||||
* `isError` flag on the `detail` member of an event associated with swapping the
|
||||
content with `hx-target-[CODE]` will be set to `false` when non-erroneous response
|
||||
code is received. This is no different from the default behavior. You may change
|
||||
this by setting a configuration flag `htmx.config.responseTargetSetsError` to
|
||||
`true` (default is `false`). This setting will not affect the response code 200
|
||||
since it is not handled by this extension.
|
||||
|
||||
## Usage
|
||||
|
||||
Here is an example that targets a `div` for normal (200) response but another `div`
|
||||
for 404 (not found) response, and yet another for all 5xx response codes:
|
||||
|
||||
```html
|
||||
<div hx-ext="response-targets">
|
||||
<div id="response-div"></div>
|
||||
<button hx-post="/register"
|
||||
hx-target="#response-div"
|
||||
hx-target-5*="#serious-errors"
|
||||
hx-target-404="#not-found">
|
||||
Register!
|
||||
</button>
|
||||
<div id="serious-errors"></div>
|
||||
<div id="not-found"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
* The response from the `/register` URL will replace contents of the `div` with the
|
||||
`id` `response-div` when response code is 200 (OK).
|
||||
|
||||
* The response from the `/register` URL will replace contents of the `div` with the `id`
|
||||
`serious-errors` when response code begins with a digit 5 (server errors).
|
||||
|
||||
* The response from the `/register` URL will will replace contents of the `div` with
|
||||
the `id` `not-found` when response code is 404 (Not Found).
|
||||
|
||||
## Wildcard resolution
|
||||
|
||||
When status response code does not match existing `hx-target-[CODE]` attribute name
|
||||
then its numeric part expressed as a string is trimmed with last character being
|
||||
replaced with the asterisk (`*`). This lookup process continues until the attribute
|
||||
is found or there are no more digits.
|
||||
|
||||
For example, if a browser receives 404 error code, the following attribute names will
|
||||
be looked up (in the given order):
|
||||
|
||||
* `hx-target-404`
|
||||
* `hx-target-40*`
|
||||
* `hx-target-4*`
|
||||
* `hx-target-*`.
|
||||
|
||||
## Notes
|
||||
|
||||
* `hx-target-…` is inherited and can be placed on a parent element.
|
||||
* `hx-target-…` cannot be used to handle HTTP response code 200.
|
||||
* `hx-target-…` will honor `HX-Retarget` by default and will prefer it over any
|
||||
calculated target but it can be changed by disabling the
|
||||
`htmx.config.responseTargetPrefersRetargetHeader` configuration option.
|
||||
* To avoid surprises the `hx-ext` attribute used to enable this extension should be
|
||||
placed on a parent element containing elements with `hx-target-…` and `hx-target`
|
||||
attributes.
|
||||
|
||||
## See also
|
||||
|
||||
* [`hx-target`](@/attributes/hx-target.md), specifies the target element to be swapped
|
Loading…
x
Reference in New Issue
Block a user