diff --git a/src/cdk/scrolling/scrolling.md b/src/cdk/scrolling/scrolling.md index a1bec9184880..41cee4137d40 100644 --- a/src/cdk/scrolling/scrolling.md +++ b/src/cdk/scrolling/scrolling.md @@ -9,6 +9,55 @@ This marks the element as a `Scrollable` and registers it with the `ScrollDispat dispatcher, then, allows components to share both event listeners and knowledge of all of the scrollable containers in the application. +#### Reacting to scroll events +`CdkScrollable` exposes an `elementScrolled` stream that emits whenever its element is scrolled. +Use it together with `measureScrollOffset`, which returns the scroll position relative to a given +edge of the element, to respond to the user scrolling. The stream completes when the directive is +destroyed. + + + +To react to scrolling in any of the scrollable containers in the application, inject the +`ScrollDispatcher` and subscribe to `scrolled`. It emits the `CdkScrollable` that was scrolled, or +`undefined` when the page itself is scrolled. If you only care about the containers around a +particular element, use `ancestorScrolled` instead, which only emits for those containers and the +page. `ancestorScrolled` looks up the containers at the time it is called, so call it in `ngOnInit` +or later, once the containers have been registered. + +Both streams are throttled to one event every 20 milliseconds by default. You can change this by +passing a different time in milliseconds. Unsubscribe once you no longer need the events, because +the dispatcher keeps a global scroll listener as long as the stream has subscribers. + +```ts +import {Component, DestroyRef, ElementRef, OnInit, inject} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {ScrollDispatcher} from '@angular/cdk/scrolling'; + +@Component({ + selector: 'my-component', + template: '...', +}) +export class MyComponent implements OnInit { + private _scrollDispatcher = inject(ScrollDispatcher); + private _elementRef = inject(ElementRef); + private _destroyRef = inject(DestroyRef); + + ngOnInit() { + this._scrollDispatcher + .ancestorScrolled(this._elementRef) + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(() => { + // Respond to the scroll event. + }); + } +} +``` + +Scroll events are emitted outside of the Angular zone to avoid running change detection on every +scroll. Updating a class property in response to a scroll event won't refresh the view on its own, +so store the values that your template uses in signals, or call `ChangeDetectorRef.markForCheck` +after updating them. + ### ViewportRuler The `ViewportRuler` is a service that can be injected and used to measure the bounds of the browser viewport. diff --git a/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.css b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.css new file mode 100644 index 000000000000..9b1c879a6827 --- /dev/null +++ b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.css @@ -0,0 +1,11 @@ +.example-container { + height: 200px; + width: 200px; + margin-bottom: 8px; + overflow: auto; + border: 1px solid black; +} + +.example-item { + height: 50px; +} diff --git a/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.html b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.html new file mode 100644 index 000000000000..7e07f59067b4 --- /dev/null +++ b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.html @@ -0,0 +1,11 @@ +

Scrolled {{scrollOffset()}}px from the top

+ +
+ @for (item of items; track item) { +
{{item}}
+ } +
+ + diff --git a/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.ts b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.ts new file mode 100644 index 000000000000..4bf610b3db1d --- /dev/null +++ b/src/components-examples/cdk/scrolling/cdk-scrollable-overview/cdk-scrollable-overview-example.ts @@ -0,0 +1,29 @@ +import {AfterViewInit, Component, signal, viewChild} from '@angular/core'; +import {CdkScrollable} from '@angular/cdk/scrolling'; + +/** @title Reacting to scroll events */ +@Component({ + selector: 'cdk-scrollable-overview-example', + templateUrl: 'cdk-scrollable-overview-example.html', + styleUrl: 'cdk-scrollable-overview-example.css', + imports: [CdkScrollable], +}) +export class CdkScrollableOverviewExample implements AfterViewInit { + scrollable = viewChild.required(CdkScrollable); + items = Array.from({length: 50}).map((_, i) => `Item #${i}`); + scrollOffset = signal(0); + + ngAfterViewInit() { + const scrollable = this.scrollable(); + + // Scroll events are emitted outside of the Angular zone. Updating a signal + // lets Angular know that the view needs to be refreshed. + scrollable.elementScrolled().subscribe(() => { + this.scrollOffset.set(Math.round(scrollable.measureScrollOffset('top'))); + }); + } + + scrollToTop() { + this.scrollable().scrollTo({top: 0}); + } +} diff --git a/src/components-examples/cdk/scrolling/index.ts b/src/components-examples/cdk/scrolling/index.ts index 59e342198e5b..e65c7f3f804d 100644 --- a/src/components-examples/cdk/scrolling/index.ts +++ b/src/components-examples/cdk/scrolling/index.ts @@ -1,3 +1,4 @@ +export {CdkScrollableOverviewExample} from './cdk-scrollable-overview/cdk-scrollable-overview-example'; export {CdkVirtualScrollAppendOnlyExample} from './cdk-virtual-scroll-append-only/cdk-virtual-scroll-append-only-example'; export {CdkVirtualScrollContextExample} from './cdk-virtual-scroll-context/cdk-virtual-scroll-context-example'; export {CdkVirtualScrollCustomStrategyExample} from './cdk-virtual-scroll-custom-strategy/cdk-virtual-scroll-custom-strategy-example';