Skip to content

Commit 6c1af2c

Browse files
intersect from modifier support (#4080)
* intersect from modifier support * fix hx-on tests
1 parent 60ace48 commit 6c1af2c

5 files changed

Lines changed: 52 additions & 26 deletions

File tree

‎src/htmx.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -815,14 +815,14 @@ var htmx = (() => {
815815
spec.observer = new IntersectionObserver((entries) => {
816816
for (let i = 0; i < entries.length; i++) {
817817
if (entries[i].isIntersecting) {
818-
this.trigger(elt, 'intersect', {}, false);
818+
this.trigger(entries[i].target, 'intersect', {}, false);
819819
if (isRevealed) spec.observer.disconnect();
820820
break;
821821
}
822822
}
823823
}, observerOptions);
824824
eventName = 'intersect';
825-
spec.observer.observe(elt);
825+
for (let fromElt of fromElts) spec.observer.observe(fromElt);
826826
}
827827

828828
// Every: set up interval

‎test/tests/attributes/hx-on.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ describe('hx-on="eventSpec -> code" syntax', function() {
583583
};
584584
let div = createProcessedHTML('<div hx-on="revealed -> this.classList.add(\'visible\')">x</div>');
585585
div.classList.contains('visible').should.equal(false);
586-
observerCallback([{ isIntersecting: true }]);
586+
observerCallback([{ isIntersecting: true, target: div }]);
587587
div.classList.contains('visible').should.equal(true);
588588
window.IntersectionObserver = originalIO;
589589
});
@@ -597,7 +597,7 @@ describe('hx-on="eventSpec -> code" syntax', function() {
597597
};
598598
let div = createProcessedHTML('<div hx-on="intersect once -> this.classList.add(\'in-view\')">x</div>');
599599
div.classList.contains('in-view').should.equal(false);
600-
observerCallback([{ isIntersecting: true }]);
600+
observerCallback([{ isIntersecting: true, target: div }]);
601601
div.classList.contains('in-view').should.equal(true);
602602
window.IntersectionObserver = originalIO;
603603
});

‎test/tests/unit/__initializeTriggers.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,30 @@ describe('__initializeTriggers unit tests', function() {
254254
assert.isUndefined(captured[0].root);
255255
assert.isUndefined(captured[0].threshold);
256256
});
257+
258+
it('from: modifier observes the from element, not the trigger element', function() {
259+
let observed = [];
260+
const origIO = window.IntersectionObserver;
261+
window.IntersectionObserver = function(cb, opts) {
262+
const io = new origIO(cb, opts);
263+
const origObserve = io.observe.bind(io);
264+
io.observe = (el) => { observed.push(el); origObserve(el); };
265+
return io;
266+
};
267+
playground().innerHTML = '<div id="scroller3"><div id="sentinel" hx-action="js:null" hx-trigger="intersect from:#scroller3">Test</div></div>';
268+
htmx.process(playground());
269+
window.IntersectionObserver = originalIO;
270+
assert.equal(observed.length, 1);
271+
assert.equal(observed[0], document.getElementById('scroller3'));
272+
assert.notEqual(observed[0], document.getElementById('sentinel'));
273+
});
274+
275+
it('from: modifier with threshold passes both to IntersectionObserver', function() {
276+
playground().innerHTML = '<div id="scroller4"><div id="target4" hx-action="js:null" hx-trigger="intersect from:#scroller4 threshold:0.75">Test</div></div>';
277+
htmx.process(playground());
278+
assert.equal(captured.length, 1);
279+
assert.equal(captured[0].threshold, 0.75);
280+
});
257281
});
258282

259283
});

‎www/src/content/reference/01-attributes/07-hx-trigger.md‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,30 +62,27 @@ Fires when the element is loaded into the DOM. Useful for [lazy-loading](/patter
6262
<div hx-trigger="load" hx-get="...">Loading...</div>
6363
```
6464

65-
### `revealed`
65+
### `intersect` / `revealed`
6666

67-
Fires when the element is scrolled into the viewport. Useful for [infinite scroll](/patterns/infinite-scroll).
67+
Both use the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options) to fire when an element enters the viewport. The only difference is that `revealed` automatically disconnects the observer after the first intersection — it fires exactly once.
6868

6969
```html
70+
<!-- fires every time the element intersects -->
71+
<div hx-trigger="intersect" hx-get="...">...</div>
72+
73+
<!-- fires once, then stops observing -->
7074
<div hx-trigger="revealed" hx-get="...">Loading...</div>
7175
```
7276

73-
_Note: `revealed` always observes the browser viewport. For scrollable containers with `overflow`, use [`intersect`](#intersect) with `root` instead._
74-
75-
### `intersect`
76-
77-
Fires when an element becomes visible in the viewport.
78-
79-
Uses the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#options).
80-
81-
Modifiers:
77+
Modifiers (`intersect` and `revealed` both support these):
8278

83-
- [`root`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)
84-
- [`rootMargin`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
85-
- [`threshold`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds)
79+
- `from:<selector>` — observe a different element instead of the element itself
80+
- [`root:<selector>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) — use a scrollable container as the intersection root instead of the viewport
81+
- [`rootMargin:<value>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) — expand or shrink the root bounds
82+
- [`threshold:<0–1>`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds) — minimum visible fraction before firing
8683

8784
```html
88-
<div hx-trigger="intersect once" hx-get="...">...</div>
85+
<div hx-trigger="intersect from:#other" hx-get="...">...</div>
8986
<div hx-trigger="intersect root:#scroll-container" hx-get="...">...</div>
9087
<div hx-trigger="intersect rootMargin:100px" hx-get="...">...</div>
9188
<div hx-trigger="intersect threshold:0.5" hx-get="...">...</div>

‎www/src/content/reference/03-events/33-intersect.md‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ title: "intersect"
33
description: "Fires when element enters viewport"
44
---
55

6-
The `intersect` trigger event fires when an element enters the viewport, detected via IntersectionObserver.
6+
The `intersect` event fires when an observed element enters the viewport, detected via IntersectionObserver. It is dispatched on the observed element.
7+
8+
`revealed` is identical except it disconnects the observer after the first intersection, so it fires exactly once.
79

810
## When It Fires
911

10-
When an element becomes visible in the viewport, used by [`hx-trigger`](/reference/attributes/hx-trigger)`="intersect"`.
12+
Used by [`hx-trigger`](/reference/attributes/hx-trigger)`="intersect"` and `hx-trigger="revealed"`. Also fires when `from:<selector>` is used on either — the event is dispatched on the `from` element when it intersects.
1113

1214
## Event Detail
1315

@@ -16,15 +18,18 @@ Empty - no additional context provided.
1618
## Example
1719

1820
```html
19-
<div id="lazy" hx-get="/lazy-content" hx-trigger="intersect">
20-
Content loads when scrolled into view
21-
</div>
21+
<!-- fires every time element intersects -->
22+
<div hx-get="/lazy-content" hx-trigger="intersect">...</div>
23+
24+
<!-- fires once -->
25+
<div hx-get="/lazy-content" hx-trigger="revealed">Loading...</div>
26+
27+
<!-- observe a different element -->
28+
<div hx-get="/more" hx-trigger="intersect from:#sentinel">...</div>
2229
```
2330

2431
```javascript
2532
htmx.on('#lazy', 'intersect', (evt) => {
2633
console.log('Element visible:', evt.target);
2734
});
2835
```
29-
30-
This enables lazy loading and infinite scroll patterns.

0 commit comments

Comments
 (0)