diff --git a/src/htmx.js b/src/htmx.js index 4c8203ef2..73484e88f 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1455,19 +1455,24 @@ var htmx = (function() { function oobSwap(oobValue, oobElement, settleInfo, rootNode) { rootNode = rootNode || getDocument() let selector = '#' + CSS.escape(getRawAttribute(oobElement, 'id')) - /** @type HtmxSwapStyle */ - let swapStyle = 'outerHTML' - if (oobValue === 'true') { - // do nothing - } else if (oobValue.indexOf(':') > 0) { - swapStyle = oobValue.substring(0, oobValue.indexOf(':')) - selector = oobValue.substring(oobValue.indexOf(':') + 1) + var swapSpec = getSwapSpecification(oobElement, oobValue, {}) + if (swapSpec && Object.keys(swapSpec).length > 1) { + selector = swapSpec.target || selector // if it parses as a full swapSpec and is not a single value then use it } else { - swapStyle = oobValue + const split = oobValue.indexOf(':') // otherwise split as style:selector for legacy support + if (split !== -1) { + swapSpec = { swapStyle: oobValue.substring(0, split) } + selector = oobValue.substring(split + 1) + } else { + swapSpec = { swapStyle: oobValue === 'true' ? 'outerHTML' : oobValue } + } + } + if (WHITESPACE.test(swapSpec.swapStyle)) { + logError('invalid modifier in hx-swap-oob: ' + oobValue) } + oobElement.removeAttribute('hx-swap-oob') oobElement.removeAttribute('data-hx-swap-oob') - const targets = querySelectorAllExt(rootNode, selector, false) if (targets.length) { forEach( @@ -1475,31 +1480,38 @@ var htmx = (function() { function(target) { let fragment const oobElementClone = oobElement.cloneNode(true) - fragment = getDocument().createDocumentFragment() - fragment.appendChild(oobElementClone) - if (!isInlineSwap(swapStyle, target)) { - fragment = asParentNode(oobElementClone) // if this is not an inline swap, we use the content of the node, not the node itself + if (swapSpec.strip === undefined && !isInlineSwap(swapSpec.swapStyle, target)) { + swapSpec.strip = true + } + if (swapSpec.strip) { + // @ts-ignore if elt is template, content will be valid so use as inner content + fragment = oobElementClone.content || asParentNode(oobElementClone) // if this is not an inline swap, we use the content of the node, not the node itself + swapSpec.strip = undefined + } else { + fragment = getDocument().createDocumentFragment() + fragment.appendChild(oobElementClone) } - const beforeSwapDetails = { shouldSwap: true, target, fragment } + const beforeSwapDetails = { shouldSwap: true, target, fragment, swapSpec } if (!triggerEvent(target, 'htmx:oobBeforeSwap', beforeSwapDetails)) return target = beforeSwapDetails.target // allow re-targeting if (beforeSwapDetails.shouldSwap) { - handlePreservedElements(fragment) - swapWithStyle(swapStyle, target, target, fragment, settleInfo) - restorePreservedElements() + swap(target, fragment, beforeSwapDetails.swapSpec, { + contextElement: target, + afterSwapCallback: function(settleInfo) { + forEach(settleInfo.elts, function(elt) { + triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails) + }) + } + }, settleInfo) } - forEach(settleInfo.elts, function(elt) { - triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails) - }) } ) - oobElement.parentNode.removeChild(oobElement) } else { - oobElement.parentNode.removeChild(oobElement) triggerErrorEvent(getDocument().body, 'htmx:oobErrorNoTarget', { content: oobElement }) } + oobElement.parentNode.removeChild(oobElement) return oobValue } @@ -1840,7 +1852,7 @@ var htmx = (function() { } /** - * @param {DocumentFragment} fragment + * @param {DocumentFragment|ParentNode} fragment * @param {HtmxSettleInfo} settleInfo * @param {Node|Document} [rootNode] */ @@ -1864,18 +1876,24 @@ var htmx = (function() { * Implements complete swapping pipeline, including: delay, view transitions, focus and selection preservation, * title updates, scroll, OOB swapping, normal swapping and settling * @param {string|Element} target - * @param {string} content + * @param {string|ParentNode} content * @param {HtmxSwapSpecification} swapSpec * @param {SwapOptions} [swapOptions] + * @param {HtmxSettleInfo} [oobSettleInfo] */ - function swap(target, content, swapSpec, swapOptions) { + function swap(target, content, swapSpec, swapOptions, oobSettleInfo) { if (!swapOptions) { swapOptions = {} } + + addClassToElement(target, htmx.config.swappingClass) + // optional transition API promise callbacks let settleResolve = null let settleReject = null + const isOOBSwap = !!oobSettleInfo // calls passing oobSettleInfo are from oobSwap function and can skip some logic + let doSwap = function() { maybeCall(swapOptions.beforeSwapCallback) @@ -1892,45 +1910,54 @@ var htmx = (function() { // @ts-ignore end: activeElt ? activeElt.selectionEnd : null } - const settleInfo = makeSettleInfo(target) + if (swapSpec.settleDelay !== undefined || swapSpec.swapDelay != undefined) { + oobSettleInfo = undefined // for oobSwaps with swap or settle modifier make and perform own settleInfo + } + const settleInfo = oobSettleInfo || makeSettleInfo(target) // For text content swaps, don't parse the response as HTML, just insert it if (swapSpec.swapStyle === 'textContent') { - target.textContent = content + target.textContent = typeof content === 'string' ? content : content.textContent // Otherwise, make the fragment and process it } else { - let fragment = makeFragment(content) + /** @type DocumentFragment|ParentNode */ + let fragment = typeof content === 'string' ? makeFragment(content) : content + // @ts-ignore if fragment is a ParentNode title will be undefined which is fine settleInfo.title = swapOptions.title || fragment.title if (swapOptions.historyRequest) { - // @ts-ignore fragment can be a parentNode Element fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment } - // select-oob swaps - if (swapOptions.selectOOB) { - const oobSelectValues = swapOptions.selectOOB.split(',') - for (let i = 0; i < oobSelectValues.length; i++) { - const oobSelectValue = oobSelectValues[i].split(':', 2) - let id = oobSelectValue[0].trim() - if (id.indexOf('#') === 0) { - id = id.substring(1) - } - const oobValue = oobSelectValue[1] || 'true' - const oobElement = fragment.querySelector('#' + id) - if (oobElement) { - oobSwap(oobValue, oobElement, settleInfo, rootNode) + if (!isOOBSwap) { + // select-oob swaps + if (swapOptions.selectOOB) { + const oobSelectValues = swapOptions.selectOOB.split(',') + for (let i = 0; i < oobSelectValues.length; i++) { + const oobSelectValue = oobSelectValues[i].split(':') + const selector = oobSelectValue.shift().trim() + const oobValue = oobSelectValue.join(':') || 'true' + let oobElements + if (selector.indexOf('#') !== 0 && /^[a-z\-_](\\\.|[a-z0-9\-_])*$/i.test(selector)) { + oobElements = fragment.querySelectorAll('#' + selector) // check if selector is an id first + } + if (!oobElements || !oobElements.length) { + oobElements = fragment.querySelectorAll(selector) // then support any full selector + } + forEach(oobElements, function(elt) { + oobSwap(oobValue, elt, settleInfo, rootNode) + }) } } + // oob swaps + findAndSwapOobElements(fragment, settleInfo, rootNode) + forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) { + if (template.content && findAndSwapOobElements(template.content, settleInfo, rootNode)) { + // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap + template.remove() + } + }) } - // oob swaps - findAndSwapOobElements(fragment, settleInfo, rootNode) - forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) { - if (template.content && findAndSwapOobElements(template.content, settleInfo, rootNode)) { - // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap - template.remove() - } - }) // normal swap if (swapOptions.select) { @@ -1940,6 +1967,9 @@ var htmx = (function() { }) fragment = newFragment } + if (swapSpec.strip) { + fragment = fragment.firstElementChild + } handlePreservedElements(fragment) swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo) restorePreservedElements() @@ -1965,51 +1995,55 @@ var htmx = (function() { } } - target.classList.remove(htmx.config.swappingClass) + removeClassFromElement(target, htmx.config.swappingClass) forEach(settleInfo.elts, function(elt) { if (elt.classList) { elt.classList.add(htmx.config.settlingClass) } - triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo) + if (!isOOBSwap) { + triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo) + } }) - maybeCall(swapOptions.afterSwapCallback) + if (swapOptions.afterSwapCallback) { + swapOptions.afterSwapCallback(settleInfo) + } // merge in new title after swap but before settle if (!swapSpec.ignoreTitle) { handleTitle(settleInfo.title) } - // settle - const doSettle = function() { - forEach(settleInfo.tasks, function(task) { - task.call() - }) - forEach(settleInfo.elts, function(elt) { - if (elt.classList) { - elt.classList.remove(htmx.config.settlingClass) - } - triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo) - }) + // settle unless this is a oobSwap that settles at the end of its normal swap + if (!oobSettleInfo) { + const doSettle = function() { + forEach(settleInfo.tasks, function(task) { + task.call() + }) + forEach(settleInfo.elts, function(elt) { + removeClassFromElement(elt, htmx.config.settlingClass) + triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo) + }) - if (swapOptions.anchor) { - const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor)) - if (anchorTarget) { - anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' }) + if (swapOptions.anchor) { + const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor)) + if (anchorTarget) { + anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' }) + } } - } - updateScrollState(settleInfo.elts, swapSpec) - maybeCall(swapOptions.afterSettleCallback) - maybeCall(settleResolve) - } + updateScrollState(settleInfo.elts, swapSpec) + maybeCall(swapOptions.afterSettleCallback) + maybeCall(settleResolve) + } - if (swapSpec.settleDelay > 0) { - getWindow().setTimeout(doSettle, swapSpec.settleDelay) - } else { - doSettle() + if (swapSpec.settleDelay > 0) { + getWindow().setTimeout(doSettle, swapSpec.settleDelay) + } else { + doSettle() + } } } - let shouldTransition = htmx.config.globalViewTransitions + let shouldTransition = !isOOBSwap && htmx.config.globalViewTransitions if (swapSpec.hasOwnProperty('transition')) { shouldTransition = swapSpec.transition } @@ -3733,17 +3767,18 @@ var htmx = (function() { /** * @param {Element} elt * @param {HtmxSwapStyle} [swapInfoOverride] + * @param {Object} [defaults] * @returns {HtmxSwapSpecification} */ - function getSwapSpecification(elt, swapInfoOverride) { + function getSwapSpecification(elt, swapInfoOverride, defaults) { const swapInfo = swapInfoOverride || getClosestAttributeValue(elt, 'hx-swap') /** @type HtmxSwapSpecification */ - const swapSpec = { + const swapSpec = defaults || { swapStyle: getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle, swapDelay: htmx.config.defaultSwapDelay, settleDelay: htmx.config.defaultSettleDelay } - if (htmx.config.scrollIntoViewOnBoost && getInternalData(elt).boosted && !isAnchorLink(elt)) { + if (!defaults && htmx.config.scrollIntoViewOnBoost && getInternalData(elt).boosted && !isAnchorLink(elt)) { swapSpec.show = 'top' } if (swapInfo) { @@ -3777,9 +3812,18 @@ var htmx = (function() { } else if (value.indexOf('focus-scroll:') === 0) { const focusScrollVal = value.slice('focus-scroll:'.length) swapSpec.focusScroll = focusScrollVal == 'true' + } else if (value.indexOf('strip:') === 0) { + swapSpec.strip = value.slice(6) === 'true' + } else if (value.indexOf('target:') === 0) { + swapSpec.target = value.slice(7) } else if (i == 0) { swapSpec.swapStyle = value + } else if (swapSpec.target) { + swapSpec.target += (' ' + value) // unfound modifers must be part of target selector } else { + if (defaults) { + return // on invalid modifers allow oob swap to fall back to old style + } logError('Unknown modifier in hx-swap: ' + value) } } @@ -4889,8 +4933,6 @@ var htmx = (function() { swapSpec.ignoreTitle = ignoreTitle } - target.classList.add(htmx.config.swappingClass) - if (responseInfoSelect) { selectOverride = responseInfoSelect } @@ -5138,7 +5180,7 @@ var htmx = (function() { * @property {*} [eventInfo] * @property {string} [anchor] * @property {Element} [contextElement] - * @property {swapCallback} [afterSwapCallback] + * @property {afterSwapCallback} [afterSwapCallback] * @property {swapCallback} [afterSettleCallback] * @property {swapCallback} [beforeSwapCallback] * @property {string} [title] @@ -5149,6 +5191,11 @@ var htmx = (function() { * @callback swapCallback */ +/** + * @callback afterSwapCallback + * @param {HtmxSettleInfo} settleInfo + */ + /** * @typedef {'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string} HtmxSwapStyle */ @@ -5156,8 +5203,8 @@ var htmx = (function() { /** * @typedef HtmxSwapSpecification * @property {HtmxSwapStyle} swapStyle - * @property {number} swapDelay - * @property {number} settleDelay + * @property {number} [swapDelay] + * @property {number} [settleDelay] * @property {boolean} [transition] * @property {boolean} [ignoreTitle] * @property {string} [head] @@ -5166,6 +5213,8 @@ var htmx = (function() { * @property {string} [show] * @property {string} [showTarget] * @property {boolean} [focusScroll] + * @property {boolean} [strip] + * @property {string} [target] */ /** diff --git a/test/attributes/hx-ext.js b/test/attributes/hx-ext.js index adc047fe5..e0214bd89 100644 --- a/test/attributes/hx-ext.js +++ b/test/attributes/hx-ext.js @@ -56,6 +56,17 @@ describe('hx-ext attribute', function() { } } }) + htmx.defineExtension('ext-6', { + isInlineSwap: function(swapStyle) { + return swapStyle === 'morph:outerHTML' + }, + handleSwap: function(swapStyle, target, fragment, settleInfo) { + if (swapStyle === 'morph:outerHTML') { + const swapOuterHTML = htmx._('swapOuterHTML') + swapOuterHTML(target, fragment, settleInfo) + } + } + }) }) afterEach(function() { @@ -66,6 +77,7 @@ describe('hx-ext attribute', function() { htmx.removeExtension('ext-3') htmx.removeExtension('ext-4') htmx.removeExtension('ext-5') + htmx.removeExtension('ext-6') }) it('A simple extension is invoked properly', function() { @@ -195,4 +207,16 @@ describe('hx-ext attribute', function() { this.server.respond() byId('b1').innerHTML.should.equal('Bar') }) + + it('oob swap via swap extension can accept custom swap styles with ":" if it is a complex style', function() { + this.server.respondWith( + 'GET', + '/test', + '
Bar
' + ) + var btn = make('
Foo
') + btn.click() + this.server.respond() + byId('b1').innerHTML.should.equal('Bar') + }) }) diff --git a/test/attributes/hx-select-oob.js b/test/attributes/hx-select-oob.js index 9751b081a..b30f66abb 100644 --- a/test/attributes/hx-select-oob.js +++ b/test/attributes/hx-select-oob.js @@ -45,4 +45,98 @@ describe('hx-select-oob attribute', function() { var div2 = byId('d2') div2.innerHTML.should.equal('') }) + + it('hx-select-oob works with advanced swap style like strip:false and target:', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + }) + + it('hx-select-oob works with basic targeting selector', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + }) + + it('basic hx-select-oob works with just an id without #', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + }) + + it('basic hx-select-oob works with just a non id selector', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + }) + + it('hx-select-oob can end with a blank swap style which is ignored', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + div2.classList.contains('foo').should.equal(true) + }) + + it('basic hx-select-oob works supports non text based selectors', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + div2.classList.contains('foo').should.equal(true) + }) + + it('basic hx-select-oob works with CSS escaped id containing "."', function() { + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('my.div3') + div2.innerHTML.should.equal('bar') + }) + + it('hx-select-oob can select multiple elements with a selector', function() { + this.server.respondWith('GET', '/test', "
foo
bar
baz
") + var div = make('
') + make('
') + make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('
foo
') + var div2 = byId('d2') + div2.innerHTML.should.equal('bar') + div2.classList.contains('foo').should.equal(true) + var div3 = byId('d3') + div3.innerHTML.should.equal('baz') + div3.classList.contains('foo').should.equal(true) + }) }) diff --git a/test/attributes/hx-select.js b/test/attributes/hx-select.js index c9a2df5a1..40daee71a 100644 --- a/test/attributes/hx-select.js +++ b/test/attributes/hx-select.js @@ -34,4 +34,13 @@ describe('BOOTSTRAP - htmx AJAX Tests', function() { this.server.respond() div.innerHTML.should.equal('
foo
') }) + + it('properly handles a select with strip:true', function() { + var i = 1 + this.server.respondWith('GET', '/test', "
foo
bar
") + var div = make('
') + div.click() + this.server.respond() + div.innerHTML.should.equal('foo') + }) }) diff --git a/test/attributes/hx-swap-oob.js b/test/attributes/hx-swap-oob.js index 875c4d452..8a53c73d7 100644 --- a/test/attributes/hx-swap-oob.js +++ b/test/attributes/hx-swap-oob.js @@ -387,4 +387,121 @@ describe('hx-swap-oob attribute', function() { element.innerHTML.should.equal('Swapped11') }) }) + + it('swaps into all targets that match the selector with target: format', function() { + this.server.respondWith('GET', '/test', "
Clicked
Swapped12
") + var div = make('
click me
') + make('
No swap
') + make('
Not swapped
') + make('
Not swapped
') + div.click() + this.server.respond() + byId('d1').innerHTML.should.equal('No swap') + byId('d2').innerHTML.should.equal('Swapped12') + byId('d3').innerHTML.should.equal('Swapped12') + }) + + it('swaps innerHTML including wrapping tag when strip:false', function() { + this.server.respondWith('GET', '/test', "
Clicked
Swapped13
") + var div = make('
click me
') + make('
Not swapped
') + div.click() + this.server.respond() + byId('d1').innerHTML.should.equal('
Swapped13
') + }) + + it('swaps outerHTML excluding wrapping tag when strip:true', function() { + this.server.respondWith('GET', '/test', "
Clicked
Swapped14
Swapped14
") + var div = make('
click me
') + make('
Not swapped
') + div.click() + this.server.respond() + byId('d2').innerHTML.should.equal('Swapped14') + byId('d3').innerHTML.should.equal('Swapped14') + }) + + it('handles using template as the encapsulating tag of an inner swap', function() { + this.server.respondWith('GET', '/test', '') + var div = make('
click me
') + make('
') + div.click() + this.server.respond() + byId('foo').innerHTML.should.equal('Swapped15') + }) + + it('handles taget: that includes spaces', function() { + this.server.respondWith('GET', '/test', 'Swapped15') + var div = make('
click me
') + make('
') + div.click() + this.server.respond() + byId('table').innerHTML.should.equal('Swapped15') + }) + + it('works with a swap delay', function(done) { + this.server.respondWith('GET', '/test', 'Clicked!
delay swapped
') + var div = make("
") + var div2 = make("
") + div.click() + this.server.respond() + div.innerText.should.equal('Clicked!') + div2.innerText.should.equal('') + setTimeout(function() { + div2.innerText.should.equal('delay swapped') + done() + }, 30) + }) + + if (/chrome/i.test(navigator.userAgent)) { + it('works with transition:true', function(done) { + this.server.respondWith('GET', '/test', 'Clicked!
transition swapped
') + var div = make("
") + var div2 = make("
") + div.click() + this.server.respond() + div.innerText.should.equal('Clicked!') + div2.innerText.should.equal('') + setTimeout(function() { + div2.innerText.should.equal('transition swapped') + done() + }, 50) + }) + } + + it('works with a settle delay', function(done) { + this.server.respondWith('GET', '/test', 'Clicked!
swapped
') + var div = make("
") + var div2 = make("
") + div.click() + this.server.respond() + div.innerText.should.equal('Clicked!') + div2.classList.contains('foo').should.equal(false) + setTimeout(function() { + byId('foo').classList.contains('foo').should.equal(true) + done() + }, 30) + }) + + it('handles textContent swap style', function() { + this.server.respondWith('GET', '/test', '

Swapped16

!

') + var div = make('
click me
') + var div2 = make('
') + div.click() + this.server.respond() + div2.innerHTML.should.equal('Swapped16!') + }) + + it('invalid swap modifers with ":" will prevent oob swap and log error', function() { + this.server.respondWith('GET', '/test', '
Swapped17
') + var div = make('
click me
') + var div2 = make('
') + var errorSpy = sinon.spy(console, 'error') + try { + div.click() + this.server.respond() + } catch (e) {} + errorSpy.called.should.equal(true) + div2.innerHTML.should.equal('') + errorSpy.restore() + }) }) diff --git a/test/attributes/hx-swap.js b/test/attributes/hx-swap.js index 9f29791dc..9f7f4e3d1 100644 --- a/test/attributes/hx-swap.js +++ b/test/attributes/hx-swap.js @@ -285,6 +285,12 @@ describe('hx-swap attribute', function() { swapSpec(make("
")).transition.should.equal(true) + swapSpec(make("
")).strip.should.equal(true) + + swapSpec(make("
")).target.should.equal('#table tbody') + + swapSpec(make("
")).target.should.equal('#table tbody') + swapSpec(make("
")).swapStyle.should.equal('customstyle') }) @@ -560,4 +566,15 @@ describe('hx-swap attribute', function() { htmx._('makeSettleInfo = htmx.backupMakeSettleInfo') } }) + + it('swap innerHTML with strip:true properly', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + + var div = make('
') + div.click() + should.equal(byId('d1'), div) + this.server.respond() + div.innerHTML.should.equal('Clicked!') + div.outerHTML.should.equal('
Clicked!
') + }) }) diff --git a/www/content/attributes/hx-select-oob.md b/www/content/attributes/hx-select-oob.md index ac35ba751..be461d2ef 100644 --- a/www/content/attributes/hx-select-oob.md +++ b/www/content/attributes/hx-select-oob.md @@ -27,8 +27,7 @@ This button will issue a `GET` to `/info` and then select the element with the i which will replace the entire button in the DOM, and, in addition, pick out an element with the id `alert` in the response and swap it in for div in the DOM with the same ID. -Each value in the comma separated list of values can specify any valid [`hx-swap`](@/attributes/hx-swap.md) -strategy by separating the selector and the swap strategy with a `:`, with the strategy otherwise defaulting to `outerHTML`. +Each value in the comma-separated list consists of a CSS selector used to locate the elements in the response. Optionally, this can be followed by a colon `:` and any valid [`hx-swap-oob`](@/attributes/hx-swap-oob.md) value; if omitted, the swap strategy defaults to outerHTML. As with `hx-swap-oob`, the target element for the swap will default to the element’s ID, but this can be overridden if needed. For example, to prepend the alert content instead of replacing it: @@ -47,3 +46,4 @@ For example, to prepend the alert content instead of replacing it: ## Notes * `hx-select-oob` is inherited and can be placed on a parent element +* the CSS selector used to locate the elements in the response can not contain a `:` diff --git a/www/content/attributes/hx-swap-oob.md b/www/content/attributes/hx-swap-oob.md index 94393fb32..8741f67a4 100644 --- a/www/content/attributes/hx-swap-oob.md +++ b/www/content/attributes/hx-swap-oob.md @@ -7,7 +7,7 @@ description = """\ +++ The `hx-swap-oob` attribute allows you to specify that some content in a response should be -swapped into the DOM somewhere other than the target, that is "Out of Band". This allows you to piggyback updates to other element updates on a response. +swapped into the DOM somewhere other than the target, that is "Out of Band". This allows you to update multiple elements on a page with a single request. Consider the following response HTML: @@ -23,58 +23,116 @@ Consider the following response HTML: The first div will be swapped into the target the usual manner. The second div, however, will be swapped in as a replacement for the element with the id `alerts`, and will not end up in the target. -The value of the `hx-swap-oob` can be: +### Syntax Options -* `true` -* any valid [`hx-swap`](@/attributes/hx-swap.md) value -* any valid [`hx-swap`](@/attributes/hx-swap.md) value, followed by a colon, followed by a CSS selector +The value of the `hx-swap-oob` attribute can be: + +* `true` - Uses the default `outerHTML` swap strategy with ID-based targeting +* Any valid basic [`hx-swap`](@/attributes/hx-swap.md) strategy (innerHTML, outerHTML, beforebegin, etc) +* Any valid basic [`hx-swap`](@/attributes/hx-swap.md) strategy, followed by a colon, followed by a target CSS selector +* Any valid complex [`hx-swap`](@/attributes/hx-swap.md) value including modifiers like `target:` to set the target of the swap + +### Behavior Details If the value is `true` or `outerHTML` (which are equivalent) the element will be swapped inline. -If a swap value is given, that swap strategy will be used and the encapsulating tag pair will be stripped for all strategies other than `outerHTML`. +If a different swap strategy than `true`/`outerHTML` is supplied the encapsulating tag pair will be stripped so only the inner contents of the element will be used. You can now use `strip:true` modifier to enable tag stripping for `outerHTML` or `strip:false` to disable it for the other strategies when required. + +If a selector is given, all elements matched by that selector will be swapped. If not, the element on the page with an ID matching that of the oob element will be swapped instead. + +### Modifers -If a selector is given, all elements matched by that selector will be swapped. If not, the element with an ID matching the new content will be swapped. +The following modifers from [`hx-swap`](@/attributes/hx-swap.md) can now be included after the swap strategy sperated by spaces: + +* `transition:` - Wrap the oob swap in its own [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) +* `swap:`/`settle:` - Delay the swap or settle time for the oob swap and make its settle independant from the main swap +* `scroll:`/`show:`/`focus-scroll:` - Control the scrolling behaviour of the oob swap +* `strip:` - Override the stripping or not of the oob elements encapsulating tag pair +* `target:` - Set a custom CSS selector to use as the target + +Before modifers were supported the swap strategy value could not contain spaces and the target selector was placed after a colon like `innerHTML:#status`. Now the `hx-swap-oob` value is parsed and checked to see if is a valid swap specification with modifers before using it. If this parsing fails it falls back to the old legacy method and treats the first `:` as a seperator between a swap style and CSS selector. Any invalid or incorrectly spelt modifers will log errors and will either fail or fall back to an innerHTML oob swap. ### Using alternate swap strategies -As mentioned previously when using swap strategies other than `true` or `outerHTML` the encapsulating tags are stripped, as such you need to excapsulate the returned data with the correct tags for the context. +Here are some examples of the various swap strategies: + +```html + +
New notification!
+ + +
Processing complete
+ + +
Processing complete
+ + +
New log entry
+ + +
Updated content
+``` + +### Proper Element Encapsulation + +As mentioned previously when using swap strategies other than `true` or `outerHTML` the encapsulating tags are stripped by default, however you still need to excapsulate the returned data with the correct tags for the content so it can be parsed as valid html. When trying to insert a `` in a table that uses ``: ```html - - ... - + ... ``` A "plain" table: ```html - - - ... - +
+ ...
``` A `
  • ` may be encapsulated in `
      `, `
        `, `
        ` or ``, for example: ```html -
          +
          • ...
          ``` A `

          ` can be encapsulated in `

          ` or ``: ```html - +

          ...

          ``` +You can also now use `template` tag as a universal tag that can encapsulate all tag types: +```html + +``` + +### Overriding Element Encapsulation + +Another new option is the `strip:true` swap modifier that allows you to replace an element with multiple nodes: +```html +
          +
          Replace original
          +
          And add something more
          +
          +``` + +You can also use `strip:false` to allow you to place the oob element itself in various locations +```html + +
          + User Name is already taken! +
          +``` + ### Troublesome Tables and lists -Note that you can use a `template` tag to encapsulate types of elements that, by the HTML spec, can't stand on their own in the -DOM (``, ``, ``, ``, ``, ``, ``, ``, `` & `
        • `). +Note that you can use a `template` tag to encapsulate types of elements that, by the HTML spec, can not be placed adjacent to other normal tags in the DOM (``, ``, ``, ``, ``, ``, ``, ``, `` & `
        • `). Here is an example with an out-of-band swap of a table row being encapsulated in this way: @@ -90,6 +148,7 @@ Here is an example with an out-of-band swap of a table row being encapsulated in ``` Note that these template tags will be removed from the final content of the page. +When the main content node tag is one of the restricted ones you may also need to wrap the oob nodes. ### Slippery SVGs diff --git a/www/content/attributes/hx-swap.md b/www/content/attributes/hx-swap.md index 250b2881b..1e0c6c677 100644 --- a/www/content/attributes/hx-swap.md +++ b/www/content/attributes/hx-swap.md @@ -141,6 +141,22 @@ Alternatively, if you want the page to automatically scroll to the focused eleme hx-swap="outerHTML focus-scroll:false"/> ``` +#### hx-swap-oob: `strip` & `target` + +Designed for use with hx-swap-oob there are two modifers that have been added. `strip:true` allows you to override outerHTML swaps so that it will swap in the inner contents and not the outer tag. It can also be used with normal hx-swaps as well but here it will swap in the inner contents of the first Element in the response content but any later Elements will be lost. + +```html + +
          + Get Some Content +
          +``` + +`strip:false` is used for hx-swap-oob inner style swap strategies to allow them to swap in the encapsulating tag. + +`target:` is only usable with hx-swap-oob and not normal hx-swaps and it allows you to set a custom target selector to replace during the oob swap. + ## Notes * `hx-swap` is inherited and can be placed on a parent element diff --git a/www/content/events.md b/www/content/events.md index 47aa460fa..a1ddf3c76 100644 --- a/www/content/events.md +++ b/www/content/events.md @@ -375,6 +375,7 @@ This event is triggered as part of an [out of band swap](@/docs.md#oob_swaps) an * `detail.shouldSwap` - if the content will be swapped (defaults to `true`) * `detail.target` - the target of the swap * `detail.fragment` - the response fragment +* `detail.swapSpec` - the swapSpec to be used containing the swapStyle ### Event - `htmx:oobErrorNoTarget` {#htmx:oobErrorNoTarget}