Replies: 4 comments 1 reply
-
Here's what I've worked out (to try implementing as an extension): (function() {
var api;
htmx.defineExtension('my-ext', {
init: function(apiRef) {
api = apiRef;
},
onEvent: function(name, evt) {
//console.debug("onEvent", {name, evt});
},
transformResponse: function(text, xhr, elt) {
let fragment = api.makeFragment(text);
console.debug("frag", fragment);
// TODO: remove elements that I don't want handled by the existing OOB logic
// TODO: turn the remaining fragment back into text and return
return text;
},
isInlineSwap: function(swapStyle) {
//console.debug("isInlineSwap", {swapStyle});
return false;
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// OOB swaps don't happen here
//console.debug("handleSwap", {swapStyle, target, fragment, settleInfo});
return false;
},
encodeParameters: function(xhr, parameters, elt) {
//console.debug("encodeParameters", {xhr, parameters, elt});
return null;
}
})
})()
I'll keep trying and see if I can work around these issues. |
Beta Was this translation helpful? Give feedback.
-
All right, got it working. (function() {
var api;
htmx.defineExtension('oob-attribute-swap', {
init: function(apiRef) {
api = apiRef;
},
onEvent: function(name, evt) {
//console.debug("onEvent", {name, evt});
return true;
},
transformResponse: function(text, xhr, elt) {
let fragment = api.makeFragment(text);
console.debug("frag", fragment);
// N.B. You will need to get your backend to echo this back (until I can find a way
// around the issue with boosted responses. I don't see any issue as someone
// looking at the network tab can see that it was set on request.
if (xhr.getResponseHeader("hx-boosted")) {
// Running the code below with a boosted response causes issues. The conversion from
// text to fragment and back yields a different result somehow.
return text;
}
// Take out all fragments where an attribute update is requested
let taken = fragment.querySelectorAll("[hx-swap-oob=attributes]");
// Collect all the fragments that we aren't going to touch
let preserved = fragment.querySelectorAll(":not([hx-swap-oob=attributes])");
// For each node where we're swapping attributes...
Array.from(taken).forEach(node => {
// Take the existing node in the document...
let target = document.querySelector(`#${node.getAttribute('id')}`);
// Copy all the attributes from the OOB fragment to the target node
[...node.attributes].map(({name, value}) => {
target.setAttribute(name, value);
})
})
let preservedHtml = Array.from(preserved).map((n) => n.outerHTML).join("");
// Return HTML that we didn't do attribute updates for
return preservedHtml;
},
isInlineSwap: function(swapStyle) {
//console.debug("isInlineSwap", {swapStyle});
return false;
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// OOB swaps don't happen here
//console.debug("handleSwap", {swapStyle, target, fragment, settleInfo});
return false;
},
encodeParameters: function(xhr, parameters, elt) {
//console.debug("encodeParameters", {xhr, parameters, elt});
return null;
}
})
})()
That's all I need for my purposes, but maybe someone can think of a better way to do it. I'm not sure what the consequences of doing this so hackishly would be. edit: found a small issue when running this on boosted responses, so I added a check which skips the logic if the response isn't a partial. |
Beta Was this translation helpful? Give feedback.
-
I see the issue. The use of |
Beta Was this translation helpful? Give feedback.
-
Fixed it. Now it works on boosted responses without the header passing trick as well. (function() {
var api;
htmx.defineExtension('oob-attribute-swap', {
init: function(apiRef) {
api = apiRef;
},
onEvent: function(name, evt) {
//console.debug("onEvent", {name, evt});
return true;
},
transformResponse: function(text, xhr, elt) {
let fragment = api.makeFragment(text);
// Take out items which:
//
// 1. Are direct descendants of the root
// 2. Which have [hx-swap-oob="attributes"]
let taken = Array.from(fragment.children).filter(node => {
let swap_attr = node.attributes.getNamedItem("hx-swap-oob");
if (swap_attr && swap_attr.value === "attributes") {
return true;
}
return false;
});
// Preserve and pass on everything else.
let preserved = Array.from(fragment.children).filter(node => {
let swap_attr = node.attributes.getNamedItem("hx-swap-oob");
if (swap_attr === null) {
return true;
} else if (swap_attr && swap_attr.value !== "attributes") {
return true;
}
return false;
});
// For each node where we're swapping attributes...
Array.from(taken).forEach(node => {
// Take the existing node in the document...
let target = document.querySelector(`#${node.getAttribute('id')}`);
// Copy all the attributes from the OOB fragment to the target node
[...node.attributes].map(({name, value}) => {
target.setAttribute(name, value);
})
});
let preservedHtml = Array.from(preserved).map((n) => n.outerHTML).join("");
// Return HTML that we didn't do attribute updates for
return preservedHtml;
},
isInlineSwap: function(swapStyle) {
//console.debug("isInlineSwap", {swapStyle});
return false;
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// OOB swaps don't happen here
//console.debug("handleSwap", {swapStyle, target, fragment, settleInfo});
return false;
},
encodeParameters: function(xhr, parameters, elt) {
//console.debug("encodeParameters", {xhr, parameters, elt});
return null;
}
})
})() |
Beta Was this translation helpful? Give feedback.
-
tl;dr - I'd like a version of
hx-swap
/hx-swap-oob
that targets a node's attributes without having to change/update its contents.edit: A working prototype in extension form is here
I'm developing techniques to make gracefully degrading apps without having to create multiple templates.
Imagine a page which has a left and right panel:
If we assume the happy path, the markup (ignoring CSS) might look something like:
The user clicks on a link in the left pane:
hx-get
+hx-target
)href
)There's only one downside: the page looks kind of goofy to a non-JS user (there's a panel that doesn't do anything). It would be ideal if the pane for item details didn't display until it was reasonably certain that the user had JS enabled.
On the backend side, we can look for the
HX-Request
header, and any time we see it we can send back a fragment after the usual content:The
#js
div can be combined with CSS to control content on the rest of the page:Now it's possible to give a good experience to both JS/non-JS users without having to create two copies of the template. The fragment that does the OOB update can be part of a middleware in your application so individual route handlers don't have to care about JS support.
The only possible improvement from here would be to avoid having to use the sibling selector.
Imagine if that same OOB fragment could do:
HTMX would take all the attributes in the fragment, and add them to the target (probably merging in this case, but you could imagine
attributesReplace
as well).Now your CSS selectors could look like:
To recap on the benefits of this approach:
href
for non-JS, andhx-get
for JSHX-Request
comes in, we can be almost 100% certain that we can provide the upgraded experience<body>
tag); i.e. save bandwidth and maybe DOM update time.The ability to swap attributes (either with
hx-swap
orhx-swap-oob
) probably has more applications I haven't thought of.The only question I have now is: could this cause any problems?
Beta Was this translation helpful? Give feedback.
All reactions