Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/ext/hx-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,8 @@
if (p === Symbol.iterator) return () => elts.values();
if (p === 'q') return s => {
let out = new Set();
for (let e of elts) for (let r of makeQ(e, e)(s).arr()) out.add(r);
let isPositional = /^(next|previous)\s/.test(s);
for (let e of elts) for (let r of makeQ(e, isPositional ? e.parentElement : e)(s).arr()) out.add(r);
return qProxy([...out]);
};
if (p === 'trigger') return (t, d, b) => { elts.forEach(e => htmx.trigger(e, t, d, b)); return proxy; };
Expand Down
114 changes: 114 additions & 0 deletions test/tests/ext/hx-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,120 @@ describe('hx-live extension', function () {
proxy.count.should.equal(1);
});

// -------------------------------------------------------------------------
// chained q: directional scoping (option 4 behaviour)
// -------------------------------------------------------------------------

it('chained q: next finds sibling within same parent', function() {
playground().innerHTML = `
<div id="parent">
<div class="el">One</div>
<div class="el selected">Two</div>
<div class="el">Three</div>
<div class="el">Four</div>
</div>
`;
let proxy = htmx.live.q('#parent .el.selected').q('next .el');
proxy.count.should.equal(1);
proxy.textContent.should.equal('Three');
});

it('chained q: previous finds sibling within same parent', function() {
playground().innerHTML = `
<div id="parent">
<div class="el">One</div>
<div class="el selected">Two</div>
<div class="el">Three</div>
<div class="el">Four</div>
</div>
`;
let proxy = htmx.live.q('#parent .el.selected').q('previous .el');
proxy.count.should.equal(1);
proxy.textContent.should.equal('One');
});

it('chained q: next returns empty when anchor is last sibling', function() {
playground().innerHTML = `
<div id="parent">
<div class="el">One</div>
<div class="el">Two</div>
<div class="el last">Three</div>
</div>
`;
htmx.live.q('#parent .el.last').q('next .el').count.should.equal(0);
});

it('chained q: previous returns empty when anchor is first sibling', function() {
playground().innerHTML = `
<div id="parent">
<div class="el first">One</div>
<div class="el">Two</div>
<div class="el">Three</div>
</div>
`;
htmx.live.q('#parent .el.first').q('previous .el').count.should.equal(0);
});

it('chained q: next does not escape to a different parent', function() {
// Two groups in separate parents. next .item from #s1 searches within
// #s1's parent (#group1) only — cannot reach .item inside #group2.
playground().innerHTML = `
<div id="group1">
<section id="s1"></section>
</div>
<div id="group2">
<div class="item">B</div>
</div>
`;
let proxy = htmx.live.q('#s1').q('next .item');
proxy.count.should.equal(0);
});

it('chained q: first scopes inside each matched element', function() {
playground().innerHTML = `
<section><span class="i">a</span><span class="i">b</span></section>
<section><span class="i">c</span><span class="i">d</span></section>
`;
let proxy = htmx.live.q('section').q('first .i');
proxy.count.should.equal(2);
proxy.arr().map(e => e.textContent).should.deep.equal(['a', 'c']);
});

it('chained q: last scopes inside each matched element', function() {
playground().innerHTML = `
<section><span class="i">a</span><span class="i">b</span></section>
<section><span class="i">c</span><span class="i">d</span></section>
`;
let proxy = htmx.live.q('section').q('last .i');
proxy.count.should.equal(2);
proxy.arr().map(e => e.textContent).should.deep.equal(['b', 'd']);
});

it('chained q: first with in this also scopes inside each element', function() {
playground().innerHTML = `
<section><span class="i">a</span><span class="i">b</span></section>
<section><span class="i">c</span><span class="i">d</span></section>
`;
// explicit "in this" should give the same result as the default scoped behaviour
let proxy = htmx.live.q('section').q('first .i in this');
proxy.count.should.equal(2);
proxy.arr().map(e => e.textContent).should.deep.equal(['a', 'c']);
});

it('chained q: next across multiple rows returns each successor', function() {
playground().innerHTML = `
<div id="rows">
<div class="row">Row 1</div>
<div class="row">Row 2</div>
<div class="row">Row 3</div>
</div>
`;
// 3 rows: row1→row2, row2→row3, row3→nothing. Deduped result = row2, row3.
let proxy = htmx.live.q('#rows .row').q('next .row');
proxy.count.should.equal(2);
proxy.arr().map(e => e.textContent.trim()).should.deep.equal(['Row 2', 'Row 3']);
});

it('q is available in hx-on scope and bound to element', function() {
playground().innerHTML = '<button hx-on:click="window.fooLive = q(\'next #target\').textContent">x</button><div id="target">tgt</div>';
htmx.process(playground());
Expand Down
25 changes: 20 additions & 5 deletions www/src/content/extensions/06-hx-live.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ q('button').click()

```js
q('first .foo') // first match in document order
q('last .foo') // last match
q('last .foo') // last match in document order
q('next .foo') // first match after this element
q('previous .foo') // closest match before this element
q('closest .foo') // nearest ancestor matching .foo
Expand All @@ -332,14 +332,29 @@ q('.foo in this') // restrict to the current element

`next`, `previous`, and `closest` resolve against `this`. They require an expression scope with a current element. For an ancestor lookup that works anywhere, use [`closest(selector)`](#closestselector).

**Chaining**
**Chaining**

`.q(...)` on a proxy re-runs the grammar with each element as the anchor:
`.q(...)` on a proxy re-runs the grammar with each element as the positional anchor.

`first` and `last` scope inside each matched element:

```js
q('section').q('first .item') // first .item inside each section
q('section').q('last .item') // last .item inside each section
```

`next` and `previous` search within the matched element's parent, so they find siblings and their descendants but cannot reach elements under a different parent:

```js
q('.row').q('next .row') // each row's next sibling row
q('#parent .el.selected').q('next .el') // next .el after the selected one
q('#parent .el.selected').q('previous .el') // previous .el before the selected one
```

`closest` finds the nearest ancestor of each matched element:

```js
q('.error').q('closest .field') // surrounding .field of each .error
q('section').q('first .item') // first .item per section
q('.row').q('next .row') // each row's successor
```

For plain descendant queries, CSS is shorter: `q('.card .title')` and `q('.card').q('.title')` are equivalent. Use chaining when you need a directional per matched element.
Expand Down
Loading