import { DOCUMENT } from '@angular/common'; import { afterNextRender, Component, DestroyRef, inject, signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { fromEvent } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import { AfricaniesIconComponent } from '@africanies/africanies-icons'; import { categoryFragmentId, categoryHeading, categoryMeta, GLOSSARY_CATEGORIES, GLOSSARY_ENTRIES, type GlossaryCategory, glossaryEntriesForCategory, type GlossaryEntry, } from '../lecture/glossary'; import { PageHeaderComponent } from '../shared/page-header.component'; /** * Angular & SDK concept guide — definitions for terms linked from code snippets. */ @Component({ selector: 'app-lecture-page', standalone: true, imports: [RouterLink, PageHeaderComponent, AfricaniesIconComponent], template: `

Category abbreviations

Section labels use short names where docs usually do. Expanded forms are shown in parentheses.

@for (category of GLOSSARY_CATEGORIES; track category) {
{{ categoryHeading(category) }}
{{ categoryMeta(category).description }}
}

On this page

{{ GLOSSARY_ENTRIES.length }} entries · click a term in any implementation snippet to jump to its definition.

@for (entry of GLOSSARY_ENTRIES; track entry.id) { {{ entry.title }} }
@for (category of GLOSSARY_CATEGORIES; track category) {

{{ category }}

{{ categoryHeading(category) }}

{{ categoryMeta(category).description }}

@for (entry of entriesFor(category); track entry.id) {

{{ entry.title }}

{{ entry.summary }}

{{ entry.category }}
@for (paragraph of paragraphs(entry.detail); track paragraph) {

{{ paragraph }}

} @if (entry.example) {
{{ entry.example }}
} @if (entry.links?.length) {
Docs @for (link of entry.links!; track link.href) { {{ link.label }} }
} @if (entry.seeAlso?.length) {
See also @for (relatedId of entry.seeAlso!; track relatedId) { {{ titleFor(relatedId) }} }
}
}
}
`, }) export class LecturePage { protected readonly GLOSSARY_ENTRIES = GLOSSARY_ENTRIES; protected readonly GLOSSARY_CATEGORIES = GLOSSARY_CATEGORIES; protected readonly categoryHeading = categoryHeading; protected readonly categoryMeta = categoryMeta; protected readonly categoryFragmentId = categoryFragmentId; private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly document = inject(DOCUMENT); private readonly destroyRef = inject(DestroyRef); protected readonly activeId = signal(null); protected readonly showScrollTop = signal(false); constructor() { afterNextRender(() => { const view = this.document.defaultView; if (!view) { return; } const syncScrollTop = (): void => { this.showScrollTop.set(view.scrollY > 320); }; syncScrollTop(); fromEvent(view, 'scroll', { passive: true }) .pipe(throttleTime(100), takeUntilDestroyed(this.destroyRef)) .subscribe(() => syncScrollTop()); this.route.fragment.subscribe((fragment) => { this.activeId.set(fragment); if (!fragment) { return; } queueMicrotask(() => { this.document.getElementById(fragment)?.scrollIntoView({ behavior: 'smooth', block: 'start', }); }); }); }); } protected scrollToTop(): void { this.document.defaultView?.scrollTo({ top: 0, behavior: 'smooth' }); this.activeId.set(null); void this.router.navigate([], { relativeTo: this.route, fragment: undefined, replaceUrl: true, }); } protected entriesFor(category: GlossaryCategory): GlossaryEntry[] { return glossaryEntriesForCategory(category); } protected paragraphs(detail: string): string[] { return detail.split(/\n\n+/).filter(Boolean); } protected titleFor(id: string): string { return GLOSSARY_ENTRIES.find((entry) => entry.id === id)?.title ?? id; } }