Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ext/hx-targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
let selector = api.attributeValue(ctx.sourceElement, 'hx-targets');
if (!selector) return;

let targets = htmx.findAll(ctx.sourceElement, selector);
let targets = api.findAllExt(ctx.sourceElement, selector);
if (!targets.length) {
console.warn(`htmx: '${selector}' on hx-targets did not match any elements`, { selector });
return;
Expand Down
10 changes: 6 additions & 4 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ var htmx = (() => {
this.#internalAPI = {
HCON,
attributeValue: this.__attributeValue.bind(this),
findAllExt: this.__findAllExt.bind(this),
parseTriggerSpecs: this.__parseTriggerSpecs.bind(this),
determineMethodAndAction: this.__determineMethodAndAction.bind(this),
createRequestContext: this.__createRequestContext.bind(this),
Expand Down Expand Up @@ -1568,11 +1569,12 @@ var htmx = (() => {
}

find(selectorOrElt, selector) {
return this.__findExt(selectorOrElt, selector)
return this.findAll(selectorOrElt, selector)[0]
}

findAll(selectorOrElt, selector) {
return this.__findAllExt(selectorOrElt, selector)
// With an element, plain CSS selectors only match within it, as in htmx 2
return this.__findAllExt(selectorOrElt, selector, undefined, false, !!selector)
}

parseInterval(str) {
Expand Down Expand Up @@ -1897,7 +1899,7 @@ var htmx = (() => {
return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
}

__findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
__findAllExt(eltOrSelector, maybeSelector, thisAttr, global, scoped) {
let selector = maybeSelector ?? eltOrSelector;
let elt = maybeSelector ? (this.__normalizeElement(eltOrSelector) || document.body) : document;
if (selector.startsWith('global ')) {
Expand Down Expand Up @@ -1948,7 +1950,7 @@ var htmx = (() => {

if (unprocessedParts.length > 0) {
let standardSelector = unprocessedParts.join(',')
let rootNode = this.__getRootNode(elt, !!global)
let rootNode = scoped ? elt : this.__getRootNode(elt, !!global)
result.push(...rootNode.querySelectorAll(standardSelector))
}

Expand Down
1 change: 1 addition & 0 deletions src/skills/htmx-extension-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ init: (internalAPI) => { api = internalAPI; },
|----------|-------------|
| `api.HCON` | Parse and merge HCON values |
| `api.attributeValue(elt, name, defaultVal, returnElt)` | Get an attribute value with inheritance support |
| `api.findAllExt(elt, selector, thisAttr)` | Find elements with an extended selector, resolved like `hx-target` |
| `api.parseTriggerSpecs(spec)` | Parse a trigger specification |
| `api.determineMethodAndAction(elt, evt)` | Get `{method, action}` for an element |
| `api.createRequestContext(elt, evt)` | Create a request context |
Expand Down
60 changes: 60 additions & 0 deletions test/tests/ext/hx-targets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
describe('hx-targets extension', function() {

let extBackup;
let configExtensions;

before(async () => {
extBackup = backupExtensions();
configExtensions = htmx.config.extensions;
clearExtensions();

htmx.config.extensions = 'hx-targets';
htmx.__approvedExt = 'hx-targets';

let script = document.createElement('script');
script.src = '../src/ext/hx-targets.js';
await new Promise(resolve => {
script.onload = resolve;
document.head.appendChild(script);
});

if (!htmx.__registeredExt.has('hx-targets')) {
throw new Error('hx-targets extension failed to register - check approval');
}
});

after(() => {
restoreExtensions(extBackup);
htmx.config.extensions = configExtensions;
});

beforeEach(function() {
setupTest();
});

afterEach(function() {
cleanupTest();
});

it('swaps into every element matching a CSS selector outside the source element', async function() {
mockResponse('GET', '/test', 'Updated')
createProcessedHTML('<button id="btn" hx-get="/test" hx-targets=".card">Click</button><div class="card">A</div><div class="card">B</div>');
find('#btn').click()
await forRequest()
let cards = playground().querySelectorAll('.card')
assert.equal(cards.length, 2)
assert.equal(cards[0].textContent, 'Updated')
assert.equal(cards[1].textContent, 'Updated')
assert.equal(find('#btn').textContent, 'Click')
})

it('supports extended selectors', async function() {
mockResponse('GET', '/test', 'Updated')
createProcessedHTML('<div class="card"><button id="btn" hx-get="/test" hx-targets="closest .card">Click</button></div><div class="card">B</div>');
find('#btn').click()
await forRequest()
let cards = playground().querySelectorAll('.card')
assert.equal(cards[0].textContent, 'Updated')
assert.equal(cards[1].textContent, 'B')
})
});
22 changes: 22 additions & 0 deletions test/tests/unit/__findAllExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,26 @@ describe('__findAllExt unit tests', function() {
assert.equal(results.length, 2);
})

it('htmx.findAll(elt, selector) only searches within elt', function() {
createProcessedHTML('<div id="a"><span class="x"></span></div><div id="b"><span class="x"></span></div>');
let results = htmx.findAll(document.getElementById('b'), '.x');
assert.equal(results.length, 1);
assert.equal(results[0].parentElement.id, 'b');
assert.equal(htmx.findAll(document.getElementById('a').firstChild, '.x').length, 0);
})

it('htmx.find(elt, selector) only searches within elt', function() {
createProcessedHTML('<div id="a"><span class="x"></span></div><div id="b"></div>');
assert.isTrue(htmx.find(document.getElementById('b'), '.x') === undefined);
assert.equal(htmx.find(document.getElementById('a'), '.x').parentElement.id, 'a');
})

it('htmx.findAll(elt, selector) keeps extended selectors relative to elt', function() {
createProcessedHTML('<div class="parent"><div id="a"></div><div class="target"></div></div><div class="target"></div>');
let a = document.getElementById('a');
assert.equal(htmx.findAll(a, 'closest .parent')[0].className, 'parent');
assert.equal(htmx.find(a, 'next .target').parentElement.className, 'parent');
assert.equal(htmx.findAll(a, 'global .target').length, 2);
})

});
1 change: 1 addition & 0 deletions www/src/content/docs/extension-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Available internal API:
<!-- check_extension_api:start -->
- `HCON` - Parse and merge HCON values
- `attributeValue(elt, name, defaultVal, returnElt)` - Get an attribute value with inheritance support
- `findAllExt(elt, selector, thisAttr)` - Find elements with an extended selector, resolved like `hx-target`
- `parseTriggerSpecs(spec)` - Parse a trigger specification
- `determineMethodAndAction(elt, evt)` - Get `{method, action}` for an element
- `createRequestContext(elt, evt)` - Create a request context
Expand Down