diff --git a/packages/angular/src/lib/legacy/router/ns-router-link-active.ts b/packages/angular/src/lib/legacy/router/ns-router-link-active.ts
index 331ae90..cd9a2b0 100644
--- a/packages/angular/src/lib/legacy/router/ns-router-link-active.ts
+++ b/packages/angular/src/lib/legacy/router/ns-router-link-active.ts
@@ -1,132 +1,250 @@
-import { AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer2 } from '@angular/core';
-import { Subscription } from 'rxjs';
-
-import { NavigationEnd, Router, UrlTree } from '@angular/router';
-import { containsTree } from './private-imports/router-url-tree';
-
+import {
+ AfterContentInit,
+ ChangeDetectorRef,
+ ContentChildren,
+ Directive,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ Optional,
+ Output,
+ QueryList,
+ Renderer2,
+ SimpleChanges,
+} from '@angular/core';
+import { Event, IsActiveMatchOptions, NavigationEnd, Router } from '@angular/router';
+import { from, of, Subscription } from 'rxjs';
+import { mergeAll } from 'rxjs/operators';
import { NSRouterLink } from './ns-router-link';
/**
- * The NSRouterLinkActive directive lets you add a CSS class to an element when the link"s route
- * becomes active.
*
- * Consider the following example:
+ * @description
*
- * ```
- * Bob
+ * Tracks whether the linked route of an element is currently active, and allows you
+ * to specify one or more CSS classes to add to the element when the linked route
+ * is active.
+ *
+ * Use this directive to create a visual distinction for elements associated with an active route.
+ * For example, the following code highlights the word "Bob" when the router
+ * activates the associated route:
+ *
+ * ```html
+ * Bob
* ```
*
- * When the url is either "/user" or "/user/bob", the active-link class will
- * be added to the component. If the url changes, the class will be removed.
+ * Whenever the URL is either '/user' or '/user/bob', the "active-link" class is
+ * added to the anchor tag. If the URL changes, the class is removed.
*
- * You can set more than one class, as follows:
+ * You can set more than one class using a space-separated string or an array.
+ * For example:
*
- * ```
- * Bob
- * Bob
+ * ```html
+ * Bob
+ * Bob
* ```
*
- * You can configure NSRouterLinkActive by passing `exact: true`. This will add the
- * classes only when the url matches the link exactly.
+ * To add the classes only when the URL matches the link exactly, add the option `exact: true`:
*
- * ```
- * Bob
+ * ```html
+ * Bob
* ```
*
- * Finally, you can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
+ * To directly check the `isActive` status of the link, assign the `NsRouterLinkActive`
+ * instance to a template variable.
+ * For example, the following checks the status without assigning any CSS classes:
*
+ * ```html
+ *
+ * Bob {{ rla.isActive ? '(already open)' : ''}}
+ *
* ```
- *
- *
Jim
- *
Bob
+ *
+ * You can apply the `NsRouterLinkActive` directive to an ancestor of linked elements.
+ * For example, the following sets the active-link class on the `
` parent tag
+ * when the URL is either '/user/jim' or '/user/bob'.
+ *
+ * ```html
+ *
* ```
*
- * This will set the active-link class on the div tag if the url is either "/user/jim" or
- * "/user/bob".
+ * The `NsRouterLinkActive` directive can also be used to set the aria-current attribute
+ * to provide an alternative distinction for active elements to visually impaired users.
+ *
+ * For example, the following code adds the 'active' class to the Home Page link when it is
+ * indeed active and in such case also sets its aria-current attribute to 'page':
+ *
+ * ```html
+ *
Home Page
+ * ```
+ *
+ * @ngModule RouterModule
*
- * @stable
+ * @publicApi
*/
@Directive({
selector: '[nsRouterLinkActive]',
- exportAs: 'routerLinkActive',
- standalone: true,
+ exportAs: 'nsRouterLinkActive',
})
-export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
- // tslint:disable-line:max-line-length directive-class-suffix
- @ContentChildren(NSRouterLink) links: QueryList
;
+export class NsRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
+ @ContentChildren(NSRouterLink, { descendants: true }) links!: QueryList;
private classes: string[] = [];
- private subscription: Subscription;
- private active = false;
+ private routerEventsSubscription: Subscription;
+ private linkInputChangesSubscription?: Subscription;
+ private _isActive = false;
+
+ get isActive() {
+ return this._isActive;
+ }
+
+ /**
+ * Options to configure how to determine if the router link is active.
+ *
+ * These options are passed to the `Router.isActive()` function.
+ *
+ * @see {@link Router#isActive}
+ */
+ @Input() nsRouterLinkActiveOptions: { exact: boolean } | IsActiveMatchOptions = { exact: false };
- @Input() nsRouterLinkActiveOptions: { exact: boolean } = { exact: false };
+ /**
+ * Aria-current attribute to apply when the router link is active.
+ *
+ * Possible values: `'page'` | `'step'` | `'location'` | `'date'` | `'time'` | `true` | `false`.
+ *
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current}
+ */
+ @Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false;
- constructor(private router: Router, private element: ElementRef, private renderer: Renderer2) {
- this.subscription = router.events.subscribe((s) => {
+ /**
+ *
+ * You can use the output `isActiveChange` to get notified each time the link becomes
+ * active or inactive.
+ *
+ * Emits:
+ * true -> Route is active
+ * false -> Route is inactive
+ *
+ * ```html
+ * Bob
+ * ```
+ */
+ @Output() readonly isActiveChange: EventEmitter = new EventEmitter();
+
+ constructor(
+ private router: Router,
+ private element: ElementRef,
+ private renderer: Renderer2,
+ private readonly cdr: ChangeDetectorRef,
+ @Optional() private link?: NSRouterLink,
+ ) {
+ this.routerEventsSubscription = router.events.subscribe((s: Event) => {
if (s instanceof NavigationEnd) {
this.update();
}
});
}
- get isActive(): boolean {
- return this.active;
+ /** @nodoc */
+ ngAfterContentInit(): void {
+ // `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
+ of(this.links.changes, of(null))
+ .pipe(mergeAll())
+ .subscribe((_) => {
+ this.update();
+ this.subscribeToEachLinkOnChanges();
+ });
}
- ngAfterContentInit(): void {
- this.links.changes.subscribe(() => this.update());
- this.update();
+ private subscribeToEachLinkOnChanges() {
+ this.linkInputChangesSubscription?.unsubscribe();
+ const allLinkChanges = [...this.links.toArray(), this.link]
+ .filter((link): link is NSRouterLink => !!link)
+ .map((link) => link.onChanges);
+ this.linkInputChangesSubscription = from(allLinkChanges)
+ .pipe(mergeAll())
+ .subscribe((link) => {
+ if (this._isActive !== this.isLinkActive(this.router)(link)) {
+ this.update();
+ }
+ });
}
@Input()
set nsRouterLinkActive(data: string[] | string) {
- if (Array.isArray(data)) {
- this.classes = data;
- } else {
- this.classes = data.split(' ');
- }
+ const classes = Array.isArray(data) ? data : data.split(' ');
+ this.classes = classes.filter((c) => !!c);
}
- ngOnChanges() {
+ /** @nodoc */
+ ngOnChanges(changes: SimpleChanges): void {
this.update();
}
- ngOnDestroy() {
- this.subscription.unsubscribe();
+ /** @nodoc */
+ ngOnDestroy(): void {
+ this.routerEventsSubscription.unsubscribe();
+ this.linkInputChangesSubscription?.unsubscribe();
}
private update(): void {
- if (!this.links) {
- return;
- }
- const hasActiveLinks = this.hasActiveLinks();
- // react only when status has changed to prevent unnecessary dom updates
- if (this.active !== hasActiveLinks) {
- const currentUrlTree = this.router.parseUrl(this.router.url);
- const isActiveLinks = this.reduceList(currentUrlTree, this.links);
+ if (!this.links || !this.router.navigated) return;
+
+ queueMicrotask(() => {
+ const hasActiveLinks = this.hasActiveLinks();
this.classes.forEach((c) => {
- if (isActiveLinks) {
+ if (hasActiveLinks) {
this.renderer.addClass(this.element.nativeElement, c);
} else {
this.renderer.removeClass(this.element.nativeElement, c);
}
});
- }
- Promise.resolve(hasActiveLinks).then((active) => (this.active = active));
- }
+ // we don't have aria in nativescript
+ // if (hasActiveLinks && this.ariaCurrentWhenActive !== undefined) {
+ // this.renderer.setAttribute(this.element.nativeElement, 'aria-current', this.ariaCurrentWhenActive.toString());
+ // } else {
+ // this.renderer.removeAttribute(this.element.nativeElement, 'aria-current');
+ // }
- private reduceList(currentUrlTree: UrlTree, q: QueryList): boolean {
- return q.reduce((res: boolean, link: NSRouterLink) => {
- return res || containsTree(currentUrlTree, link.urlTree, this.nsRouterLinkActiveOptions.exact);
- }, false);
+ // Only emit change if the active state changed.
+ if (this._isActive !== hasActiveLinks) {
+ this._isActive = hasActiveLinks;
+ this.cdr.markForCheck();
+ // Emit on isActiveChange after classes are updated
+ this.isActiveChange.emit(hasActiveLinks);
+ }
+ });
}
private isLinkActive(router: Router): (link: NSRouterLink) => boolean {
- return (link: NSRouterLink) => router.isActive(link.urlTree, this.nsRouterLinkActiveOptions.exact);
+ const options: boolean | IsActiveMatchOptions = isActiveMatchOptions(this.nsRouterLinkActiveOptions)
+ ? this.nsRouterLinkActiveOptions
+ : // While the types should disallow `undefined` here, it's possible without strict inputs
+ this.nsRouterLinkActiveOptions.exact || false;
+ return (link: NSRouterLink) => {
+ const urlTree = link.urlTree;
+ // hardcoding the "as" there to make TS happy, but this function has overloads for both boolean and IsActiveMatchOptions
+ return urlTree ? router.isActive(urlTree, options as IsActiveMatchOptions) : false;
+ };
}
private hasActiveLinks(): boolean {
- return this.links.some(this.isLinkActive(this.router));
+ const isActiveCheckFn = this.isLinkActive(this.router);
+ return (this.link && isActiveCheckFn(this.link)) || this.links.some(isActiveCheckFn);
}
}
+
+/**
+ * Use instead of `'paths' in options` to be compatible with property renaming
+ */
+function isActiveMatchOptions(options: { exact: boolean } | IsActiveMatchOptions): options is IsActiveMatchOptions {
+ return !!(options as IsActiveMatchOptions).paths;
+}
diff --git a/packages/angular/src/lib/legacy/router/ns-router-link.ts b/packages/angular/src/lib/legacy/router/ns-router-link.ts
index b71bc0b..64330cb 100644
--- a/packages/angular/src/lib/legacy/router/ns-router-link.ts
+++ b/packages/angular/src/lib/legacy/router/ns-router-link.ts
@@ -1,10 +1,11 @@
-import { Directive, Input, ElementRef, NgZone, AfterViewInit } from '@angular/core';
+import { Directive, Input, ElementRef, NgZone, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core';
import { NavigationExtras } from '@angular/router';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { NavigationTransition } from '@nativescript/core';
import { NativeScriptDebug } from '../../trace';
import { RouterExtensions } from './router-extensions';
import { NavigationOptions } from './ns-location-utils';
+import { Subject } from 'rxjs';
// Copied from "@angular/router/src/config"
export type QueryParamsHandling = 'merge' | 'preserve' | '';
@@ -33,11 +34,11 @@ export type QueryParamsHandling = 'merge' | 'preserve' | '';
* instead look in the current component"s children for the route.
* And if the segment begins with `../`, the router will go up one level.
*/
-@Directive({
- selector: '[nsRouterLink]',
+@Directive({
+ selector: '[nsRouterLink]',
standalone: true,
})
-export class NSRouterLink implements AfterViewInit {
+export class NSRouterLink implements OnChanges, AfterViewInit {
// tslint:disable-line:directive-class-suffix
@Input() target: string;
@Input() queryParams: { [k: string]: any };
@@ -55,7 +56,20 @@ export class NSRouterLink implements AfterViewInit {
private commands: any[] = [];
- constructor(private ngZone: NgZone, private router: Router, private navigator: RouterExtensions, private route: ActivatedRoute, private el: ElementRef) {}
+ /** @internal */
+ onChanges = new Subject();
+
+ constructor(
+ private ngZone: NgZone,
+ private router: Router,
+ private navigator: RouterExtensions,
+ private route: ActivatedRoute,
+ private el: ElementRef,
+ ) {}
+
+ ngOnChanges(changes?: SimpleChanges): void {
+ this.onChanges.next(this);
+ }
ngAfterViewInit() {
this.el.nativeElement.on('tap', () => {
@@ -76,7 +90,12 @@ export class NSRouterLink implements AfterViewInit {
onTap() {
if (NativeScriptDebug.isLogEnabled()) {
- NativeScriptDebug.routerLog(`nsRouterLink.tapped: ${this.commands} ` + `clear: ${this.clearHistory} ` + `transition: ${JSON.stringify(this.pageTransition)} ` + `duration: ${this.pageTransitionDuration}`);
+ NativeScriptDebug.routerLog(
+ `nsRouterLink.tapped: ${this.commands} ` +
+ `clear: ${this.clearHistory} ` +
+ `transition: ${JSON.stringify(this.pageTransition)} ` +
+ `duration: ${this.pageTransitionDuration}`,
+ );
}
const extras = this.getExtras();