-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: fetch routerLink from latest upstream
- Loading branch information
1 parent
02c218f
commit a692802
Showing
2 changed files
with
213 additions
and
76 deletions.
There are no files selected for viewing
258 changes: 188 additions & 70 deletions
258
packages/angular/src/lib/legacy/router/ns-router-link-active.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters