Skip to content

Commit 961abd1

Browse files
committed
refactor(docs-infra): use routed component input binding instead of ActivatedRoute
Replaces direct `ActivatedRoute` usage with routed component input binding for query params and resolved route data. Keeps `ActivatedRoute` only where it is still needed for relative navigation.
1 parent 319a3c4 commit 961abd1

7 files changed

Lines changed: 141 additions & 64 deletions

File tree

‎adev/src/app/features/home/home.component.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ <h1 tabindex="-1">Angular v22 is here!</h1>
1313
</div>
1414
</div>
1515

16-
@if (!isUwu) {
16+
@if (!uwu()) {
1717
<div class="title">
1818
<h2>Productivity<br />Meets scalability</h2>
1919
<h3>The framework for building scalable web apps with confidence</h3>

‎adev/src/app/features/home/home.component.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@
88

99
import {Tab, TabContent, TabList, TabPanel, Tabs} from '@angular/aria/tabs';
1010
import {A11yModule} from '@angular/cdk/a11y';
11-
import {afterRenderEffect, Component, ElementRef, inject, signal, viewChild} from '@angular/core';
11+
import {
12+
afterRenderEffect,
13+
booleanAttribute,
14+
Component,
15+
ElementRef,
16+
inject,
17+
input,
18+
signal,
19+
viewChild,
20+
} from '@angular/core';
1221
import {IconComponent, IS_SEARCH_DIALOG_OPEN, TextField} from '@angular/docs';
13-
import {ActivatedRoute, RouterLink} from '@angular/router';
22+
import {RouterLink} from '@angular/router';
1423
import {ControlFlowExample} from './components/control-flow/control-flow-example';
1524
import {DeferrableViewsExample} from './components/deferrable-views-example/deferrable-views-example';
1625
import {HydrationExample} from './components/hydration-example/hydration-example';
@@ -44,8 +53,8 @@ const FEATURE_TAB = {
4453
styleUrls: ['./home.component.scss'],
4554
})
4655
export default class Home {
47-
private readonly activatedRoute = inject(ActivatedRoute);
48-
protected readonly isUwu = 'uwu' in this.activatedRoute.snapshot.queryParams;
56+
/** Bound from the `?uwu` query param, which has an empty string value when present. */
57+
readonly uwu = input(false, {transform: booleanAttribute});
4958

5059
protected readonly displaySearchDialog = inject(IS_SEARCH_DIALOG_OPEN);
5160

‎adev/src/app/features/tutorial/tutorial.component.spec.ts‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class FakeEmbeddedEditor {}
2929
template: '<div>FakeDocsViewer</div>',
3030
})
3131
class FakeDocViewer {
32-
documentFilePath = input<string | undefined>();
32+
docContent = input<string | undefined>();
3333
}
3434

3535
// TODO: export this class, it's a helpful mock we could you on other tests.
@@ -123,10 +123,51 @@ describe('Tutorial', () => {
123123
expect(component).toBeTruthy();
124124
});
125125

126+
it('should set the tutorial data from the route data inputs', async () => {
127+
const parent = {
128+
path: 'tutorials/first-app',
129+
label: 'First App',
130+
tutorialData: {type: TutorialType.LOCAL, title: 'First App', restrictedMode: false},
131+
};
132+
fixture.componentRef.setInput('path', 'tutorials/first-app/steps/01-hello-world');
133+
fixture.componentRef.setInput('label', 'Hello world');
134+
fixture.componentRef.setInput('parent', parent);
135+
fixture.componentRef.setInput('tutorialData', {
136+
type: TutorialType.LOCAL,
137+
title: 'Hello world',
138+
sourceCodeZipPath: 'assets/tutorials/01-hello-world.zip',
139+
nextStep: 'tutorials/first-app/steps/02-home',
140+
restrictedMode: false,
141+
});
142+
fixture.componentRef.setInput('docContent', {id: 'hello-world', contents: '<p>Hello</p>'});
143+
await fixture.whenStable();
144+
145+
expect(component.documentContent()).toBe('<p>Hello</p>');
146+
expect(component.tutorialName()).toBe('First App');
147+
expect(component.stepName()).toBe('Hello world');
148+
expect(component.localTutorialZipUrl()).toBe('assets/tutorials/01-hello-world.zip');
149+
expect(component.nextStepPath).toBe('/tutorials/first-app/steps/02-home');
150+
expect(component.shouldRenderEmbeddedEditor()).toBeFalse();
151+
152+
// Navigating to another step reuses the component and only updates the inputs.
153+
fixture.componentRef.setInput('path', 'tutorials/first-app/steps/02-home');
154+
fixture.componentRef.setInput('label', 'Home');
155+
fixture.componentRef.setInput('tutorialData', {
156+
type: TutorialType.LOCAL,
157+
title: 'Home',
158+
previousStep: 'tutorials/first-app/steps/01-hello-world',
159+
restrictedMode: false,
160+
});
161+
await fixture.whenStable();
162+
163+
expect(component.stepName()).toBe('Home');
164+
expect(component.previousStepPath).toBe('/tutorials/first-app/steps/01-hello-world');
165+
expect(component.nextStepPath).toBeUndefined();
166+
});
167+
126168
// TODO: Add tests in a future PR
127169
// it('should render the embedded editor based on the tutorial config', () => {});
128170
// it('should not render the embedded editor based on the tutorial config', () => {});
129-
// it('should load the tutorial', () => {});
130171

131172
it('should reset the reveal answer', async () => {
132173
setupResetRevealAnswerValues();

‎adev/src/app/features/tutorial/tutorial.component.ts‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import {
1212
Component,
1313
computed,
1414
DestroyRef,
15+
effect,
1516
ElementRef,
1617
EnvironmentInjector,
1718
inject,
19+
input,
1820
PLATFORM_ID,
1921
Signal,
2022
signal,
2123
Type,
24+
untracked,
2225
viewChild,
2326
} from '@angular/core';
2427
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@@ -32,9 +35,8 @@ import {
3235
TutorialNavigationItem,
3336
TutorialType,
3437
} from '@angular/docs';
35-
import {ActivatedRoute, RouterLink} from '@angular/router';
38+
import {RouterLink} from '@angular/router';
3639
import {from} from 'rxjs';
37-
import {filter} from 'rxjs/operators';
3840

3941
import {PAGE_PREFIX} from '../../core/constants/pages';
4042
import {
@@ -74,11 +76,17 @@ export default class Tutorial {
7476
private readonly elementRef = inject(ElementRef<unknown>);
7577
private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager);
7678
private readonly nodeRuntimeState = inject(NodeRuntimeState);
77-
private readonly route = inject(ActivatedRoute);
7879
private readonly splitResizerHandler = inject(SplitResizerHandler);
7980
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
8081

81-
readonly documentContent = signal<string | null>(null);
82+
// Bound from the route data (the tutorial navigation item and its resolved content).
83+
readonly docContent = input<DocContent>();
84+
readonly tutorialData = input<TutorialNavigationData>();
85+
readonly path = input<string>();
86+
readonly label = input<string>();
87+
readonly parent = input<TutorialNavigationItem>();
88+
89+
readonly documentContent = computed(() => this.docContent()?.contents ?? null);
8290
readonly localTutorialZipUrl = signal<string | undefined>(undefined);
8391

8492
readonly nextTutorialPath = signal<string | null>(null);
@@ -101,18 +109,16 @@ export default class Tutorial {
101109
readonly answerRevealed = signal<boolean>(false);
102110

103111
constructor() {
104-
this.route.data
105-
.pipe(
106-
filter(() =>
107-
Boolean(this.route?.routeConfig?.path?.startsWith(`${PAGE_PREFIX.TUTORIALS}/`)),
108-
),
109-
takeUntilDestroyed(),
110-
)
111-
.subscribe((data) => {
112-
const docContent = (data['docContent'] as DocContent | undefined)?.contents ?? null;
113-
this.documentContent.set(docContent);
114-
this.setTutorialData(data as TutorialNavigationItem);
115-
});
112+
effect(() => {
113+
const tutorialData = this.tutorialData();
114+
const path = this.path();
115+
const label = this.label();
116+
const parent = this.parent();
117+
if (!tutorialData || !path || !label) {
118+
return;
119+
}
120+
untracked(() => this.setTutorialData({path, label, parent, tutorialData}));
121+
});
116122

117123
const destroyRef = inject(DestroyRef);
118124
afterNextRender(() => {

‎adev/src/app/features/update/update.component.html‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ <h3>Angular versions</h3>
1111
From v.
1212

1313
<button type="button" [cdkMenuTriggerFor]="templatesMenuFrom">
14-
<span>{{ from.name }}</span>
14+
<span>{{ from().name }}</span>
1515
<docs-icon>expand_more</docs-icon>
1616
</button>
1717

1818
<ng-template #templatesMenuFrom>
1919
<ul class="adev-template-dropdown" cdkMenu>
2020
@for (version of versions; track $index) {
2121
<li>
22-
<button cdkMenuItem type="button" (click)="from = version; showUpdatePath()">
22+
<button cdkMenuItem type="button" (click)="from.set(version); showUpdatePath()">
2323
<span>{{ version.name }}</span>
2424
</button>
2525
</li>
@@ -32,15 +32,15 @@ <h3>Angular versions</h3>
3232
<span class="adev-template-select">
3333
To v.
3434
<button type="button" [cdkMenuTriggerFor]="templatesMenuTo">
35-
<span>{{ to.name }}</span>
35+
<span>{{ to().name }}</span>
3636
<docs-icon>expand_more</docs-icon>
3737
</button>
3838

3939
<ng-template #templatesMenuTo>
4040
<ul class="adev-template-dropdown" cdkMenu>
4141
@for (version of versions; track $index) {
4242
<li>
43-
<button cdkMenuItem type="button" (click)="to = version; showUpdatePath()">
43+
<button cdkMenuItem type="button" (click)="to.set(version); showUpdatePath()">
4444
<span>{{ version.name }}</span>
4545
</button>
4646
</li>
@@ -51,7 +51,7 @@ <h3>Angular versions</h3>
5151
</span>
5252
</div>
5353

54-
@if (from.number >= futureVersion || to.number >= futureVersion) {
54+
@if (from().number >= futureVersion || to().number >= futureVersion) {
5555
<div class="docs-alert docs-alert-critical">
5656
<p>
5757
<strong>Warning:</strong>
@@ -61,7 +61,7 @@ <h3>Angular versions</h3>
6161
</div>
6262
}
6363

64-
@if (from.number > to.number) {
64+
@if (from().number > to().number) {
6565
<div class="docs-alert docs-alert-critical">
6666
<p>
6767
<strong>Warning:</strong>
@@ -70,7 +70,7 @@ <h3>Angular versions</h3>
7070
</div>
7171
}
7272

73-
@if (to.number - from.number > 150 && from.number > 240) {
73+
@if (to().number - from().number > 150 && from().number > 240) {
7474
<div class="docs-alert docs-alert-critical">
7575
<p>
7676
<strong>Warning:</strong>
@@ -84,19 +84,19 @@ <h3>Angular versions</h3>
8484

8585
<h3>Application complexity</h3>
8686
<mat-button-toggle-group
87-
(change)="level = $event.value; showUpdatePath()"
88-
[value]="level"
87+
(change)="level.set($event.value); showUpdatePath()"
88+
[value]="level()"
8989
style="margin-bottom: 16px"
9090
>
9191
<mat-button-toggle [value]="1">Basic</mat-button-toggle>
9292
<mat-button-toggle [value]="2">Medium</mat-button-toggle>
9393
<mat-button-toggle [value]="3">Advanced</mat-button-toggle>
9494
</mat-button-toggle-group>
95-
@if (level === 1) {
95+
@if (level() === 1) {
9696
<p>Shows information for all Angular developers.</p>
97-
} @else if (level === 2) {
97+
} @else if (level() === 2) {
9898
<p>Shows information that's of interest to more advanced Angular developers.</p>
99-
} @else if (level === 3) {
99+
} @else if (level() === 3) {
100100
<p>Shows all the information we have about this update.</p>
101101
}
102102

@@ -111,7 +111,7 @@ <h3>Other dependencies</h3>
111111
</div>
112112
}
113113

114-
@if (from.number < 600) {
114+
@if (from().number < 600) {
115115
<h4>Package Manager</h4>
116116
<mat-button-toggle-group
117117
(change)="packageManager = $event.value; showUpdatePath()"

‎adev/src/app/features/update/update.component.spec.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ describe('UpdateComponent', () => {
3333
expect(component).toBeTruthy();
3434
});
3535

36+
it('should read the versions and level from the query param inputs', async () => {
37+
const queryParamFixture = TestBed.createComponent(UpdateComponent);
38+
queryParamFixture.componentRef.setInput('v', '20.0-21.0');
39+
queryParamFixture.componentRef.setInput('l', '3');
40+
await queryParamFixture.whenStable();
41+
42+
const queryParamComponent = queryParamFixture.componentInstance;
43+
expect(queryParamComponent['from']().name).toBe('20.0');
44+
expect(queryParamComponent['to']().name).toBe('21.0');
45+
expect(queryParamComponent['level']()).toBe(3);
46+
});
47+
3648
describe('getComplexityLevelName', () => {
3749
it('should return "Basic" for ApplicationComplexity.Basic', () => {
3850
expect(component['getComplexityLevelName'](ApplicationComplexity.Basic)).toBe('Basic');

0 commit comments

Comments
 (0)