swap refactor polishing

This commit is contained in:
Denis Palashevskii
2024-01-17 21:40:25 +04:00
parent 2562599b12
commit a5e2b6ca86
5 changed files with 118 additions and 33 deletions

16
src/htmx.d.ts vendored
View File

@@ -438,6 +438,22 @@ export interface HtmxConfig {
triggerSpecsCache?: {[trigger: string]: HtmxTriggerSpecification[]};
}
type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string
export interface HtmxSwapSpecification {
swapStyle: HtmxSwapStyle;
swapDelay?: number;
settleDelay?: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: string;
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
}
/**
* https://htmx.org/extensions/#defining
*/

View File

@@ -25,7 +25,7 @@ var htmx = (function() {
removeClass: removeClassFromElement,
toggleClass: toggleClassOnElement,
takeClass: takeClassForElement,
swap: fullSwap,
swap: swap,
/* Extension entrypoints */
defineExtension,
removeExtension,
@@ -87,7 +87,7 @@ var htmx = (function() {
canAccessLocalStorage,
findThisElement,
filterValues,
fullSwap,
swap,
hasAttribute,
getAttributeValue,
getClosestAttributeValue,
@@ -851,7 +851,7 @@ var htmx = (function() {
target = beforeSwapDetails.target // allow re-targeting
if (beforeSwapDetails.shouldSwap) {
swap(swapStyle, target, target, fragment, settleInfo)
swapWithStyle(swapStyle, target, target, fragment, settleInfo)
}
forEach(settleInfo.elts, function(elt) {
triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
@@ -1038,7 +1038,14 @@ var htmx = (function() {
}
}
function swap(swapStyle, elt, target, fragment, settleInfo) {
/**
* @param {string} swapStyle
* @param {HTMLElement} elt
* @param {HTMLElement} target
* @param {Node} fragment
* @param {{ tasks: (() => void)[]; }} settleInfo
*/
function swapWithStyle(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case 'none':
return
@@ -1085,7 +1092,7 @@ var htmx = (function() {
if (swapStyle === 'innerHTML') {
swapInnerHTML(target, fragment, settleInfo)
} else {
swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
swapWithStyle(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
}
}
}
@@ -1120,13 +1127,11 @@ var htmx = (function() {
/**
* @typedef {Object} SwapOptions
* @property {SwapSpecification} spec
* @property {?string} select
* @property {?string} selectOOB
* @property {?boolean} ignoreTitle
* @property {?*} responseInfo
* @property {?*} headStrategy
* @property {?Element} contextElement
* @property {?*} eventInfo
* @property {?*} anchor
* @property {?HTMLElement} contextElement
* @property {?swapCallback} afterSwapCallback
* @property {?swapCallback} afterSettleCallback
*/
@@ -1136,9 +1141,14 @@ var htmx = (function() {
* title updates, head merging, scroll, OOB swapping, normal swapping and settling
* @param {string|Element} target
* @param {string} content
* @param {import("./htmx").HtmxSwapSpecification} swapSpec
* @param {SwapOptions} swapOptions
*/
function fullSwap(target, content, swapOptions) {
function swap(target, content, swapSpec, swapOptions) {
if (!swapOptions) {
swapOptions = {}
}
target = resolveTarget(target);
// preserve focus and selection
@@ -1197,19 +1207,19 @@ var htmx = (function() {
fragment = newFragment
}
handlePreservedElements(fragment);
swap(swapOptions.spec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo);
swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo);
// apply saved focus and selection information to swapped content
if (selectionInfo.elt &&
!bodyContains(selectionInfo.elt) &&
getRawAttribute(selectionInfo.elt, 'id')) {
const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
const focusOptions = { preventScroll: swapOptions.spec.focusScroll !== undefined ? !swapOptions.spec.focusScroll : !htmx.config.defaultFocusScroll }
const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
if (newActiveElt) {
// @ts-ignore
if (selectionInfo.start && newActiveElt.setSelectionRange) {
// @ts-ignore
try {
// @ts-ignore
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
} catch (e) {
// the setSelectionRange method is present on fields that don't support it, so just let this fail
@@ -1224,18 +1234,18 @@ var htmx = (function() {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass)
}
triggerEvent(elt, 'htmx:afterSwap', swapOptions.responseInfo)
triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
})
if (swapOptions.afterSwapCallback) {
swapOptions.afterSwapCallback();
}
// merge in new head after swap but before settle
if (!swapOptions.ignoreTitle) {
if (!swapSpec.ignoreTitle) {
handleTitle(settleInfo.title);
}
if (triggerEvent(document.body, "htmx:beforeHeadMerge", {head: settleInfo.head})) {
handleHeadTag(settleInfo.head, swapOptions.headStrategy);
handleHeadTag(settleInfo.head, swapSpec.head);
}
// settle
@@ -1247,24 +1257,24 @@ var htmx = (function() {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass)
}
triggerEvent(elt, 'htmx:afterSettle', swapOptions.responseInfo)
triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
})
if (swapOptions.responseInfo && swapOptions.responseInfo.pathInfo.anchor) {
const anchorTarget = getDocument().getElementById(swapOptions.responseInfo.pathInfo.anchor)
if (swapOptions.anchor) {
const anchorTarget = resolveTarget(swapOptions.anchor);
if (anchorTarget) {
anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
}
}
updateScrollState(settleInfo.elts, swapOptions.spec)
updateScrollState(settleInfo.elts, swapSpec)
if (swapOptions.afterSettleCallback) {
swapOptions.afterSettleCallback()
}
}
if (swapOptions.spec.settleDelay > 0) {
setTimeout(doSettle, swapOptions.spec.settleDelay)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle, swapSpec.settleDelay)
} else {
doSettle()
}
@@ -2682,11 +2692,12 @@ var htmx = (function() {
/**
*
* @param {HTMLElement} elt
* @param {string} swapInfoOverride
* @param {import("./htmx").HtmxSwapStyle} swapInfoOverride
* @returns {import("./htmx").HtmxSwapSpecification}
*/
function getSwapSpecification(elt, swapInfoOverride) {
const swapInfo = swapInfoOverride || getClosestAttributeValue(elt, 'hx-swap')
/** @type import("./htmx").HtmxSwapSpecification */
const swapSpec = {
swapStyle: getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
swapDelay: htmx.config.defaultSwapDelay,
@@ -3571,11 +3582,11 @@ var htmx = (function() {
}
var swapSpec = getSwapSpecification(elt, swapOverride)
if (swapSpec.hasOwnProperty('ignoreTitle')) {
ignoreTitle = swapSpec.ignoreTitle
if (!swapSpec.hasOwnProperty('ignoreTitle')) {
swapSpec.ignoreTitle = ignoreTitle
}
if (swapSpec.hasOwnProperty('head')) {
head = swapSpec.head
if (!swapSpec.hasOwnProperty('head')) {
swapSpec.head = head
}
target.classList.add(htmx.config.swappingClass)
@@ -3609,14 +3620,12 @@ var htmx = (function() {
}
}
fullSwap(target, serverResponse, {
spec: swapSpec,
swap(target, serverResponse, swapSpec, {
select: selectOverride || select,
selectOOB,
ignoreTitle,
responseInfo,
eventInfo: responseInfo,
anchor: responseInfo.pathInfo.anchor,
contextElement: elt,
headStrategy: head,
afterSwapCallback: function() {
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
let finalElt = elt

View File

@@ -375,4 +375,32 @@ describe('Core htmx API test', function() {
htmx.trigger(div, 'myEvent')
myEventCalled.should.equal(true)
})
it('swaps content properly (basic)', function() {
var output = make('<output id="output"/>')
htmx.swap('#output', '<div>Swapped!</div>', {swapStyle: 'innerHTML'});
output.innerHTML.should.be.equal('<div>Swapped!</div>');
})
it('swaps content properly (with select)', function() {
var output = make('<output id="output"/>')
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', {swapStyle: 'innerHTML'}, {select: '#select-me'});
output.innerHTML.should.be.equal('<p id="select-me">Swapped!</p>');
})
it('swaps content properly (with oob)', function() {
var output = make('<output id="output"/>')
var oobDiv = make('<div id="oob"/>')
htmx.swap('#output', '<div id="oob" hx-swap-oob="innerHTML">OOB Swapped!</div><div>Swapped!</div>', {swapStyle: 'innerHTML'});
output.innerHTML.should.be.equal('<div>Swapped!</div>');
oobDiv.innerHTML.should.be.equal('OOB Swapped!');
})
it('swaps content properly (with select oob)', function() {
var output = make('<output id="output"/>')
var oobDiv = make('<div id="oob"/>')
htmx.swap('#output', '<div id="oob">OOB Swapped!</div><div>Swapped!</div>', {swapStyle: 'innerHTML'}, {selectOOB: '#oob:innerHTML'});
output.innerHTML.should.be.equal('<div>Swapped!</div>');
oobDiv.innerHTML.should.be.equal('OOB Swapped!');
})
})

View File

@@ -448,6 +448,37 @@ Removes the given extension from htmx
htmx.removeExtension("my-extension");
```
### Method - `htmx.swap()` {#swap}
Performs swapping (and settling) of HTML content
##### Parameters
* `target` - the HTML element or string selector of swap target
* `content` - string representation of content to be swapped
* `swapSpec` - swapping specification, representing parameters from `hx-swap`
* `swapStyle` (required) - swapping style (`innerHTML`, `outerHTML`, `beforebegin` etc)
* `swapDelay`, `settleDelay` (number) - delays before swapping and settling respectively
* `transition` (bool) - whether to use HTML transitions for swap
* `ignoreTitle` (bool) - disables page title updates
* `head` (string) - specifies `head` tag handling strategy (`merge` or `append`). Leave empty to disable head handling
* `scroll`, `scrollTarget`, `show`, `showTarget`, `focusScroll` - specifies scroll handling after swap
* `swapOptions` - additional *optional* parameters for swapping
* `select` - selector for the content to be swapped (equivalent of `hx-select`)
* `selectOOB` - selector for the content to be swapped out-of-band (equivalent of `hx-select-oob`)
* `eventInfo` - an object to be attached to `htmx:afterSwap` and `htmx:afterSettle` elements
* `anchor` - an anchor element that triggered scroll, will be scrolled into view on settle. Provides simple alternative to full scroll handling
* `contextElement` - DOM element that serves as context to swapping operation. Currently used to find extensions enabled for specific element
* `afterSwapCallback`, `afterSettleCallback` - callback functions called after swap and settle respectively. Take no arguments
##### Example
```js
// swap #output element inner HTML with div element with "Swapped!" text
htmx.swap("#output", "<div>Swapped!</div>", {swapStyle: 'innerHTML'});
```
### Method - `htmx.takeClass()` {#takeClass}
Takes the given class from its siblings, so that among its siblings, only the given element will have the class.

View File

@@ -202,6 +202,7 @@ The table below lists all other attributes available in htmx.
| [`htmx.remove()`](@/api.md#remove) | Removes the given element
| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element
| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](@/extensions/_index.md)
| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content
| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element
| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element
| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element