diff --git a/apps/nativescript-demo-ng/src/tests/ns-router-link.spec.ts b/apps/nativescript-demo-ng/src/tests/ns-router-link.spec.ts
index febabe3..d12edf6 100644
--- a/apps/nativescript-demo-ng/src/tests/ns-router-link.spec.ts
+++ b/apps/nativescript-demo-ng/src/tests/ns-router-link.spec.ts
@@ -1,37 +1,51 @@
-import { NSRouterLink } from '@nativescript/angular';
-import { ActivatedRoute, Router } from '@angular/router';
+import { NSRouterLink, NativeScriptRouterModule } from '@nativescript/angular';
import { RouterExtensions } from '@nativescript/angular';
-import { fake, spy, stub } from './test-config.spec';
-import { SinonStub } from 'sinon';
-import { Label } from '@nativescript/core';
+import { fake } from './test-config.spec';
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, ComponentFixture } from '@angular/core/testing';
+import { NativeScriptModule } from '@nativescript/angular';
-describe('NSRouterLink', () => {
- const mockRouter = {} as Router;
- const mockRouterExtensions = {
- navigateByUrl: fake(),
- navigate: fake(),
- };
- const mockActivatedRoute = {} as ActivatedRoute;
- let nsRouterLink: NSRouterLink;
- let urlTreeStub: SinonStub;
+@Component({
+ imports: [NativeScriptRouterModule, NSRouterLink],
+ template: ``,
+})
+class RouterLinkTestComponent {
+ @ViewChild(NSRouterLink, { static: false }) nsRouterLink: NSRouterLink;
+}
- beforeEach(() => {
- const el = {
- nativeElement: new Label(),
- };
- nsRouterLink = new NSRouterLink(null, mockRouter, mockRouterExtensions as unknown as RouterExtensions, mockActivatedRoute, el);
- urlTreeStub = stub(nsRouterLink, 'urlTree').get(() => null);
- });
+describe('NSRouterLink', () => {
+ let mockNavigate: ReturnType;
+ let fixture: ComponentFixture;
- afterEach(() => {
- urlTreeStub.restore();
+ beforeEach(async () => {
+ mockNavigate = fake();
+ TestBed.configureTestingModule({
+ imports: [
+ NativeScriptModule,
+ NativeScriptRouterModule.forRoot([{ path: 'test', component: RouterLinkTestComponent }]),
+ RouterLinkTestComponent,
+ ],
+ providers: [
+ {
+ provide: RouterExtensions,
+ useValue: {
+ navigateByUrl: fake(),
+ navigate: mockNavigate,
+ },
+ },
+ ],
+ });
+ await TestBed.compileComponents();
+ fixture = TestBed.createComponent(RouterLinkTestComponent);
+ fixture.detectChanges();
+ await fixture.whenStable();
});
it('#tap should call navigate with undefined transition in extras when boolean is given for pageTransition input', () => {
- nsRouterLink.pageTransition = false;
- nsRouterLink.onTap();
- expect(mockRouterExtensions.navigate.lastCall.args[1].transition).toBeUndefined();
- // assert.isUndefined(mockRouterExtensions.navigateByUrl.lastCall.args[1].transition);
+ const directive = fixture.componentInstance.nsRouterLink;
+ directive.pageTransition = false;
+ directive['onTap']();
+ expect(mockNavigate.lastCall.args[1].transition).toBeUndefined();
});
it('#tap should call navigate with correct transition in extras when NavigationTransition object is given for pageTransition input', () => {
@@ -39,9 +53,9 @@ describe('NSRouterLink', () => {
name: 'slide',
duration: 500,
};
- nsRouterLink.pageTransition = pageTransition;
- stub(nsRouterLink, 'urlTree').get(() => null);
- nsRouterLink.onTap();
- expect(mockRouterExtensions.navigate.lastCall.args[1].transition).toBe(pageTransition);
+ const directive = fixture.componentInstance.nsRouterLink;
+ directive.pageTransition = pageTransition;
+ directive['onTap']();
+ expect(mockNavigate.lastCall.args[1].transition).toBe(pageTransition);
});
});
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..0d71ff9 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,19 +1,43 @@
-import { AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer2 } from '@angular/core';
-import { Subscription } from 'rxjs';
+import { AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, inject, Input, OnChanges, OnDestroy, Output, QueryList, Renderer2, SimpleChanges, untracked } from '@angular/core';
+import { from, of, Subscription } from 'rxjs';
+import { mergeAll } from 'rxjs/operators';
-import { NavigationEnd, Router, UrlTree } from '@angular/router';
-import { containsTree } from './private-imports/router-url-tree';
+import { IsActiveMatchOptions, NavigationEnd, Router, isActive } from '@angular/router';
import { NSRouterLink } from './ns-router-link';
+// Inline equivalent of upstream's exactMatchOptions
+const exactMatchOptions: IsActiveMatchOptions = {
+ paths: 'exact',
+ fragment: 'ignored',
+ matrixParams: 'ignored',
+ queryParams: 'exact',
+};
+
+// Inline equivalent of upstream's subsetMatchOptions
+const subsetMatchOptions: IsActiveMatchOptions = {
+ paths: 'subset',
+ fragment: 'ignored',
+ matrixParams: 'ignored',
+ queryParams: 'subset',
+};
+
/**
- * The NSRouterLinkActive directive lets you add a CSS class to an element when the link"s route
+ * Use instead of `'paths' in options` to be compatible with property renaming
+ */
+function isActiveMatchOptions(options: { exact: boolean } | Partial): options is Partial {
+ const o = options as Partial;
+ return !!(o.paths || o.matrixParams || o.queryParams || o.fragment);
+}
+
+/**
+ * The NSRouterLinkActive directive lets you add a CSS class to an element when the link's route
* becomes active.
*
* Consider the following example:
*
* ```
- * Bob
+ *
* ```
*
* When the url is either "/user" or "/user/bob", the active-link class will
@@ -22,31 +46,48 @@ import { NSRouterLink } from './ns-router-link';
* You can set more than one class, as follows:
*
* ```
- * Bob
- * Bob
+ *
+ *
* ```
*
* You can configure NSRouterLinkActive by passing `exact: true`. This will add the
* classes only when the url matches the link exactly.
*
* ```
- * Bob
+ *
+ * ```
+ *
+ * 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:
+ *
+ * ```
+ *
* ```
*
- * Finally, you can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
+ * You can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
*
* ```
- *
+ *
+ *
+ *
+ *
* ```
*
- * This will set the active-link class on the div tag if the url is either "/user/jim" or
+ * This will set the active-link class on the StackLayout if the url is either "/user/jim" or
* "/user/bob".
*
- * @stable
+ * 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':
+ *
+ * ```
+ *
+ * ```
*/
@Directive({
selector: '[nsRouterLinkActive]',
@@ -54,79 +95,151 @@ import { NSRouterLink } from './ns-router-link';
standalone: true,
})
export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
- // tslint:disable-line:max-line-length directive-class-suffix
- @ContentChildren(NSRouterLink) links: QueryList;
+ @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(): boolean {
+ return this._isActive;
+ }
+
+ /**
+ * Options to configure how to determine if the router link is active.
+ *
+ * These options are passed to the `isActive()` function.
+ *
+ * @see {@link isActive}
+ */
+ @Input() nsRouterLinkActiveOptions: { exact: boolean } | Partial = { 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
+ *
+ * ```
+ */
+ @Output() readonly isActiveChange: EventEmitter = new EventEmitter();
+
+ private readonly link = inject(NSRouterLink, { optional: true });
+ private readonly router = inject(Router);
+ private readonly element = inject(ElementRef);
+ private readonly renderer = inject(Renderer2);
+ private readonly cdr = inject(ChangeDetectorRef);
+
+ constructor() {
+ this.routerEventsSubscription = this.router.events.subscribe((s) => {
if (s instanceof NavigationEnd) {
this.update();
}
});
}
- get isActive(): boolean {
- return this.active;
+ 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() {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ngOnChanges(_changes: SimpleChanges): void {
this.update();
}
- ngOnDestroy() {
- this.subscription.unsubscribe();
+
+ 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));
- }
+ 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: Partial = isActiveMatchOptions(this.nsRouterLinkActiveOptions)
+ ? this.nsRouterLinkActiveOptions
+ : // While the types should disallow `undefined` here, it's possible without strict inputs
+ (this.nsRouterLinkActiveOptions.exact ?? false)
+ ? { ...exactMatchOptions }
+ : { ...subsetMatchOptions };
+
+ return (link: NSRouterLink) => {
+ const urlTree = link.urlTree;
+ return urlTree ? untracked(isActive(urlTree, router, options)) : 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);
}
}
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..28fa173 100644
--- a/packages/angular/src/lib/legacy/router/ns-router-link.ts
+++ b/packages/angular/src/lib/legacy/router/ns-router-link.ts
@@ -1,128 +1,372 @@
-import { Directive, Input, ElementRef, NgZone, AfterViewInit } from '@angular/core';
-import { NavigationExtras } from '@angular/router';
-import { ActivatedRoute, Router, UrlTree } from '@angular/router';
+import { booleanAttribute, computed, Directive, ElementRef, inject, Input, NgZone, OnChanges, OnDestroy, signal, SimpleChanges, untracked } from '@angular/core';
+import { ActivatedRoute, NavigationExtras, QueryParamsHandling, Router, UrlTree } from '@angular/router';
import { NavigationTransition } from '@nativescript/core';
+import { Subject } from 'rxjs';
import { NativeScriptDebug } from '../../trace';
import { RouterExtensions } from './router-extensions';
import { NavigationOptions } from './ns-location-utils';
-// Copied from "@angular/router/src/config"
-export type QueryParamsHandling = 'merge' | 'preserve' | '';
+function isUrlTree(value: any): value is UrlTree {
+ return value instanceof UrlTree;
+}
/**
* The nsRouterLink directive lets you link to specific parts of your app.
*
* Consider the following route configuration:
* ```
- * [{ path: "/user", component: UserCmp }]
+ * [{ path: 'user/:name', component: UserCmp }]
* ```
*
* When linking to this `User` route, you can write:
*
* ```
- * link to user component
+ *
* ```
*
- * NSRouterLink expects the value to be an array of path segments, followed by the params
- * for that level of routing. For instance `["/team", {teamId: 1}, "user", {userId: 2}]`
- * means that we want to generate a link to `/team;teamId=1/user;userId=2`.
+ * You can use dynamic values to generate the link.
+ * For a dynamic link, pass an array of path segments,
+ * followed by the params for each segment.
+ * For example, `['/team', teamId, 'user', userName, {details: true}]`
+ * generates a link to `/team/11/user/bob;details=true`.
+ *
+ * Multiple static segments can be merged into one term and combined with
+ * dynamic segments. For example, `['/team/11/user', userName, {details: true}]`
+ *
+ * The input that you provide to the link is treated as a delta to the current
+ * URL. For instance, suppose the current URL is `/user/(box//aux:team)`. The
+ * link `` creates the URL
+ * `/user/(jim//aux:team)`.
+ * See {@link Router#createUrlTree} for more information.
+ *
+ * @usageNotes
+ *
+ * You can use absolute or relative paths in a link, set query parameters,
+ * control how parameters are handled, and keep a history of navigation states.
+ *
+ * ### Relative link paths
*
* The first segment name can be prepended with `/`, `./`, or `../`.
- * If the segment begins with `/`, the router will look up the route from the root of the app.
- * If the segment begins with `./`, or doesn"t begin with a slash, the router will
- * instead look in the current component"s children for the route.
- * And if the segment begins with `../`, the router will go up one level.
+ * * If the first segment begins with `/`, the router looks up the route from
+ * the root of the app.
+ * * If the first segment begins with `./`, or doesn't begin with a slash, the
+ * router looks in the children of the current activated route.
+ * * If the first segment begins with `../`, the router goes up one level in the
+ * route tree.
+ *
+ * ### Setting and handling query params and fragments
+ *
+ * The following link adds a query parameter and a fragment to the generated URL:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * By default, the directive constructs the new URL using the given query
+ * parameters. The example generates the link: `/user/bob?debug=true#education`.
+ *
+ * You can instruct the directive to handle query parameters differently
+ * by specifying the `queryParamsHandling` option in the link.
+ * Allowed values are:
+ *
+ * - `'merge'`: Merge the given `queryParams` into the current query params.
+ * - `'preserve'`: Preserve the current query params.
+ *
+ * For example:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * `queryParams`, `fragment`, `queryParamsHandling`, `preserveFragment`, and
+ * `relativeTo` cannot be used when the `nsRouterLink` input is a `UrlTree`.
+ *
+ * ### NativeScript-specific options
+ *
+ * NativeScript adds support for page transitions and history clearing:
+ *
+ * ```html
+ *
+ * ```
*/
-@Directive({
- selector: '[nsRouterLink]',
+@Directive({
+ selector: '[nsRouterLink]',
standalone: true,
})
-export class NSRouterLink implements AfterViewInit {
- // tslint:disable-line:directive-class-suffix
- @Input() target: string;
- @Input() queryParams: { [k: string]: any };
- @Input() fragment: string;
-
- @Input() queryParamsHandling: QueryParamsHandling;
- @Input() preserveQueryParams: boolean;
- @Input() preserveFragment: boolean;
- @Input() skipLocationChange: boolean;
- @Input() replaceUrl: boolean;
+export class NSRouterLink implements OnChanges, OnDestroy {
+ private readonly ngZone = inject(NgZone);
+ private readonly router = inject(Router);
+ private readonly navigator = inject(RouterExtensions);
+ private readonly route = inject(ActivatedRoute);
+ private readonly el = inject(ElementRef);
+
+ /**
+ * Passed to {@link Router#createUrlTree} as part of the
+ * `UrlCreationOptions`.
+ * @see {@link UrlCreationOptions#queryParams}
+ * @see {@link Router#createUrlTree}
+ */
+ @Input() set queryParams(value: { [k: string]: any } | null | undefined) {
+ this._queryParams.set(value);
+ }
+ get queryParams(): { [k: string]: any } | null | undefined {
+ return untracked(this._queryParams);
+ }
+ // Rather than trying deep equality checks or serialization, just allow urlTree to recompute
+ // whenever queryParams change (which will be rare).
+ private _queryParams = signal<{ [k: string]: any } | null | undefined>(undefined, { equal: () => false });
+
+ /**
+ * Passed to {@link Router#createUrlTree} as part of the
+ * `UrlCreationOptions`.
+ * @see {@link UrlCreationOptions#fragment}
+ * @see {@link Router#createUrlTree}
+ */
+ @Input() set fragment(value: string | undefined) {
+ this._fragment.set(value);
+ }
+ get fragment(): string | undefined {
+ return untracked(this._fragment);
+ }
+ private _fragment = signal(undefined);
+
+ /**
+ * Passed to {@link Router#createUrlTree} as part of the
+ * `UrlCreationOptions`.
+ * @see {@link UrlCreationOptions#queryParamsHandling}
+ * @see {@link Router#createUrlTree}
+ */
+ @Input() set queryParamsHandling(value: QueryParamsHandling | null | undefined) {
+ this._queryParamsHandling.set(value);
+ }
+ get queryParamsHandling(): QueryParamsHandling | null | undefined {
+ return untracked(this._queryParamsHandling);
+ }
+ private _queryParamsHandling = signal(undefined);
+ /**
+ * Passed to {@link Router#navigateByUrl} as part of the
+ * `NavigationBehaviorOptions`.
+ * @see {@link NavigationBehaviorOptions#state}
+ * @see {@link Router#navigateByUrl}
+ */
+ @Input() set state(value: { [k: string]: any } | undefined) {
+ this._state.set(value);
+ }
+ get state(): { [k: string]: any } | undefined {
+ return untracked(this._state);
+ }
+ private _state = signal<{ [k: string]: any } | undefined>(undefined, { equal: () => false });
+
+ /**
+ * Passed to {@link Router#navigateByUrl} as part of the
+ * `NavigationBehaviorOptions`.
+ * @see {@link NavigationBehaviorOptions#info}
+ * @see {@link Router#navigateByUrl}
+ */
+ @Input() set info(value: unknown) {
+ this._info.set(value);
+ }
+ get info(): unknown {
+ return untracked(this._info);
+ }
+ private _info = signal(undefined, { equal: () => false });
+
+ /**
+ * Passed to {@link Router#createUrlTree} as part of the
+ * `UrlCreationOptions`.
+ * Specify a value here when you do not want to use the default value
+ * for `nsRouterLink`, which is the current activated route.
+ * Note that a value of `undefined` here will use the `nsRouterLink` default.
+ * @see {@link UrlCreationOptions#relativeTo}
+ * @see {@link Router#createUrlTree}
+ */
+ @Input() set relativeTo(value: ActivatedRoute | null | undefined) {
+ this._relativeTo.set(value);
+ }
+ get relativeTo(): ActivatedRoute | null | undefined {
+ return untracked(this._relativeTo);
+ }
+ private _relativeTo = signal(undefined);
+
+ /**
+ * Passed to {@link Router#createUrlTree} as part of the
+ * `UrlCreationOptions`.
+ * @see {@link UrlCreationOptions#preserveFragment}
+ * @see {@link Router#createUrlTree}
+ */
+ @Input({ transform: booleanAttribute }) set preserveFragment(value: boolean) {
+ this._preserveFragment.set(value);
+ }
+ get preserveFragment(): boolean {
+ return untracked(this._preserveFragment);
+ }
+ private _preserveFragment = signal(false);
+
+ /**
+ * Passed to {@link Router#navigateByUrl} as part of the
+ * `NavigationBehaviorOptions`.
+ * @see {@link NavigationBehaviorOptions#skipLocationChange}
+ * @see {@link Router#navigateByUrl}
+ */
+ @Input({ transform: booleanAttribute }) set skipLocationChange(value: boolean) {
+ this._skipLocationChange.set(value);
+ }
+ get skipLocationChange(): boolean {
+ return untracked(this._skipLocationChange);
+ }
+ private _skipLocationChange = signal(false);
+
+ /**
+ * Passed to {@link Router#navigateByUrl} as part of the
+ * `NavigationBehaviorOptions`.
+ * @see {@link NavigationBehaviorOptions#replaceUrl}
+ * @see {@link Router#navigateByUrl}
+ */
+ @Input({ transform: booleanAttribute }) set replaceUrl(value: boolean) {
+ this._replaceUrl.set(value);
+ }
+ get replaceUrl(): boolean {
+ return untracked(this._replaceUrl);
+ }
+ private _replaceUrl = signal(false);
+
+ // NativeScript-specific inputs
@Input() clearHistory: boolean;
@Input() pageTransition: boolean | string | NavigationTransition = true;
@Input() pageTransitionDuration;
- private commands: any[] = [];
+ /** @internal */
+ onChanges = new Subject();
- constructor(private ngZone: NgZone, private router: Router, private navigator: RouterExtensions, private route: ActivatedRoute, private el: ElementRef) {}
+ private routerLinkInput = signal(null);
- ngAfterViewInit() {
- this.el.nativeElement.on('tap', () => {
+ private tapHandler: () => void;
+
+ constructor() {
+ // NativeScript uses tap events instead of click events
+ this.tapHandler = () => {
this.ngZone.run(() => {
this.onTap();
});
- });
+ };
+ this.el.nativeElement.on('tap', this.tapHandler);
}
- @Input('nsRouterLink')
- set params(data: any[] | string) {
- if (Array.isArray(data)) {
- this.commands = data;
+ /**
+ * Commands to pass to {@link Router#createUrlTree} or a `UrlTree`.
+ * - **array**: commands to pass to {@link Router#createUrlTree}.
+ * - **string**: shorthand for array of commands with just the string, i.e. `['/route']`
+ * - **UrlTree**: a `UrlTree` for this link rather than creating one from
+ * the commands and other inputs that correspond to properties of `UrlCreationOptions`.
+ * - **null|undefined**: effectively disables the `nsRouterLink`
+ * @see {@link Router#createUrlTree}
+ */
+ @Input()
+ set nsRouterLink(commandsOrUrlTree: readonly any[] | string | UrlTree | null | undefined) {
+ if (commandsOrUrlTree == null) {
+ this.routerLinkInput.set(null);
} else {
- this.commands = [data];
+ if (isUrlTree(commandsOrUrlTree)) {
+ this.routerLinkInput.set(commandsOrUrlTree);
+ } else {
+ this.routerLinkInput.set(Array.isArray(commandsOrUrlTree) ? commandsOrUrlTree : [commandsOrUrlTree]);
+ }
}
}
- onTap() {
+ // This is subscribed to by `NSRouterLinkActive` so that it knows to update
+ // when there are changes to the RouterLinks it's tracking.
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ngOnChanges(_changes?: SimpleChanges): void {
+ this.onChanges.next(this);
+ }
+
+ ngOnDestroy(): void {
+ this.el.nativeElement.off('tap', this.tapHandler);
+ }
+
+ /** @internal */
+ _urlTree = computed(() => {
+ const routerLinkInput = this.routerLinkInput();
+ if (routerLinkInput === null || !this.router.createUrlTree) {
+ return null;
+ } else if (isUrlTree(routerLinkInput)) {
+ return routerLinkInput;
+ }
+ return this.router.createUrlTree(routerLinkInput, {
+ // If the `relativeTo` input is not defined, we want to use `this.route`
+ // by default.
+ // Otherwise, we should use the value provided by the user in the input.
+ relativeTo: this._relativeTo() !== undefined ? this._relativeTo() : this.route,
+ queryParams: this._queryParams(),
+ fragment: this._fragment(),
+ queryParamsHandling: this._queryParamsHandling(),
+ preserveFragment: this._preserveFragment(),
+ });
+ });
+
+ get urlTree(): UrlTree | null {
+ return untracked(this._urlTree);
+ }
+
+ // NativeScript tap handler - replaces click handler from upstream
+ private onTap() {
+ const urlTree = this.urlTree;
+
+ if (urlTree === null) {
+ return;
+ }
+
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.routerLinkInput()} ` + `clear: ${this.clearHistory} ` + `transition: ${JSON.stringify(this.pageTransition)} ` + `duration: ${this.pageTransitionDuration}`);
}
const extras = this.getExtras();
- // this.navigator.navigateByUrl(this.urlTree, extras);
- this.navigator.navigate(this.commands, {
- ...extras,
- relativeTo: this.route,
- queryParams: this.queryParams,
- fragment: this.fragment,
- queryParamsHandling: this.queryParamsHandling,
- preserveFragment: attrBoolValue(this.preserveFragment),
- });
+ const routerLinkInput = this.routerLinkInput();
+
+ // When the input is a UrlTree, use navigateByUrl directly.
+ // Otherwise, use navigate with commands array.
+ if (isUrlTree(routerLinkInput)) {
+ this.navigator.navigateByUrl(urlTree, extras);
+ } else {
+ this.navigator.navigate(routerLinkInput as any[], {
+ ...extras,
+ // If the `relativeTo` input is not defined, we want to use `this.route`
+ // by default.
+ relativeTo: this._relativeTo() !== undefined ? this._relativeTo() : this.route,
+ queryParams: this._queryParams(),
+ fragment: this._fragment(),
+ queryParamsHandling: this._queryParamsHandling(),
+ preserveFragment: this._preserveFragment(),
+ });
+ }
}
private getExtras(): NavigationExtras & NavigationOptions {
const transition = this.getTransition();
return {
- skipLocationChange: attrBoolValue(this.skipLocationChange),
- replaceUrl: attrBoolValue(this.replaceUrl),
+ skipLocationChange: this.skipLocationChange,
+ replaceUrl: this.replaceUrl,
+ state: this.state,
+ info: this.info,
+ // NativeScript-specific navigation options
clearHistory: this.convertClearHistory(this.clearHistory),
animated: transition.animated,
transition: transition.transition,
};
}
- get urlTree(): UrlTree {
- const urlTree = this.router.createUrlTree(this.commands, {
- relativeTo: this.route,
- queryParams: this.queryParams,
- fragment: this.fragment,
- queryParamsHandling: this.queryParamsHandling,
- preserveFragment: attrBoolValue(this.preserveFragment),
- });
-
- if (NativeScriptDebug.isLogEnabled()) {
- NativeScriptDebug.routerLog(`nsRouterLink urlTree created: ${urlTree}`);
- }
-
- return urlTree;
- }
-
private convertClearHistory(value: boolean | string): boolean {
return value === true || value === 'true';
}
+ // NativeScript-specific page transition handling
private getTransition(): { animated: boolean; transition?: NavigationTransition } {
let transition: NavigationTransition;
let animated: boolean;
@@ -152,7 +396,3 @@ export class NSRouterLink implements AfterViewInit {
return { animated, transition };
}
}
-
-function attrBoolValue(s: any): boolean {
- return s === '' || !!s;
-}
diff --git a/packages/angular/src/lib/legacy/router/private-imports/router-url-tree.ts b/packages/angular/src/lib/legacy/router/private-imports/router-url-tree.ts
deleted file mode 100644
index 23dcbe2..0000000
--- a/packages/angular/src/lib/legacy/router/private-imports/router-url-tree.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/* tslint:disable:forin */
-// Copied unexported functions from @angular/router/src/url_tree
-import { UrlTree, UrlSegment, PRIMARY_OUTLET } from '@angular/router';
-// UrlSegmentGroup not exported, just use any.
-type UrlSegmentGroup = any;
-
-export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
- if (exact) {
- return equalSegmentGroups(container.root, containee.root);
- } else {
- return containsSegmentGroup(container.root, containee.root);
- }
-}
-
-function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
- if (!equalPath(container.segments, containee.segments)) {
- return false;
- }
- if (container.numberOfChildren !== containee.numberOfChildren) {
- return false;
- }
- for (const c in containee.children) {
- if (!container.children[c]) {
- return false;
- }
- if (!equalSegmentGroups(container.children[c], containee.children[c])) {
- return false;
- }
- }
- return true;
-}
-
-function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
- return containsSegmentGroupHelper(container, containee, containee.segments);
-}
-
-function containsSegmentGroupHelper(container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): boolean {
- if (container.segments.length > containeePaths.length) {
- const current = container.segments.slice(0, containeePaths.length);
- if (!equalPath(current, containeePaths)) {
- return false;
- }
- if (containee.hasChildren()) {
- return false;
- }
- return true;
- } else if (container.segments.length === containeePaths.length) {
- if (!equalPath(container.segments, containeePaths)) {
- return false;
- }
- for (const c in containee.children) {
- if (!container.children[c]) {
- return false;
- }
- if (!containsSegmentGroup(container.children[c], containee.children[c])) {
- return false;
- }
- }
- return true;
- } else {
- const current = containeePaths.slice(0, container.segments.length);
- const next = containeePaths.slice(container.segments.length);
- if (!equalPath(container.segments, current)) {
- return false;
- }
- if (!container.children[PRIMARY_OUTLET]) {
- return false;
- }
- return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next);
- }
-}
-
-export function equalPath(a: UrlSegment[], b: UrlSegment[]): boolean {
- if (a.length !== b.length) {
- return false;
- }
- for (let i = 0; i < a.length; ++i) {
- if (a[i].path !== b[i].path) {
- return false;
- }
- }
- return true;
-}