Skip to content

Commit

Permalink
feat: fetch routerLink from latest upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
edusperoni committed Jan 17, 2025
1 parent 02c218f commit a692802
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 76 deletions.
258 changes: 188 additions & 70 deletions packages/angular/src/lib/legacy/router/ns-router-link-active.ts
Original file line number Diff line number Diff line change
@@ -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
*
* ```
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link">Bob</a>
* 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
* <a nsRouterLink="/user/bob" nsRouterLinkActive="active-link">Bob</a>
* ```
*
* 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:
*
* ```
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="class1 class2">Bob</a>
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="["class1", "class2"]">Bob</a>
* ```html
* <a nsRouterLink="/user/bob" nsRouterLinkActive="class1 class2">Bob</a>
* <a nsRouterLink="/user/bob" [nsRouterLinkActive]="['class1', 'class2']">Bob</a>
* ```
*
* 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`:
*
* ```
* <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link"
* [nsRouterLinkActiveOptions]="{exact: true}">Bob</a>
* ```html
* <a nsRouterLink="/user/bob" nsRouterLinkActive="active-link" [nsRouterLinkActiveOptions]="{exact:
* true}">Bob</a>
* ```
*
* 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
* <a nsRouterLink="/user/bob" nsRouterLinkActive #rla="nsRouterLinkActive">
* Bob {{ rla.isActive ? '(already open)' : ''}}
* </a>
* ```
* <div [nsRouterLinkActive]="active-link" [nsRouterLinkActiveOptions]="{exact: true}">
* <a [nsRouterLink]="/user/jim">Jim</a>
* <a [nsRouterLink]="/user/bob">Bob</a>
*
* You can apply the `NsRouterLinkActive` directive to an ancestor of linked elements.
* For example, the following sets the active-link class on the `<div>` parent tag
* when the URL is either '/user/jim' or '/user/bob'.
*
* ```html
* <div nsRouterLinkActive="active-link" [nsRouterLinkActiveOptions]="{exact: true}">
* <a nsRouterLink="/user/jim">Jim</a>
* <a nsRouterLink="/user/bob">Bob</a>
* </div>
* ```
*
* 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
* <a nsRouterLink="/" nsRouterLinkActive="active" ariaCurrentWhenActive="page">Home Page</a>
* ```
*
* @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<NSRouterLink>;
export class NsRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
@ContentChildren(NSRouterLink, { descendants: true }) links!: QueryList<NSRouterLink>;

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
* <a
* nsRouterLink="/user/bob"
* nsRouterLinkActive="active-link"
* (isActiveChange)="this.onNsRouterLinkActive($event)">Bob</a>
* ```
*/
@Output() readonly isActiveChange: EventEmitter<boolean> = 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 = <any>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<any>): 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;
}
31 changes: 25 additions & 6 deletions packages/angular/src/lib/legacy/router/ns-router-link.ts
Original file line number Diff line number Diff line change
@@ -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' | '';
Expand Down Expand Up @@ -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 };
Expand All @@ -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<NSRouterLink>();

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', () => {
Expand All @@ -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();
Expand Down

0 comments on commit a692802

Please sign in to comment.