@@ -10,13 +10,15 @@ import {LocationStrategy} from '@angular/common';
1010import {
1111Attribute ,
1212booleanAttribute ,
13+ computed ,
1314Directive ,
15+ effect ,
1416ElementRef ,
1517HostAttributeToken ,
16- HostBinding ,
1718HostListener ,
1819inject ,
1920Input ,
21+ linkedSignal ,
2022OnChanges ,
2123OnDestroy ,
2224Renderer2 ,
@@ -28,10 +30,8 @@ import {
2830} from '@angular/core' ;
2931import { Subject , Subscription } from 'rxjs' ;
3032import { RuntimeErrorCode } from '../errors' ;
31- import { Event , NavigationEnd } from '../events' ;
3233import { QueryParamsHandling } from '../models' ;
3334import { Router } from '../router' ;
34- import { ROUTER_CONFIGURATION } from '../router_config' ;
3535import { ActivatedRoute } from '../router_state' ;
3636import { Params } from '../shared' ;
3737import { isUrlTree , UrlTree } from '../url_tree' ;
@@ -146,11 +146,23 @@ import {isUrlTree, UrlTree} from '../url_tree';
146146selector :'[routerLink]' ,
147147host :{
148148'[attr.href]' :'reactiveHref()' ,
149+ '[attr.target]' :'_target()' ,
149150} ,
150151} )
151152export class RouterLink implements OnChanges , OnDestroy {
152- /**@nodoc */
153- protected readonly reactiveHref = signal < string | null > ( null ) ;
153+ private hrefAttributeValue = inject ( new HostAttributeToken ( 'href' ) , { optional :true } ) ;
154+ /**@docs -private */
155+ protected readonly reactiveHref = linkedSignal ( ( ) => {
156+ if ( ! this . isAnchorElement ) {
157+ // Set the initial href value to whatever exists on the host element already
158+ return this . hrefAttributeValue ;
159+ }
160+
161+ const urlTree = this . _urlTree ( ) ;
162+ return urlTree !== null && this . locationStrategy
163+ ?( this . locationStrategy ?. prepareExternalUrl ( this . router . serializeUrl ( urlTree ) ) ?? '' )
164+ :null ;
165+ } ) ;
154166/**
155167 * Represents an `href` attribute value applied to a host element,
156168 * when a host element is an `<a>`/`<area>` tag or a compatible custom element.
@@ -169,7 +181,13 @@ export class RouterLink implements OnChanges, OnDestroy {
169181 * This is only used when the host element is
170182 * an `<a>`/`<area>` tag or a compatible custom element.
171183 */
172- @HostBinding ( 'attr.target' ) @Input ( ) target ?:string ;
184+ @Input ( ) target ?:string ;
185+
186+ /**@docs -private @internal */
187+ protected _target = computed ( ( ) => {
188+ this . changes ( ) ; // track input changes
189+ return this . target ;
190+ } ) ;
173191
174192/**
175193 * Passed to {@link Router#createUrlTree} as part of the
@@ -217,16 +235,38 @@ export class RouterLink implements OnChanges, OnDestroy {
217235 */
218236 @Input ( ) relativeTo ?:ActivatedRoute | null ;
219237
220- /** Whether a host element is an `<a>`/`<area>` tag or a compatible custom element. */
221- private isAnchorElement :boolean ;
238+ /**
239+ * Passed to {@link Router#createUrlTree} as part of the
240+ * `UrlCreationOptions`.
241+ *@see {@link UrlCreationOptions#preserveFragment }
242+ *@see {@link Router#createUrlTree }
243+ */
244+ @Input ( { transform :booleanAttribute } ) preserveFragment :boolean = false ;
222245
223- private subscription ?:Subscription ;
246+ /**
247+ * Passed to {@link Router#navigateByUrl} as part of the
248+ * `NavigationBehaviorOptions`.
249+ *@see {@link NavigationBehaviorOptions#skipLocationChange }
250+ *@see {@link Router#navigateByUrl }
251+ */
252+ @Input ( { transform :booleanAttribute } ) skipLocationChange :boolean = false ;
224253
254+ /**
255+ * Passed to {@link Router#navigateByUrl} as part of the
256+ * `NavigationBehaviorOptions`.
257+ *@see {@link NavigationBehaviorOptions#replaceUrl }
258+ *@see {@link Router#navigateByUrl }
259+ */
260+ @Input ( { transform :booleanAttribute } ) replaceUrl :boolean = false ;
261+
262+ /** Whether a host element is an `<a>`/`<area>` tag or a compatible custom element. */
263+ private readonly isAnchorElement :boolean ;
264+ private subscription ?:Subscription ;
225265/**@internal */
226266onChanges = new Subject < RouterLink > ( ) ;
227-
267+ // This is a hack around not having signal inputs.
268+ private readonly changes = signal ( { } ) ;
228269private readonly applicationErrorHandler = inject ( ɵINTERNAL_APPLICATION_ERROR_HANDLER ) ;
229- private readonly options = inject ( ROUTER_CONFIGURATION , { optional :true } ) ;
230270
231271constructor (
232272private router :Router ,
@@ -236,8 +276,6 @@ export class RouterLink implements OnChanges, OnDestroy {
236276private readonly el :ElementRef ,
237277private locationStrategy ?:LocationStrategy ,
238278) {
239- // Set the initial href value to whatever exists on the host element already
240- this . reactiveHref . set ( inject ( new HostAttributeToken ( 'href' ) , { optional :true } ) ) ;
241279const tagName = el . nativeElement . tagName ?. toLowerCase ( ) ;
242280this . isAnchorElement =
243281tagName === 'a' ||
@@ -254,61 +292,28 @@ export class RouterLink implements OnChanges, OnDestroy {
254292)
255293) ;
256294
257- if ( ! this . isAnchorElement ) {
258- this . subscribeToNavigationEventsIfNecessary ( ) ;
259- } else {
260- this . setTabIndexIfNotOnNativeEl ( '0' ) ;
261- }
262- }
263-
264- private subscribeToNavigationEventsIfNecessary ( ) {
265- if ( this . subscription !== undefined || ! this . isAnchorElement ) {
266- return ;
267- }
295+ this . setTabIndexIfNotOnNativeEl ( '0' ) ;
268296
269- // preserving fragment in router state
270- let createSubcription = this . preserveFragment ;
271- // preserving or merging with query params in router state
272- const dependsOnRouterState = ( handling ?:QueryParamsHandling | null ) =>
273- handling === 'merge' || handling === 'preserve' ;
274- createSubcription ||= dependsOnRouterState ( this . queryParamsHandling ) ;
275- createSubcription ||=
276- ! this . queryParamsHandling && ! dependsOnRouterState ( this . options ?. defaultQueryParamsHandling ) ;
277- if ( ! createSubcription ) {
278- return ;
297+ if ( ngDevMode ) {
298+ effect ( ( ) => {
299+ this . changes ( ) ; // track input changes
300+ if (
301+ isUrlTree ( this . routerLinkInput ) &&
302+ ( this . fragment !== undefined ||
303+ this . queryParams ||
304+ this . queryParamsHandling ||
305+ this . preserveFragment ||
306+ this . relativeTo )
307+ ) {
308+ throw new RuntimeError (
309+ RuntimeErrorCode . INVALID_ROUTER_LINK_INPUTS ,
310+ 'Cannot configure queryParams or fragment when using a UrlTree as the routerLink input value.' ,
311+ ) ;
312+ }
313+ } ) ;
279314}
280-
281- this . subscription = this . router . events . subscribe ( ( s :Event ) => {
282- if ( s instanceof NavigationEnd ) {
283- this . updateHref ( ) ;
284- }
285- } ) ;
286315}
287316
288- /**
289- * Passed to {@link Router#createUrlTree} as part of the
290- * `UrlCreationOptions`.
291- *@see {@link UrlCreationOptions#preserveFragment }
292- *@see {@link Router#createUrlTree }
293- */
294- @Input ( { transform :booleanAttribute } ) preserveFragment :boolean = false ;
295-
296- /**
297- * Passed to {@link Router#navigateByUrl} as part of the
298- * `NavigationBehaviorOptions`.
299- *@see {@link NavigationBehaviorOptions#skipLocationChange }
300- *@see {@link Router#navigateByUrl }
301- */
302- @Input ( { transform :booleanAttribute } ) skipLocationChange :boolean = false ;
303-
304- /**
305- * Passed to {@link Router#navigateByUrl} as part of the
306- * `NavigationBehaviorOptions`.
307- *@see {@link NavigationBehaviorOptions#replaceUrl }
308- *@see {@link Router#navigateByUrl }
309- */
310- @Input ( { transform :booleanAttribute } ) replaceUrl :boolean = false ;
311-
312317/**
313318 * Modifies the tab index if there was not a tabindex attribute on the element during
314319 * instantiation.
@@ -323,24 +328,7 @@ export class RouterLink implements OnChanges, OnDestroy {
323328/**@docs -private */
324329// TODO(atscott): Remove changes parameter in major version as a breaking change.
325330ngOnChanges ( changes ?:SimpleChanges ) :void {
326- if (
327- ngDevMode &&
328- isUrlTree ( this . routerLinkInput ) &&
329- ( this . fragment !== undefined ||
330- this . queryParams ||
331- this . queryParamsHandling ||
332- this . preserveFragment ||
333- this . relativeTo )
334- ) {
335- throw new RuntimeError (
336- RuntimeErrorCode . INVALID_ROUTER_LINK_INPUTS ,
337- 'Cannot configure queryParams or fragment when using a UrlTree as the routerLink input value.' ,
338- ) ;
339- }
340- if ( this . isAnchorElement ) {
341- this . updateHref ( ) ;
342- this . subscribeToNavigationEventsIfNecessary ( ) ;
343- }
331+ this . changes . set ( { } ) ;
344332// This is subscribed to by `RouterLinkActive` so that it knows to update when there are changes
345333// to the RouterLinks it's tracking.
346334this . onChanges . next ( this ) ;
@@ -427,15 +415,6 @@ export class RouterLink implements OnChanges, OnDestroy {
427415this . subscription ?. unsubscribe ( ) ;
428416}
429417
430- private updateHref ( ) :void {
431- const urlTree = this . urlTree ;
432- this . reactiveHref . set (
433- urlTree !== null && this . locationStrategy
434- ?( this . locationStrategy ?. prepareExternalUrl ( this . router . serializeUrl ( urlTree ) ) ?? '' )
435- :null ,
436- ) ;
437- }
438-
439418private applyAttributeValue ( attrName :string , attrValue :string | null ) {
440419const renderer = this . renderer ;
441420const nativeElement = this . el . nativeElement ;
@@ -446,21 +425,32 @@ export class RouterLink implements OnChanges, OnDestroy {
446425}
447426}
448427
428+ /**@internal */
429+ _urlTree = linkedSignal ( {
430+ source :( ) => {
431+ this . changes ( ) ; // Recompute source signal whenever inputs change.
432+
433+ const routerLinkInput = this . routerLinkInput ;
434+ if ( routerLinkInput === null ) {
435+ return signal ( null ) ;
436+ } else if ( isUrlTree ( routerLinkInput ) ) {
437+ return signal ( routerLinkInput ) ;
438+ }
439+ return this . router . createComputedUrlTree ( routerLinkInput , {
440+ // If the `relativeTo` input is not defined, we want to use `this.route` by default.
441+ // Otherwise, we should use the value provided by the user in the input.
442+ relativeTo :this . relativeTo !== undefined ?this . relativeTo :this . route ,
443+ queryParams :this . queryParams ,
444+ fragment :this . fragment ,
445+ queryParamsHandling :this . queryParamsHandling ,
446+ preserveFragment :this . preserveFragment ,
447+ } ) ;
448+ } ,
449+ computation :( v ) => v ( ) ,
450+ } ) ;
451+
449452get urlTree ( ) :UrlTree | null {
450- if ( this . routerLinkInput === null ) {
451- return null ;
452- } else if ( isUrlTree ( this . routerLinkInput ) ) {
453- return this . routerLinkInput ;
454- }
455- return this . router . createUrlTree ( this . routerLinkInput , {
456- // If the `relativeTo` input is not defined, we want to use `this.route` by default.
457- // Otherwise, we should use the value provided by the user in the input.
458- relativeTo :this . relativeTo !== undefined ?this . relativeTo :this . route ,
459- queryParams :this . queryParams ,
460- fragment :this . fragment ,
461- queryParamsHandling :this . queryParamsHandling ,
462- preserveFragment :this . preserveFragment ,
463- } ) ;
453+ return untracked ( this . _urlTree ) ;
464454}
465455}
466456