@ep44/trackable-nav-sections v1.0.1
TrackableSections
Introduction
TrackableSections is small library that aims to help with keeping track of wich content user sees on the screen and keep taht in sync witch yor navigation.
It might be useful for creating Table of Contents or other situations where contents of document or container are very large and scrolling distance is huge.
Install
Accordingly to your package manager.
npm install @ep44/trackable-nav-sectionsYour projet must use angular@17.0.0 or higher.
Usage
Play around with live sample -> https://stackblitz.com/~/github.com/EP-coode/trackable-nav-sections <-
Basic usage
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
TrackSectionDirective,
TrackableSectionService,
} from 'trackable-nav-sections';
@Component({
selector: 'ns-vertical-list-full',
standalone: true,
// Inport TrackSectionDirective direcite
imports: [CommonModule, TrackSectionDirective],
// TrackSectionDirective needs to be provided with TrackableSectionService in scrolling context
providers: [TrackableSectionService],
templateUrl: './vertical-list-full.component.html',
styleUrl: './vertical-list-full.component.scss',
})
export class VerticalListComponentFull {
// Simply inject service and watch to changes in sections
// By default service uses document as scrollin context
private trackableSectionsService = inject(TrackableSectionService);
protected navSections$ = this.trackableSectionsService.watchSections();
}<nav>
<ul>
<!-- You may watch to state of all sections, and render them accordingly to your needs -->
@for(section of navSections$ | async; track section.sectionId){
<li
[style.color]="section.isIntersecting ? 'red' : 'gray'"
[style.opacity]="section.isActive ? '1' : '0.3'"
>
<span>{{ section.sectionDisplayName }}</span>
</li>
}
</ul>
</nav>
<section>
<!-- For each section you must provide unique id. -->
<!-- You may also set custom display name if id of section is not for you a sufficient data. -->
<div tnTrackableSection="section1" tnTrackableSectionDispalyName="S1">SECTION 1</div>
<div tnTrackableSection="section2" tnTrackableSectionDispalyName="S2">SECTION 2</div>
<div tnTrackableSection="section3" tnTrackableSectionDispalyName="S3">SECTION 3</div>
<div>NOT TRACKED SECTION</div>
</section>Effect

Custom scrolling context
import { Component, ElementRef, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
TrackSectionDirective,
TrackableSectionService,
} from 'trackable-nav-sections';
@Component({
selector: 'ns-vertical-list',
standalone: true,
imports: [CommonModule, TrackSectionDirective],
providers: [TrackableSectionService],
templateUrl: './vertical-list.component.html',
styleUrl: './vertical-list.component.scss',
})
export class VerticalListComponent implements OnInit {
private trackableSectionsService = inject(TrackableSectionService);
protected navSections$ = this.trackableSectionsService.watchSections();
private selfRef = inject(ElementRef);
ngOnInit(): void {
// Simply pass reference to element that will handle overflow
this.trackableSectionsService.setScrollingContext(
this.selfRef.nativeElement
);
}
}<nav>
<ul>
@for(section of navSections$ | async; track section.sectionId){
<li
[style.color]="section.isIntersecting ? 'red' : 'gray'"
[style.opacity]="section.isActive ? '1' : '0.3'"
>
<span>{{ section.sectionDisplayName }}</span>
</li>
}
</ul>
</nav>
<!-- Simply reference element that will handle overflow -->
<section #scrolingContext>
<div tnTrackableSection="section1" tnTrackableSectionDispalyName="S1">SECTION 1</div>
<div tnTrackableSection="section2" tnTrackableSectionDispalyName="S2">SECTION 2</div>
<div tnTrackableSection="section3" tnTrackableSectionDispalyName="S3">SECTION 3</div>
<div>NOT TRACKED SECTION</div>
</section>Customization of Intersection detection
import { Component, ElementRef, ViewChild, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
TRACKABLE_SECTION_CONFIG,
TrackSectionDirective,
TrackableSectionService,
} from 'trackable-nav-sections';
@Component({
selector: 'ns-vertical-list-vertical',
standalone: true,
imports: [CommonModule, TrackSectionDirective],
providers: [
TrackableSectionService,
// You may customize behavior of IntersectionObserver used to detect if section is visable to adjust lib for yout needs.
// You may also implement custom strategy of picking active section, by passing activeSrctionPickingStrategy parameter.
// Just use TRACKABLE_SECTION_CONFIG injection token.
{
provide: TRACKABLE_SECTION_CONFIG,
useValue: {
intercectionConfig: {
threshold: 0.3,
},
},
},
],
templateUrl: './horizontal-list.component.html',
styleUrl: './horizontal-list.component.scss',
})
export class HorizontalListComponent {
private trackableSectionsService = inject(TrackableSectionService);
protected navSections$ = this.trackableSectionsService.watchSections();
@ViewChild('scrolingContext', { static: true })
scrolingContext!: ElementRef<HTMLElement>;
ngOnInit(): void {
this.trackableSectionsService.setScrollingContext(
this.scrolingContext.nativeElement
);
}
}Custom active section selection strategy
Default selection filters all sections that fulfil isIntersecting == true predicate.
Then it splits bouding rect-s of all sections into verticies.
Section with verticiy closest to center of scrolling context is picked as active.
You may also implement custom selection strategy simply by implementing comparator of signature:
export type ActiveItemSelectionStrategy = (
s1: TrackableSection,
s2: TrackableSection,
scrollingContext: Element | Window
) => number;License
Copyright © 2023 Ernest Przybył, released under the MIT license.