66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { ChangeDetectorRef , ComponentRef , Directive , EnvironmentInjector , EventEmitter , inject , Injector , Input , OnDestroy , OnInit , Output , SimpleChanges , ViewContainerRef , ɵRuntimeError as RuntimeError , } from '@angular/core' ;
9+ import { ChangeDetectorRef , ComponentRef , Directive , EnvironmentInjector , EventEmitter , inject , Injectable , InjectionToken , Injector , Input , OnDestroy , OnInit , Output , reflectComponentType , SimpleChanges , ViewContainerRef , ɵRuntimeError as RuntimeError , } from '@angular/core' ;
10+ import { combineLatest , Subscription } from 'rxjs' ;
11+ import { switchMap } from 'rxjs/operators' ;
1012
1113import { RuntimeErrorCode } from '../errors' ;
1214import { Data } from '../models' ;
@@ -96,6 +98,16 @@ export interface RouterOutletContract {
9698 * subtree.
9799 */
98100 detachEvents ?: EventEmitter < unknown > ;
101+
102+ /**
103+ * Used to indicate that the outlet is able to bind data from the `Router` to the outlet
104+ * component's inputs.
105+ *
106+ * When this is `undefined` or `false` and the developer has opted in to the
107+ * feature using `withComponentInputBinding`, a warning will be logged in dev mode if this outlet
108+ * is used in the application.
109+ */
110+ readonly supportsBindingToComponentInputs ?: true ;
99111}
100112
101113/**
@@ -156,6 +168,10 @@ export interface RouterOutletContract {
156168} )
157169export class RouterOutlet implements OnDestroy , OnInit , RouterOutletContract {
158170 private activated : ComponentRef < any > | null = null ;
171+ /** @internal */
172+ get activatedComponentRef ( ) : ComponentRef < any > | null {
173+ return this . activated ;
174+ }
159175 private _activatedRoute : ActivatedRoute | null = null ;
160176 /**
161177 * The name of the outlet
@@ -181,6 +197,9 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
181197 private location = inject ( ViewContainerRef ) ;
182198 private changeDetector = inject ( ChangeDetectorRef ) ;
183199 private environmentInjector = inject ( EnvironmentInjector ) ;
200+ private inputBinder = inject ( INPUT_BINDER , { optional : true } ) ;
201+ /** @nodoc */
202+ readonly supportsBindingToComponentInputs = true ;
184203
185204 /** @nodoc */
186205 ngOnChanges ( changes : SimpleChanges ) {
@@ -208,6 +227,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
208227 if ( this . isTrackedInParentContexts ( this . name ) ) {
209228 this . parentContexts . onChildOutletDestroyed ( this . name ) ;
210229 }
230+ this . inputBinder ?. unsubscribeFromRouteData ( this ) ;
211231 }
212232
213233 private isTrackedInParentContexts ( outletName : string ) {
@@ -293,6 +313,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
293313 this . activated = ref ;
294314 this . _activatedRoute = activatedRoute ;
295315 this . location . insert ( ref . hostView ) ;
316+ this . inputBinder ?. bindActivatedRouteToOutletComponent ( this ) ;
296317 this . attachEvents . emit ( ref . instance ) ;
297318 }
298319
@@ -328,6 +349,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
328349 // Calling `markForCheck` to make sure we will run the change detection when the
329350 // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
330351 this . changeDetector . markForCheck ( ) ;
352+ this . inputBinder ?. bindActivatedRouteToOutletComponent ( this ) ;
331353 this . activateEvents . emit ( this . activated . instance ) ;
332354 }
333355}
@@ -349,3 +371,70 @@ class OutletInjector implements Injector {
349371 return this . parent . get ( token , notFoundValue ) ;
350372 }
351373}
374+
375+ export const INPUT_BINDER = new InjectionToken < RoutedComponentInputBinder > ( '' ) ;
376+
377+ /**
378+ * Injectable used as a tree-shakable provider for opting in to binding router data to component
379+ * inputs.
380+ *
381+ * The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
382+ * activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
383+ * queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
384+ * Importantly, when an input does not have an item in the route data with a matching key, this
385+ * input is set to `undefined`. If it were not done this way, the previous information would be
386+ * retained if the data got removed from the route (i.e. if a query parameter is removed).
387+ *
388+ * The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
389+ * the subscriptions are cleaned up.
390+ */
391+ @Injectable ( )
392+ export class RoutedComponentInputBinder {
393+ private outletDataSubscriptions = new Map < RouterOutlet , Subscription > ;
394+
395+ bindActivatedRouteToOutletComponent ( outlet : RouterOutlet ) {
396+ this . unsubscribeFromRouteData ( outlet ) ;
397+ this . subscribeToRouteData ( outlet ) ;
398+ }
399+
400+ unsubscribeFromRouteData ( outlet : RouterOutlet ) {
401+ this . outletDataSubscriptions . get ( outlet ) ?. unsubscribe ( ) ;
402+ this . outletDataSubscriptions . delete ( outlet ) ;
403+ }
404+
405+ private subscribeToRouteData ( outlet : RouterOutlet ) {
406+ const { activatedRoute} = outlet ;
407+ const dataSubscription =
408+ combineLatest ( [
409+ activatedRoute . queryParams ,
410+ activatedRoute . params ,
411+ activatedRoute . data ,
412+ ] )
413+ . pipe ( switchMap ( ( [ queryParams , params , data ] ) => {
414+ // Promise.resolve is used to avoid synchronously writing the wrong data when two of
415+ // the Observables in the `combineLatest` stream emit one after another.
416+ return Promise . resolve ( { ...queryParams , ...params , ...data } ) ;
417+ } ) )
418+ . subscribe ( data => {
419+ // Outlet may have been deactivated or changed names to be associated with a different
420+ // route
421+ if ( ! outlet . isActivated || ! outlet . activatedComponentRef ||
422+ outlet . activatedRoute !== activatedRoute || activatedRoute . component === null ) {
423+ this . unsubscribeFromRouteData ( outlet ) ;
424+ return ;
425+ }
426+
427+ const mirror = reflectComponentType ( activatedRoute . component ) ;
428+ if ( ! mirror ) {
429+ this . unsubscribeFromRouteData ( outlet ) ;
430+ return ;
431+ }
432+
433+ for ( const { templateName} of mirror . inputs ) {
434+ outlet . activatedComponentRef . setInput ( templateName , data [ templateName ] ) ;
435+ }
436+ } ) ;
437+
438+ this . outletDataSubscriptions . set ( outlet , dataSubscription ) ;
439+ }
440+ }
0 commit comments