1- import { AfterContentInit , ContentChildren , Directive , ElementRef , Input , OnChanges , OnDestroy , QueryList , Renderer2 } from '@angular/core' ;
2- import { Subscription } from 'rxjs' ;
1+ import { AfterContentInit , ChangeDetectorRef , ContentChildren , Directive , ElementRef , EventEmitter , inject , Input , OnChanges , OnDestroy , Output , QueryList , Renderer2 , SimpleChanges , untracked } from '@angular/core' ;
2+ import { from , of , Subscription } from 'rxjs' ;
3+ import { mergeAll } from 'rxjs/operators' ;
34
4- import { NavigationEnd , Router , UrlTree } from '@angular/router' ;
5- import { containsTree } from './private-imports/router-url-tree' ;
5+ import { IsActiveMatchOptions , NavigationEnd , Router , isActive } from '@angular/router' ;
66
77import { NSRouterLink } from './ns-router-link' ;
88
9+ // Inline equivalent of upstream's exactMatchOptions
10+ const exactMatchOptions : IsActiveMatchOptions = {
11+ paths : 'exact' ,
12+ fragment : 'ignored' ,
13+ matrixParams : 'ignored' ,
14+ queryParams : 'exact' ,
15+ } ;
16+
17+ // Inline equivalent of upstream's subsetMatchOptions
18+ const subsetMatchOptions : IsActiveMatchOptions = {
19+ paths : 'subset' ,
20+ fragment : 'ignored' ,
21+ matrixParams : 'ignored' ,
22+ queryParams : 'subset' ,
23+ } ;
24+
925/**
10- * The NSRouterLinkActive directive lets you add a CSS class to an element when the link"s route
26+ * Use instead of `'paths' in options` to be compatible with property renaming
27+ */
28+ function isActiveMatchOptions ( options : { exact : boolean } | Partial < IsActiveMatchOptions > ) : options is Partial < IsActiveMatchOptions > {
29+ const o = options as Partial < IsActiveMatchOptions > ;
30+ return ! ! ( o . paths || o . matrixParams || o . queryParams || o . fragment ) ;
31+ }
32+
33+ /**
34+ * The NSRouterLinkActive directive lets you add a CSS class to an element when the link's route
1135 * becomes active.
1236 *
1337 * Consider the following example:
1438 *
1539 * ```
16- * <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link"> Bob</a >
40+ * <Label [nsRouterLink]="/user/bob" [nsRouterLinkActive]="' active-link'" text=" Bob"></Label >
1741 * ```
1842 *
1943 * When the url is either "/user" or "/user/bob", the active-link class will
@@ -22,111 +46,200 @@ import { NSRouterLink } from './ns-router-link';
2246 * You can set more than one class, as follows:
2347 *
2448 * ```
25- * <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="class1 class2"> Bob</a >
26- * <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="[" class1", " class2"]"> Bob</a >
49+ * <Label [nsRouterLink]="/user/bob" [nsRouterLinkActive]="' class1 class2'" text=" Bob"></Label >
50+ * <Label [nsRouterLink]="/user/bob" [nsRouterLinkActive]="[' class1', ' class2']" text=" Bob"></Label >
2751 * ```
2852 *
2953 * You can configure NSRouterLinkActive by passing `exact: true`. This will add the
3054 * classes only when the url matches the link exactly.
3155 *
3256 * ```
33- * <a [nsRouterLink]="/user/bob" [nsRouterLinkActive]="active-link"
34- * [nsRouterLinkActiveOptions]="{exact: true}">Bob</a>
57+ * <Label [nsRouterLink]="/user/bob" [nsRouterLinkActive]="'active-link'"
58+ * [nsRouterLinkActiveOptions]="{exact: true}" text="Bob"></Label>
59+ * ```
60+ *
61+ * To directly check the `isActive` status of the link, assign the `NSRouterLinkActive`
62+ * instance to a template variable.
63+ * For example, the following checks the status without assigning any CSS classes:
64+ *
65+ * ```
66+ * <Label [nsRouterLink]="/user/bob" nsRouterLinkActive #rla="routerLinkActive"
67+ * [text]="'Bob ' + (rla.isActive ? '(already open)' : '')"></Label>
3568 * ```
3669 *
37- * Finally, you can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
70+ * You can apply the NSRouterLinkActive directive to an ancestor of a RouterLink.
3871 *
3972 * ```
40- * <div [nsRouterLinkActive]="active-link" [nsRouterLinkActiveOptions]="{exact: true}">
41- * <a [nsRouterLink]="/user/jim"> Jim</a >
42- * <a [nsRouterLink]="/user/bob"> Bob</a >
43- * </div >
73+ * <StackLayout [nsRouterLinkActive]="' active-link' " [nsRouterLinkActiveOptions]="{exact: true}">
74+ * <Label [nsRouterLink]="/user/jim" text=" Jim"></Label >
75+ * <Label [nsRouterLink]="/user/bob" text=" Bob"></Label >
76+ * </StackLayout >
4477 * ```
4578 *
46- * This will set the active-link class on the div tag if the url is either "/user/jim" or
79+ * This will set the active-link class on the StackLayout if the url is either "/user/jim" or
4780 * "/user/bob".
4881 *
49- * @stable
82+ * The `NSRouterLinkActive` directive can also be used to set the aria-current attribute
83+ * to provide an alternative distinction for active elements to visually impaired users.
84+ *
85+ * For example, the following code adds the 'active' class to the Home Page link when it is
86+ * indeed active and in such case also sets its aria-current attribute to 'page':
87+ *
88+ * ```
89+ * <Label nsRouterLink="/" [nsRouterLinkActive]="'active'" ariaCurrentWhenActive="page" text="Home Page"></Label>
90+ * ```
5091 */
5192@Directive ( {
5293 selector : '[nsRouterLinkActive]' ,
5394 exportAs : 'routerLinkActive' ,
5495 standalone : true ,
5596} )
5697export class NSRouterLinkActive implements OnChanges , OnDestroy , AfterContentInit {
57- // tslint:disable-line:max-line-length directive-class-suffix
58- @ContentChildren ( NSRouterLink ) links : QueryList < NSRouterLink > ;
98+ @ContentChildren ( NSRouterLink , { descendants : true } ) links ! : QueryList < NSRouterLink > ;
5999
60100 private classes : string [ ] = [ ] ;
61- private subscription : Subscription ;
62- private active = false ;
101+ private routerEventsSubscription : Subscription ;
102+ private linkInputChangesSubscription ?: Subscription ;
103+ private _isActive = false ;
104+
105+ get isActive ( ) : boolean {
106+ return this . _isActive ;
107+ }
108+
109+ /**
110+ * Options to configure how to determine if the router link is active.
111+ *
112+ * These options are passed to the `isActive()` function.
113+ *
114+ * @see {@link isActive }
115+ */
116+ @Input ( ) nsRouterLinkActiveOptions : { exact : boolean } | Partial < IsActiveMatchOptions > = { exact : false } ;
63117
64- @Input ( ) nsRouterLinkActiveOptions : { exact : boolean } = { exact : false } ;
118+ /**
119+ * Aria-current attribute to apply when the router link is active.
120+ *
121+ * Possible values: `'page'` | `'step'` | `'location'` | `'date'` | `'time'` | `true` | `false`.
122+ *
123+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current }
124+ */
125+ @Input ( ) ariaCurrentWhenActive ?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false ;
65126
66- constructor ( private router : Router , private element : ElementRef , private renderer : Renderer2 ) {
67- this . subscription = router . events . subscribe ( ( s ) => {
127+ /**
128+ *
129+ * You can use the output `isActiveChange` to get notified each time the link becomes
130+ * active or inactive.
131+ *
132+ * Emits:
133+ * true -> Route is active
134+ * false -> Route is inactive
135+ *
136+ * ```html
137+ * <Label
138+ * [nsRouterLink]="/user/bob"
139+ * [nsRouterLinkActive]="'active-link'"
140+ * (isActiveChange)="this.onRouterLinkActive($event)" text="Bob"></Label>
141+ * ```
142+ */
143+ @Output ( ) readonly isActiveChange : EventEmitter < boolean > = new EventEmitter ( ) ;
144+
145+ private readonly link = inject ( NSRouterLink , { optional : true } ) ;
146+ private readonly router = inject ( Router ) ;
147+ private readonly element = inject ( ElementRef ) ;
148+ private readonly renderer = inject ( Renderer2 ) ;
149+ private readonly cdr = inject ( ChangeDetectorRef ) ;
150+
151+ constructor ( ) {
152+ this . routerEventsSubscription = this . router . events . subscribe ( ( s ) => {
68153 if ( s instanceof NavigationEnd ) {
69154 this . update ( ) ;
70155 }
71156 } ) ;
72157 }
73158
74- get isActive ( ) : boolean {
75- return this . active ;
159+ ngAfterContentInit ( ) : void {
160+ // `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
161+ of ( this . links . changes , of ( null ) )
162+ . pipe ( mergeAll ( ) )
163+ . subscribe ( ( ) => {
164+ this . update ( ) ;
165+ this . subscribeToEachLinkOnChanges ( ) ;
166+ } ) ;
76167 }
77168
78- ngAfterContentInit ( ) : void {
79- this . links . changes . subscribe ( ( ) => this . update ( ) ) ;
80- this . update ( ) ;
169+ private subscribeToEachLinkOnChanges ( ) {
170+ this . linkInputChangesSubscription ?. unsubscribe ( ) ;
171+ const allLinkChanges = [ ...this . links . toArray ( ) , this . link ]
172+ . filter ( ( link ) : link is NSRouterLink => ! ! link )
173+ . map ( ( link ) => link . onChanges ) ;
174+ this . linkInputChangesSubscription = from ( allLinkChanges )
175+ . pipe ( mergeAll ( ) )
176+ . subscribe ( ( link ) => {
177+ if ( this . _isActive !== this . isLinkActive ( this . router ) ( link ) ) {
178+ this . update ( ) ;
179+ }
180+ } ) ;
81181 }
82182
83183 @Input ( )
84184 set nsRouterLinkActive ( data : string [ ] | string ) {
85- if ( Array . isArray ( data ) ) {
86- this . classes = < any > data ;
87- } else {
88- this . classes = data . split ( ' ' ) ;
89- }
185+ const classes = Array . isArray ( data ) ? data : data . split ( ' ' ) ;
186+ this . classes = classes . filter ( ( c ) => ! ! c ) ;
90187 }
91188
92- ngOnChanges ( ) {
189+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
190+ ngOnChanges ( _changes : SimpleChanges ) : void {
93191 this . update ( ) ;
94192 }
95- ngOnDestroy ( ) {
96- this . subscription . unsubscribe ( ) ;
193+
194+ ngOnDestroy ( ) : void {
195+ this . routerEventsSubscription . unsubscribe ( ) ;
196+ this . linkInputChangesSubscription ?. unsubscribe ( ) ;
97197 }
98198
99199 private update ( ) : void {
100- if ( ! this . links ) {
101- return ;
102- }
103- const hasActiveLinks = this . hasActiveLinks ( ) ;
104- // react only when status has changed to prevent unnecessary dom updates
105- if ( this . active !== hasActiveLinks ) {
106- const currentUrlTree = this . router . parseUrl ( this . router . url ) ;
107- const isActiveLinks = this . reduceList ( currentUrlTree , this . links ) ;
200+ if ( ! this . links || ! this . router . navigated ) return ;
201+
202+ queueMicrotask ( ( ) => {
203+ const hasActiveLinks = this . hasActiveLinks ( ) ;
108204 this . classes . forEach ( ( c ) => {
109- if ( isActiveLinks ) {
205+ if ( hasActiveLinks ) {
110206 this . renderer . addClass ( this . element . nativeElement , c ) ;
111207 } else {
112208 this . renderer . removeClass ( this . element . nativeElement , c ) ;
113209 }
114210 } ) ;
115- }
116- Promise . resolve ( hasActiveLinks ) . then ( ( active ) => ( this . active = active ) ) ;
117- }
211+ if ( hasActiveLinks && this . ariaCurrentWhenActive !== undefined ) {
212+ this . renderer . setAttribute ( this . element . nativeElement , 'aria-current' , this . ariaCurrentWhenActive . toString ( ) ) ;
213+ } else {
214+ this . renderer . removeAttribute ( this . element . nativeElement , 'aria-current' ) ;
215+ }
118216
119- private reduceList ( currentUrlTree : UrlTree , q : QueryList < any > ) : boolean {
120- return q . reduce ( ( res : boolean , link : NSRouterLink ) => {
121- return res || containsTree ( currentUrlTree , link . urlTree , this . nsRouterLinkActiveOptions . exact ) ;
122- } , false ) ;
217+ // Only emit change if the active state changed.
218+ if ( this . _isActive !== hasActiveLinks ) {
219+ this . _isActive = hasActiveLinks ;
220+ this . cdr . markForCheck ( ) ;
221+ // Emit on isActiveChange after classes are updated
222+ this . isActiveChange . emit ( hasActiveLinks ) ;
223+ }
224+ } ) ;
123225 }
124226
125227 private isLinkActive ( router : Router ) : ( link : NSRouterLink ) => boolean {
126- return ( link : NSRouterLink ) => router . isActive ( link . urlTree , this . nsRouterLinkActiveOptions . exact ) ;
228+ const options : Partial < IsActiveMatchOptions > = isActiveMatchOptions ( this . nsRouterLinkActiveOptions )
229+ ? this . nsRouterLinkActiveOptions
230+ : // While the types should disallow `undefined` here, it's possible without strict inputs
231+ ( this . nsRouterLinkActiveOptions . exact ?? false )
232+ ? { ...exactMatchOptions }
233+ : { ...subsetMatchOptions } ;
234+
235+ return ( link : NSRouterLink ) => {
236+ const urlTree = link . urlTree ;
237+ return urlTree ? untracked ( isActive ( urlTree , router , options ) ) : false ;
238+ } ;
127239 }
128240
129241 private hasActiveLinks ( ) : boolean {
130- return this . links . some ( this . isLinkActive ( this . router ) ) ;
242+ const isActiveCheckFn = this . isLinkActive ( this . router ) ;
243+ return ( this . link && isActiveCheckFn ( this . link ) ) || this . links . some ( isActiveCheckFn ) ;
131244 }
132245}
0 commit comments