Skip to content

Commit 89c9a4d

Browse files
Jordan Webstermattrbeck
authored andcommitted
feat(router): Add options optional parameter for withComponentInputBinding
Add `ComponentInputBindingOptions` which is used with `withComponentInputBinding` and `bindToComponentInputs` Can set which sources to bind as follows: * queryParams * params * data feat(router): Add `options` optional parameter for `withComponentInputBinding` Add missing ternary operator for queryParams
1 parent 8edcf41 commit 89c9a4d

File tree

6 files changed

+127
-13
lines changed

6 files changed

+127
-13
lines changed

goldens/public-api/router/index.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export enum EventType {
300300

301301
// @public
302302
export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOptions {
303-
bindToComponentInputs?: boolean;
303+
bindToComponentInputs?: boolean | ComponentInputBindingOptions;
304304
enableTracing?: boolean;
305305
enableViewTransitions?: boolean;
306306
errorHandler?: (error: any) => RedirectCommand | any;
@@ -1133,7 +1133,7 @@ export interface ViewTransitionsFeatureOptions {
11331133
}
11341134

11351135
// @public
1136-
export function withComponentInputBinding(): ComponentInputBindingFeature;
1136+
export function withComponentInputBinding(options?: ComponentInputBindingOptions): ComponentInputBindingFeature;
11371137

11381138
// @public
11391139
export function withDebugTracing(): DebugTracingFeature;

packages/router/src/directives/router_outlet.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import {
2727
SimpleChanges,
2828
ViewContainerRef,
2929
} from '@angular/core';
30-
import {combineLatest, of, Subscription} from 'rxjs';
30+
import {combineLatest, Observable, of, Subscription} from 'rxjs';
3131
import {switchMap} from 'rxjs/operators';
3232

3333
import {RuntimeErrorCode} from '../errors';
3434
import {Data} from '../models';
3535
import {ChildrenOutletContexts} from '../router_outlet_context';
3636
import {ActivatedRoute} from '../router_state';
37-
import {PRIMARY_OUTLET} from '../shared';
37+
import {Params, PRIMARY_OUTLET} from '../shared';
38+
import {ComponentInputBindingOptions} from '../router_config';
3839

3940
/**
4041
* An `InjectionToken` provided by the `RouterOutlet` and can be set using the `routerOutletData`
@@ -457,6 +458,10 @@ export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>(
457458
export class RoutedComponentInputBinder {
458459
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>();
459460

461+
constructor(private options: ComponentInputBindingOptions) {
462+
this.options.queryParams ??= true;
463+
}
464+
460465
bindActivatedRouteToOutletComponent(outlet: RouterOutlet): void {
461466
this.unsubscribeFromRouteData(outlet);
462467
this.subscribeToRouteData(outlet);
@@ -470,7 +475,7 @@ export class RoutedComponentInputBinder {
470475
private subscribeToRouteData(outlet: RouterOutlet) {
471476
const {activatedRoute} = outlet;
472477
const dataSubscription = combineLatest([
473-
activatedRoute.queryParams,
478+
this.options.queryParams ? activatedRoute.queryParams : of({}),
474479
activatedRoute.params,
475480
activatedRoute.data,
476481
])

packages/router/src/provide_router.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ import {RedirectCommand, Routes} from './models';
4141
import {NAVIGATION_ERROR_HANDLER, NavigationTransitions} from './navigation_transition';
4242
import {ROUTE_INJECTOR_CLEANUP, routeInjectorCleanup} from './route_injector_cleanup';
4343
import {Router} from './router';
44-
import {InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
44+
import {
45+
ComponentInputBindingOptions,
46+
InMemoryScrollingOptions,
47+
ROUTER_CONFIGURATION,
48+
RouterConfigOptions,
49+
} from './router_config';
4550
import {ROUTES} from './router_config_loader';
4651
import {PreloadingStrategy, RouterPreloader} from './router_preloader';
4752

@@ -778,7 +783,8 @@ export type ViewTransitionsFeature = RouterFeature<RouterFeatureKind.ViewTransit
778783

779784
/**
780785
* Enables binding information from the `Router` state directly to the inputs of the component in
781-
* `Route` configurations.
786+
* `Route` configurations. Can also accept an `ComponentInputBindingOptions` object to set which
787+
* sources are allowed to bind.
782788
*
783789
* @usageNotes
784790
*
@@ -811,13 +817,27 @@ export type ViewTransitionsFeature = RouterFeature<RouterFeatureKind.ViewTransit
811817
* Default values can be provided with a resolver on the route to ensure the value is always present
812818
* or an input and use an input transform in the component.
813819
*
820+
* Advanced example of how you can disable binding from certain sources:
821+
* ```ts
822+
* const appRoutes: Routes = [];
823+
* bootstrapApplication(AppComponent,
824+
* {
825+
* providers: [
826+
* provideRouter(appRoutes, withComponentInputBinding({queryParams: false}))
827+
* ]
828+
* }
829+
* );
830+
* ```
831+
*
814832
* @see {@link /guide/components/inputs#input-transforms Input Transforms}
833+
* @see {@link ComponentInputBindingOptions}
815834
* @returns A set of providers for use with `provideRouter`.
816835
*/
817-
export function withComponentInputBinding(): ComponentInputBindingFeature {
836+
export function withComponentInputBinding(
837+
options: ComponentInputBindingOptions = {},
838+
): ComponentInputBindingFeature {
818839
const providers = [
819-
RoutedComponentInputBinder,
820-
{provide: INPUT_BINDER, useExisting: RoutedComponentInputBinder},
840+
{provide: INPUT_BINDER, useFactory: () => new RoutedComponentInputBinder(options)},
821841
];
822842

823843
return routerFeature(RouterFeatureKind.ComponentInputBindingFeature, providers);

packages/router/src/router_config.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,22 @@ export interface InMemoryScrollingOptions {
193193
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
194194
}
195195

196+
/**
197+
* Configuration options for the component input binding feature which can be used
198+
* with `withComponentInputBinding` function or `RouterModule.forRoot`
199+
*
200+
* @publicApi
201+
* @see withComponentInputBinding
202+
* @see RouterModule#forRoot
203+
*/
204+
export interface ComponentInputBindingOptions {
205+
/**
206+
* When true (default), will configure query parameters to bind to component
207+
* inputs.
208+
*/
209+
queryParams?: boolean;
210+
}
211+
196212
/**
197213
* A set of configuration options for a router module, provided in the
198214
* `forRoot()` method.
@@ -231,9 +247,10 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
231247

232248
/**
233249
* When true, enables binding information from the `Router` state directly to the inputs of the
234-
* component in `Route` configurations.
250+
* component in `Route` configurations. Can also accept an `ComponentInputBindingOptions` object
251+
* to set whether to exclude queryParams from binding.
235252
*/
236-
bindToComponentInputs?: boolean;
253+
bindToComponentInputs?: boolean | ComponentInputBindingOptions;
237254

238255
/**
239256
* When true, enables view transitions in the Router by running the route activation and

packages/router/src/router_module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,11 @@ export class RouterModule {
152152
provideRouterScroller(),
153153
config?.preloadingStrategy ? withPreloading(config.preloadingStrategy).ɵproviders : [],
154154
config?.initialNavigation ? provideInitialNavigation(config) : [],
155-
config?.bindToComponentInputs ? withComponentInputBinding().ɵproviders : [],
155+
config?.bindToComponentInputs
156+
? withComponentInputBinding(
157+
typeof config.bindToComponentInputs === 'object' ? config.bindToComponentInputs : {},
158+
).ɵproviders
159+
: [],
156160
config?.enableViewTransitions ? withViewTransitions().ɵproviders : [],
157161
provideRouterInitializer(),
158162
],

packages/router/test/directives/router_outlet.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,29 @@ describe('component input binding', () => {
218218
expect(instance.language).toEqual(undefined);
219219
});
220220

221+
it('does not set component inputs from matching query params when queryParam inputs are disabled', async () => {
222+
@Component({
223+
template: '',
224+
standalone: false,
225+
})
226+
class MyComponent {
227+
@Input() language?: string;
228+
}
229+
230+
TestBed.configureTestingModule({
231+
providers: [
232+
provideRouter(
233+
[{path: '**', component: MyComponent}],
234+
withComponentInputBinding({queryParams: false}),
235+
),
236+
],
237+
});
238+
const harness = await RouterTestingHarness.create();
239+
240+
const instance = await harness.navigateByUrl('/?language=french', MyComponent);
241+
expect(instance.language).toEqual(undefined);
242+
});
243+
221244
it('sets component inputs from resolved and static data', async () => {
222245
@Component({
223246
template: '',
@@ -315,6 +338,51 @@ describe('component input binding', () => {
315338
expect(instance.result).toEqual('from query params');
316339
});
317340

341+
it('when keys conflict, sets inputs based on priority: data > path params > query params, with queryParams disabled', async () => {
342+
@Component({
343+
template: '',
344+
standalone: false,
345+
})
346+
class MyComponent {
347+
@Input() result?: string;
348+
}
349+
350+
TestBed.configureTestingModule({
351+
providers: [
352+
provideRouter(
353+
[
354+
{
355+
path: 'withData',
356+
component: MyComponent,
357+
data: {'result': 'from data'},
358+
},
359+
{
360+
path: 'withoutData',
361+
component: MyComponent,
362+
},
363+
],
364+
withComponentInputBinding({queryParams: false}),
365+
),
366+
],
367+
});
368+
const harness = await RouterTestingHarness.create();
369+
370+
let instance = await harness.navigateByUrl(
371+
'/withData;result=from path param?result=from query params',
372+
MyComponent,
373+
);
374+
expect(instance.result).toEqual('from data');
375+
376+
// Same component, different instance because it's a different route
377+
instance = await harness.navigateByUrl(
378+
'/withoutData;result=from path param?result=from query params',
379+
MyComponent,
380+
);
381+
expect(instance.result).toEqual('from path param');
382+
instance = await harness.navigateByUrl('/withoutData?result=from query params', MyComponent);
383+
expect(instance.result).toEqual(undefined);
384+
});
385+
318386
it('does not write multiple times if two sources of conflicting keys both update', async () => {
319387
let resultLog: Array<string | undefined> = [];
320388
@Component({

0 commit comments

Comments
 (0)