mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(router): add unmatchedInputBehavior option to componentInputBinding
Introduce a new configuration option `unmatchedInputBehavior` to the `componentInputBinding` feature. This option allows users to configure the behavior when a component input is not matched by any key in the router data. The available values are: - 'alwaysUndefined': (Default) Always binds undefined to unmatched inputs. - 'undefinedIfStale': Binds undefined only if the input was previously available in the router data for the active route in the outlet. This feature addresses concerns raised in #63835 and #52946 regarding the retention of default values for inputs that were never targeted by the router, while still ensuring that stale data is cleared when a parameter is removed.
This commit is contained in:
parent
aab7dd48c0
commit
c84642ac16
5 changed files with 83 additions and 2 deletions
|
|
@ -66,6 +66,28 @@ Use `ComponentInputBindingOptions` to disable query parameter binding if you man
|
|||
provideRouter(appRoutes, withComponentInputBinding({queryParams: false}));
|
||||
```
|
||||
|
||||
### Configure behavior for inputs not available in router data
|
||||
|
||||
By default, the router sets an input to `undefined` if it was not available in the router data during a navigation. This ensures that stale data is not retained.
|
||||
|
||||
If you want to avoid setting `undefined` for inputs that have _never_ been available in the router data for the active component instance, you can set the `unmatchedInputBehavior` option to `'undefinedIfStale'`:
|
||||
|
||||
```ts
|
||||
provideRouter(appRoutes, withComponentInputBinding({unmatchedInputBehavior: 'undefinedIfStale'}));
|
||||
```
|
||||
|
||||
When you combine `unmatchedInputBehavior: 'undefinedIfStale'` with `queryParams: false`, inputs retain their initial values unless they are explicitly provided by the router. The exception is matrix parameters: if a matrix parameter is provided in one navigation and removed in a subsequent one, the router will set the input to `undefined` to avoid retaining stale data.
|
||||
|
||||
```ts
|
||||
provideRouter(
|
||||
appRoutes,
|
||||
withComponentInputBinding({
|
||||
queryParams: false,
|
||||
unmatchedInputBehavior: 'undefinedIfStale',
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### Inherit parent route data
|
||||
|
||||
By default, child routes inherit parameters and data from parent routes (equivalent to `paramsInheritanceStrategy: 'always'`). This means you can access parent route info directly in child components.
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ export type ComponentInputBindingFeature = RouterFeature<RouterFeatureKind.Compo
|
|||
// @public
|
||||
export interface ComponentInputBindingOptions {
|
||||
queryParams?: boolean;
|
||||
unmatchedInputBehavior?: 'alwaysUndefined' | 'undefinedIfStale';
|
||||
}
|
||||
|
||||
// @public
|
||||
|
|
|
|||
|
|
@ -448,8 +448,9 @@ export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>(
|
|||
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
|
||||
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
|
||||
* Importantly, when an input does not have an item in the route data with a matching key, this
|
||||
* input is set to `undefined`. If it were not done this way, the previous information would be
|
||||
* input is set to `undefined` by default. If it were not done this way, the previous information would be
|
||||
* retained if the data got removed from the route (i.e. if a query parameter is removed).
|
||||
* The `unmatchedInputBehavior` option can be used to configure this behavior.
|
||||
*
|
||||
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
|
||||
* the subscriptions are cleaned up.
|
||||
|
|
@ -457,6 +458,7 @@ export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>(
|
|||
@Injectable()
|
||||
export class RoutedComponentInputBinder {
|
||||
private outletDataSubscriptions = new Map<RouterOutlet, Subscription>();
|
||||
private outletSeenKeys = new Map<RouterOutlet, Set<string>>();
|
||||
|
||||
constructor(private options: ComponentInputBindingOptions) {
|
||||
this.options.queryParams ??= true;
|
||||
|
|
@ -470,6 +472,7 @@ export class RoutedComponentInputBinder {
|
|||
unsubscribeFromRouteData(outlet: RouterOutlet): void {
|
||||
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
|
||||
this.outletDataSubscriptions.delete(outlet);
|
||||
this.outletSeenKeys.delete(outlet);
|
||||
}
|
||||
|
||||
private subscribeToRouteData(outlet: RouterOutlet) {
|
||||
|
|
@ -512,8 +515,23 @@ export class RoutedComponentInputBinder {
|
|||
return;
|
||||
}
|
||||
|
||||
let seenKeys = this.outletSeenKeys.get(outlet);
|
||||
if (!seenKeys) {
|
||||
seenKeys = new Set<string>();
|
||||
this.outletSeenKeys.set(outlet, seenKeys);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
seenKeys.add(key);
|
||||
}
|
||||
|
||||
const behavior = this.options.unmatchedInputBehavior ?? 'alwaysUndefined';
|
||||
|
||||
for (const {templateName} of mirror.inputs) {
|
||||
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
||||
const value = data[templateName];
|
||||
if (value !== undefined || behavior === 'alwaysUndefined' || seenKeys.has(templateName)) {
|
||||
outlet.activatedComponentRef.setInput(templateName, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -208,6 +208,17 @@ export interface ComponentInputBindingOptions {
|
|||
* inputs.
|
||||
*/
|
||||
queryParams?: boolean;
|
||||
|
||||
/**
|
||||
* Configures the behavior when an input is not matched by any key in the router data.
|
||||
*
|
||||
* - `'alwaysUndefined'`: (Default) Binds `undefined` to the input. This ensures that stale data
|
||||
* is not retained.
|
||||
* - `'undefinedIfStale'`: Binds `undefined` only if the input was previously available
|
||||
* in the router data during the lifetime of the active route in this outlet. This avoids
|
||||
* setting `undefined` for inputs that were never expected to be set by the router.
|
||||
*/
|
||||
unmatchedInputBehavior?: 'alwaysUndefined' | 'undefinedIfStale';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -218,6 +218,35 @@ describe('component input binding', () => {
|
|||
expect(instance.language).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('omits binding undefined to inputs not available in router data if never available', async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: false,
|
||||
})
|
||||
class MyComponent {
|
||||
@Input() language: string | undefined = 'default';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(
|
||||
[{path: '**', component: MyComponent}],
|
||||
withComponentInputBinding({unmatchedInputBehavior: 'undefinedIfStale'}),
|
||||
),
|
||||
],
|
||||
});
|
||||
const harness = await RouterTestingHarness.create();
|
||||
|
||||
const instance = await harness.navigateByUrl('/', MyComponent);
|
||||
expect(instance.language).toEqual('default');
|
||||
|
||||
await harness.navigateByUrl('/?language=english');
|
||||
expect(instance.language).toEqual('english');
|
||||
|
||||
await harness.navigateByUrl('/');
|
||||
expect(instance.language).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('does not set component inputs from matching query params when queryParam inputs are disabled', async () => {
|
||||
@Component({
|
||||
template: '',
|
||||
|
|
|
|||
Loading…
Reference in a new issue