Documentation Index
Fetch the complete documentation index at: https://mintlify.com/adelpro/quran-search-engine/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates a standalone Angular application using the Quran Search Engine library.
Features
- Standalone Angular component (no NgModule required)
- Reactive search with debouncing
- Lemma, root, and fuzzy search options
- Highlighted results by match type
- Pagination controls
- Type-safe implementation
Setup
Start the development server
From the workspace root:pnpm -C examples/angular start
Or from the example directory:cd examples/angular
pnpm start
Angular uses start instead of dev for the development server.
Open your browser
Navigate to http://localhost:4200
Project structure
examples/angular/
├── src/
│ ├── app/
│ │ └── app.component.ts # Main standalone component
│ ├── main.ts # Bootstrap
│ └── index.html
├── package.json
└── angular.json
Component implementation
The example uses a standalone component with inline template and styles:
import { CommonModule } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
getHighlightRanges,
loadMorphology,
loadQuranData,
loadWordMap,
search,
type AdvancedSearchOptions,
type MatchType,
type MorphologyAya,
type QuranText,
type SearchResponse,
type WordMap,
} from 'quran-search-engine';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<!-- Template content -->
`,
styles: [`
/* Component styles */
`],
})
export class AppComponent implements OnInit, OnDestroy {
// Component implementation
}
This example uses Angular 18+ standalone components, eliminating the need for NgModule declarations.
Loading data on initialization
Load all datasets when the component initializes:
type LoadState = 'idle' | 'loading' | 'ready' | 'error';
export class AppComponent implements OnInit {
loadState: LoadState = 'idle';
errorMessage = '';
private quranData: QuranText[] | null = null;
private morphologyMap: Map<number, MorphologyAya> | null = null;
private wordMap: WordMap | null = null;
async ngOnInit(): Promise<void> {
this.loadState = 'loading';
this.errorMessage = '';
try {
const [quranData, morphologyMap, wordMap] = await Promise.all([
loadQuranData(),
loadMorphology(),
loadWordMap(),
]);
this.quranData = quranData;
this.morphologyMap = morphologyMap;
this.wordMap = wordMap;
this.loadState = 'ready';
} catch (err: unknown) {
this.loadState = 'error';
this.errorMessage = err instanceof Error ? err.message : 'Failed to load datasets.';
}
}
}
Search with debouncing
Implement debounced search triggered by input changes:
export class AppComponent implements OnDestroy {
query = '';
page = 1;
limit = 20;
options = { lemma: true, root: true, fuzzy: true };
response: SearchResponse<QuranText> | null = null;
private debounceHandle: number | null = null;
ngOnDestroy(): void {
if (this.debounceHandle !== null) {
window.clearTimeout(this.debounceHandle);
}
}
onQueryChange(): void {
if (this.debounceHandle !== null) {
window.clearTimeout(this.debounceHandle);
}
this.debounceHandle = window.setTimeout(() => {
this.runSearch(true);
}, 250);
}
runSearch(resetPage: boolean): void {
if (!this.quranData || !this.morphologyMap || !this.wordMap) return;
const trimmed = this.query.trim();
if (!trimmed) {
this.response = null;
return;
}
if (resetPage) this.page = 1;
const searchOptions: AdvancedSearchOptions = {
lemma: this.options.lemma,
root: this.options.root,
fuzzy: this.options.fuzzy,
};
this.response = search(
trimmed,
this.quranData,
this.morphologyMap,
this.wordMap,
searchOptions,
{ page: this.page, limit: this.limit },
);
}
}
Template with search controls
Create a reactive search interface:
app.component.ts (template)
<main class="page">
<header class="header">
<h1 class="title">Quran Search Engine</h1>
<p class="subtitle">Angular example</p>
</header>
<section class="panel" aria-label="Search controls">
<label class="label" for="query">Arabic query</label>
<div class="row">
<input
class="input"
id="query"
type="text"
[disabled]="loadState !== 'ready'"
[(ngModel)]="query"
(ngModelChange)="onQueryChange()"
placeholder="مثال: الرحمن"
/>
<button
class="button"
type="button"
(click)="runSearch(true)"
[disabled]="loadState !== 'ready'"
>
Search
</button>
</div>
<fieldset class="fieldset" [disabled]="loadState !== 'ready'">
<legend class="legend">Options</legend>
<label class="check">
<input type="checkbox" [(ngModel)]="options.lemma" (ngModelChange)="runSearch(true)" />
Lemma
</label>
<label class="check">
<input type="checkbox" [(ngModel)]="options.root" (ngModelChange)="runSearch(true)" />
Root
</label>
<label class="check">
<input type="checkbox" [(ngModel)]="options.fuzzy" (ngModelChange)="runSearch(true)" />
Fuzzy
</label>
</fieldset>
</section>
</main>
Highlighting implementation
Implement highlight rendering with caching for performance:
type HighlightPart = { text: string; matchType: MatchType | null };
export class AppComponent {
private uthmaniHighlightPartsByGid = new Map<number, readonly HighlightPart[]>();
trackByGid(_: number, verse: QuranText): number {
return verse.gid;
}
getUthmaniParts(verse: ScoredVerse<QuranText>): readonly HighlightPart[] {
return (
this.uthmaniHighlightPartsByGid.get(verse.gid) ?? [{ text: verse.uthmani, matchType: null }]
);
}
private rebuildHighlightCache(): void {
this.uthmaniHighlightPartsByGid.clear();
if (!this.response) return;
for (const verse of this.response.results) {
const parts = this.buildHighlightParts(verse.uthmani, verse.matchedTokens, verse.tokenTypes);
this.uthmaniHighlightPartsByGid.set(verse.gid, parts);
}
}
private buildHighlightParts(
text: string,
matchedTokens: readonly string[],
tokenTypes?: Record<string, MatchType>,
): readonly HighlightPart[] {
const ranges = getHighlightRanges(text, matchedTokens, tokenTypes);
if (ranges.length === 0) return [{ text, matchType: null }];
const parts: HighlightPart[] = [];
let cursor = 0;
for (const range of ranges) {
if (cursor < range.start) {
parts.push({ text: text.slice(cursor, range.start), matchType: null });
}
parts.push({ text: text.slice(range.start, range.end), matchType: range.matchType });
cursor = range.end;
}
if (cursor < text.length) {
parts.push({ text: text.slice(cursor), matchType: null });
}
return parts;
}
}
The example caches highlight parts by verse gid to avoid recalculating them on every change detection cycle.
Display results with highlighting
Render highlighted verses in the template:
app.component.ts (template)
<ol class="list">
<li *ngFor="let verse of response.results; trackBy: trackByGid" class="item">
<div class="itemHead">
<span class="badge" [attr.data-type]="verse.matchType">{{ verse.matchType }}</span>
<span class="ref">{{ verse.sura_name_en }} • {{ verse.aya_id_display }}</span>
<span class="score">score {{ verse.matchScore }}</span>
</div>
<p class="text arabic" dir="rtl">
<span
*ngFor="let part of getUthmaniParts(verse)"
[class]="part.matchType ? 'highlight-' + part.matchType : ''"
>
{{ part.text }}
</span>
</p>
<p class="text muted">{{ verse.standard }}</p>
</li>
</ol>
Implement page navigation:
goToPage(nextPage: number): void {
const target = Math.max(1, nextPage);
if (target === this.page) return;
this.page = target;
this.runSearch(false);
}
app.component.ts (template)
<div class="pager" aria-label="Pagination controls">
<button
class="button secondary"
type="button"
(click)="goToPage(page - 1)"
[disabled]="page <= 1"
>
Prev
</button>
<span class="muted">Page {{ page }} / {{ response.pagination.totalPages || 1 }}</span>
<button
class="button secondary"
type="button"
(click)="goToPage(page + 1)"
[disabled]="page >= (response.pagination.totalPages || 1)"
>
Next
</button>
</div>
Dependencies
The example uses these key dependencies:
{
"dependencies": {
"@angular/common": "^18.0.0",
"@angular/core": "^18.0.0",
"@angular/forms": "^18.0.0",
"@angular/platform-browser": "^18.0.0",
"quran-search-engine": "workspace:*",
"rxjs": "^7.8.0",
"zone.js": "^0.14.0"
},
"devDependencies": {
"@angular/cli": "^18.0.0",
"@angular/compiler-cli": "^18.0.0",
"typescript": "~5.5.0"
}
}
Key features demonstrated
- Standalone components: Modern Angular architecture without NgModules
- Two-way binding: Using
[(ngModel)] for reactive forms
- Async operations: Proper async/await handling in lifecycle hooks
- Performance optimization: Highlight caching and
trackBy for efficient rendering
- Accessibility: ARIA labels and semantic HTML
- Type safety: Full TypeScript integration with Angular