Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch routerLink from latest upstream #146

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 187 additions & 69 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>;
@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
Loading