Skip to content

Commit 4a81723

Browse files
enable hx-preserve handing for oob swaps (#2934)
* Add support for oob swaps with hx-preserve * Add tests * Documentation * Impove fix to handle when oob swaps shouldSwap set false
1 parent d528c9d commit 4a81723

File tree

3 files changed

+49
-1
lines changed

3 files changed

+49
-1
lines changed

src/htmx.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1450,7 +1450,9 @@ var htmx = (function() {
14501450

14511451
target = beforeSwapDetails.target // allow re-targeting
14521452
if (beforeSwapDetails.shouldSwap) {
1453+
handlePreservedElements(fragment)
14531454
swapWithStyle(swapStyle, target, target, fragment, settleInfo)
1455+
restorePreservedElements()
14541456
}
14551457
forEach(settleInfo.elts, function(elt) {
14561458
triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
@@ -1479,7 +1481,7 @@ var htmx = (function() {
14791481
}
14801482

14811483
/**
1482-
* @param {DocumentFragment} fragment
1484+
* @param {DocumentFragment|ParentNode} fragment
14831485
*/
14841486
function handlePreservedElements(fragment) {
14851487
forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function(preservedElt) {

test/attributes/hx-preserve.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,38 @@ describe('hx-preserve attribute', function() {
3434
byId('d1').innerHTML.should.equal('Old Content')
3535
byId('d2').innerHTML.should.equal('New Content')
3636
})
37+
38+
it('preserved element should not be swapped if it is part of a oob swap', function() {
39+
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='true'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
40+
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
41+
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
42+
div1.click()
43+
this.server.respond()
44+
byId('d1').innerHTML.should.equal('Normal Content')
45+
byId('d3').innerHTML.should.equal('Old Content')
46+
byId('d4').innerHTML.should.equal('New oob Content')
47+
})
48+
49+
it('preserved element should not be swapped if it is part of a hx-select-oob swap', function() {
50+
this.server.respondWith('GET', '/test', "Normal Content<div id='d2'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
51+
var div1 = make("<div id='d1' hx-get='/test' hx-select-oob='#d2'>Click Me!</div>")
52+
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
53+
div1.click()
54+
this.server.respond()
55+
byId('d1').innerHTML.should.equal('Normal Content')
56+
byId('d3').innerHTML.should.equal('Old Content')
57+
byId('d4').innerHTML.should.equal('New oob Content')
58+
})
59+
60+
it('preserved element should relocated unchanged if it is part of a oob swap targeting a different loction', function() {
61+
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='innerHTML:#d5'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
62+
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
63+
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
64+
var div5 = make("<div id='d5'></div>")
65+
div1.click()
66+
this.server.respond()
67+
byId('d1').innerHTML.should.equal('Normal Content')
68+
byId('d2').innerHTML.should.equal('')
69+
byId('d5').innerHTML.should.equal('<div id="d3" hx-preserve="">Old Content</div><div id="d4">New oob Content</div>')
70+
})
3771
})

www/content/attributes/hx-preserve.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ The response requires an element with the same `id`, but its type and other attr
1010
Note that some elements cannot unfortunately be preserved properly, such as `<input type="text">` (focus and caret position are lost), iframes or certain types of videos. To tackle some of these cases we recommend the [morphdom extension](https://github.com/bigskysoftware/htmx-extensions/blob/main/src/morphdom-swap/README.md), which does a more elaborate DOM
1111
reconciliation.
1212

13+
## OOB Swap Usage
14+
15+
You can include `hx-preserve` in the inner response of a [hx-swap-oob](@/attributes/hx-swap-oob.md) and it will preserve the element unchanged during the out of band partial replacement as well. However, you cannot place `hx-preserve` on the same element as the `hx-swap-oob` is placed. For example, here is an oob response that replaces notify but leaves the retain div unchanged.
16+
17+
```html
18+
<div id="notify" hx-swap-oob="true">
19+
<p>The below content will not be changed</p>
20+
<div id="retain" hx-preserve>Use the on-page contents</div>
21+
</div>
22+
```
23+
1324
## Notes
1425

1526
* `hx-preserve` is not inherited
27+
* `hx-preserve` can cause elements to be relocated to a new location when swapping in a partial response

0 commit comments

Comments
 (0)