Skip to content

Commit c42c212

Browse files
committed
feat: supporting input binding feature
supporting input binding feature for router outlet and adding demo page for testing
1 parent 88f1254 commit c42c212

9 files changed

+213
-3
lines changed

apps/nativescript-demo-ng/src/app/app-routing.module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { NgModule } from '@angular/core';
22
import { Routes } from '@angular/router';
33
import { NativeScriptRouterModule } from '@nativescript/angular';
4-
4+
import { dummyDataResolverTsResolver } from './input-bidings/dummy-data.resolver.ts.resolver';
55
import { ItemDetailComponent } from './item/item-detail.component';
66
import { ItemsComponent } from './item/items.component';
77
// import { HomeComponent } from './home/home.component';
88
// import { BootGuardService } from './boot-guard.service';
99

1010
const routes: Routes = [
11-
{ path: '', redirectTo: '/rootlazy', pathMatch: 'full' },
11+
{ path: '', redirectTo: '/redirect', pathMatch: 'full' },
1212
{ path: 'items', component: ItemsComponent },
1313
{ path: 'item/:id', component: ItemDetailComponent },
1414
{ path: 'item2', loadChildren: () => import('./item2/item2.module').then((m) => m.Item2Module) },
1515
{ path: 'rootlazy', loadChildren: () => import('./item3/item3.module').then((m) => m.Item3Module) },
16+
{ path: 'redirect', loadComponent: () => import('./input-bidings/redirect-page.component').then((c) => c.RedirectPage) },
17+
{ path: 'bindings/:name', loadComponent: () => import('./input-bidings/input-bidings.component').then((c) => c.InputBidingsComponent), resolve: {
18+
data: dummyDataResolverTsResolver
19+
} },
1620

1721
/**
1822
* Test tab named outlets
@@ -38,7 +42,9 @@ const routes: Routes = [
3842
];
3943

4044
@NgModule({
41-
imports: [NativeScriptRouterModule.forRoot(routes)],
45+
imports: [NativeScriptRouterModule.forRoot(routes, {
46+
bindToComponentInputs: true,
47+
})],
4248
exports: [NativeScriptRouterModule],
4349
})
4450
export class AppRoutingModule {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ResolveFn } from '@angular/router';
2+
import { of } from 'rxjs';
3+
4+
export const dummyDataResolverTsResolver: ResolveFn<string[]> = (route, state) => {
5+
return of(['Name1', 'Name2', "Name3"]);
6+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host {
2+
display: block;
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
<StackLayout>
3+
<Label [text]="'Name (routerparam): '+name()"></Label>
4+
<Label [text]="'Id (queryparam): '+id()"></Label>
5+
@for(name of data(); track name) {
6+
<Label [text]="'item - '+name"></Label>
7+
}
8+
<Button text="Redirect to other route" (tap)="navigationTo()"></Button>
9+
</StackLayout>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ChangeDetectionStrategy, Component, inject, input, NO_ERRORS_SCHEMA } from '@angular/core';
2+
import { NativeScriptCommonModule, RouterExtensions } from '../../../../../packages/angular/src';
3+
4+
@Component({
5+
selector: 'app-input-bidings',
6+
standalone: true,
7+
imports: [
8+
NativeScriptCommonModule,
9+
],
10+
templateUrl: `./input-bidings.component.html`,
11+
styleUrl: './input-bidings.component.css',
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
schemas: [NO_ERRORS_SCHEMA]
14+
})
15+
export class InputBidingsComponent {
16+
17+
protected readonly router = inject(RouterExtensions)
18+
19+
name = input(); //Route param
20+
21+
id = input(); //Query param
22+
23+
data = input<string[]>(); // Resolver
24+
25+
navigationTo() {
26+
this.router.navigate(['/bindings/testing2/'], { queryParams: { id: 10}})
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ChangeDetectionStrategy, Component, inject, NO_ERRORS_SCHEMA } from "@angular/core"
2+
import { NativeScriptCommonModule, NativeScriptRouterModule, RouterExtensions } from "../../../../../packages/angular/src"
3+
4+
@Component({
5+
selector: 'app-input-bidings',
6+
standalone: true,
7+
imports: [
8+
NativeScriptCommonModule, NativeScriptRouterModule,
9+
],
10+
template: `<Label text="Redirecting">`,
11+
changeDetection: ChangeDetectionStrategy.OnPush,
12+
schemas: [NO_ERRORS_SCHEMA]
13+
})
14+
export class RedirectPage {
15+
16+
protected readonly router = inject(RouterExtensions)
17+
18+
constructor() {
19+
this.router.navigate(['/bindings/test/'], { queryParams: { id: 1}, clearHistory: true})
20+
}
21+
}
22+

packages/angular/src/lib/legacy/router/page-router-outlet.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { findTopActivatedRouteNodeForOutlet, pageRouterActivatedSymbol, loaderRe
1616
import { registerElement } from '../../element-registry';
1717
import { PageService } from '../../cdk/frame-page/page.service';
1818
import { ExtendedNavigationExtras } from './router-extensions';
19+
import { INPUT_BINDER } from '../../router/router-component-input-binder';
1920

2021
export class PageRoute {
2122
activatedRoute: BehaviorSubject<ActivatedRoute>;
@@ -146,6 +147,13 @@ export class PageRouterOutlet implements OnDestroy, RouterOutletContract {
146147
return {};
147148
}
148149

150+
/** @internal */
151+
get activatedComponentRef(): ComponentRef<any> | null {
152+
return this.activated;
153+
}
154+
155+
private inputBinder = inject(INPUT_BINDER, { optional: true });
156+
149157
constructor(
150158
private parentContexts: ChildrenOutletContexts,
151159
private location: ViewContainerRef,
@@ -176,6 +184,7 @@ export class PageRouterOutlet implements OnDestroy, RouterOutletContract {
176184

177185
this.viewUtil = viewUtil;
178186
this.detachedLoaderFactory = resolver.resolveComponentFactory(DetachedLoader);
187+
179188
}
180189

181190
setActionBarVisibility(actionBarVisibility: string): void {
@@ -217,6 +226,8 @@ export class PageRouterOutlet implements OnDestroy, RouterOutletContract {
217226
this.deactivateEvents.emit(c);
218227
this.activated = null;
219228
}
229+
230+
this.inputBinder?.unsubscribeFromRouteData(this);
220231
}
221232

222233
deactivate(): void {
@@ -345,6 +356,7 @@ export class PageRouterOutlet implements OnDestroy, RouterOutletContract {
345356
this.markActivatedRoute(activatedRoute);
346357

347358
this.activateOnGoForward(activatedRoute, resolver || this.environmentInjector);
359+
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
348360
this.activateEvents.emit(this.activated.instance);
349361
}
350362

packages/angular/src/lib/legacy/router/router.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FrameService } from '../frame.service';
1212
import { NSEmptyOutletComponent } from './ns-empty-outlet.component';
1313
import { NativeScriptCommonModule } from '../../nativescript-common.module';
1414
import { START_PATH } from '../../tokens';
15+
import { withComponentInputBinding } from '../../router/router-component-input-binder';
1516

1617
export { PageRoute } from './page-router-outlet';
1718
export { RouterExtensions } from './router-extensions';
@@ -50,6 +51,7 @@ export class NativeScriptRouterModule {
5051
RouterExtensions,
5152
NSRouteReuseStrategy,
5253
{ provide: RouteReuseStrategy, useExisting: NSRouteReuseStrategy },
54+
config?.bindToComponentInputs ? withComponentInputBinding().ɵproviders : [],
5355
],
5456
};
5557
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Injectable, InjectionToken, Provider, reflectComponentType } from "@angular/core";
2+
import { Router, RouterOutlet } from "@angular/router";
3+
import { PageRouterOutlet } from "@nativescript/angular";
4+
import { combineLatest, of, Subscription, switchMap } from "rxjs";
5+
import { NativeScriptDebug } from "../trace";
6+
7+
export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
8+
/**
9+
* Injectable used as a tree-shakable provider for opting in to binding router data to component
10+
* inputs.
11+
*
12+
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
13+
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
14+
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
15+
* Importantly, when an input does not have an item in the route data with a matching key, this
16+
* input is set to `undefined`. If it were not done this way, the previous information would be
17+
* retained if the data got removed from the route (i.e. if a query parameter is removed).
18+
*
19+
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
20+
* the subscriptions are cleaned up.
21+
*/
22+
@Injectable()
23+
export class RoutedComponentInputBinder {
24+
private outletDataSubscriptions = new Map<PageRouterOutlet, Subscription>;
25+
26+
bindActivatedRouteToOutletComponent(outlet: PageRouterOutlet): void {
27+
this.unsubscribeFromRouteData(outlet);
28+
this.subscribeToRouteData(outlet);
29+
}
30+
31+
unsubscribeFromRouteData(outlet: PageRouterOutlet): void {
32+
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
33+
this.outletDataSubscriptions.delete(outlet);
34+
}
35+
36+
private subscribeToRouteData(outlet: PageRouterOutlet) {
37+
38+
const { activatedRoute } = outlet;
39+
40+
if (!activatedRoute) {
41+
if (NativeScriptDebug.isLogEnabled()) {
42+
NativeScriptDebug.routerLog('Outlet is not activatedA');
43+
}
44+
return;
45+
}
46+
47+
const dataSubscription =
48+
combineLatest([
49+
activatedRoute.queryParams,
50+
activatedRoute.params,
51+
activatedRoute.data,
52+
])
53+
.pipe(switchMap(([queryParams, params, data], index) => {
54+
data = { ...queryParams, ...params, ...data };
55+
// Get the first result from the data subscription synchronously so it's available to
56+
// the component as soon as possible (and doesn't require a second change detection).
57+
if (index === 0) {
58+
return of(data);
59+
}
60+
// Promise.resolve is used to avoid synchronously writing the wrong data when
61+
// two of the Observables in the `combineLatest` stream emit one after
62+
// another.
63+
return Promise.resolve(data);
64+
}))
65+
.subscribe(data => {
66+
// Outlet may have been deactivated or changed names to be associated with a different
67+
// route
68+
if (!outlet.isActivated || !outlet.activatedComponentRef ||
69+
outlet.activatedRoute !== activatedRoute || activatedRoute.component === null) {
70+
this.unsubscribeFromRouteData(outlet);
71+
return;
72+
}
73+
74+
const mirror = reflectComponentType(activatedRoute.component);
75+
if (!mirror) {
76+
this.unsubscribeFromRouteData(outlet);
77+
return;
78+
}
79+
80+
for (const { templateName } of mirror.inputs) {
81+
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
82+
}
83+
});
84+
85+
this.outletDataSubscriptions.set(outlet, dataSubscription);
86+
}
87+
}
88+
89+
/**
90+
* Enables binding information from the `Router` state directly to the inputs of the component in
91+
* `Route` configurations.
92+
*
93+
*/
94+
export function withComponentInputBinding(): {
95+
ɵkind: number;
96+
ɵproviders: Provider[];
97+
} {
98+
const providers = [
99+
RoutedComponentInputBinder,
100+
{ provide: INPUT_BINDER, useExisting: RoutedComponentInputBinder },
101+
];
102+
103+
return {
104+
ɵkind: RouterFeatureKind.ComponentInputBindingFeature,
105+
ɵproviders: providers
106+
}
107+
}
108+
109+
/**
110+
* The list of features as an enum to uniquely type each feature.
111+
*/
112+
export const enum RouterFeatureKind {
113+
PreloadingFeature,
114+
DebugTracingFeature,
115+
EnabledBlockingInitialNavigationFeature,
116+
DisabledInitialNavigationFeature,
117+
InMemoryScrollingFeature,
118+
RouterConfigurationFeature,
119+
RouterHashLocationFeature,
120+
NavigationErrorHandlerFeature,
121+
ComponentInputBindingFeature,
122+
}

0 commit comments

Comments
 (0)