diff --git a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts index 226b3cd52c2..6612bc9b4bd 100644 --- a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts +++ b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts @@ -21,7 +21,7 @@ import { Input, signal, Type, - ViewChild, + viewChild, } from '@angular/core'; import {CommonModule, DOCUMENT} from '@angular/common'; import {MatTabGroup, MatTabsModule} from '@angular/material/tabs'; @@ -54,7 +54,7 @@ export class ExampleViewer { @Input() githubUrl: string | null = null; @Input() stackblitzUrl: string | null = null; - @ViewChild('codeTabs') matTabGroup?: MatTabGroup; + readonly matTabGroup = viewChild('codeTabs'); private readonly changeDetector = inject(ChangeDetectorRef); private readonly clipboard = inject(Clipboard); @@ -111,7 +111,7 @@ export class ExampleViewer { `example-${this.exampleMetadata()?.id.toString()!}`, ); - this.matTabGroup?.realignInkBar(); + this.matTabGroup()?.realignInkBar(); this.listenToMatTabIndexChange(); @@ -142,8 +142,9 @@ export class ExampleViewer { } private listenToMatTabIndexChange(): void { - this.matTabGroup?.realignInkBar(); - this.matTabGroup?.selectedIndexChange + const matTabGroup = this.matTabGroup(); + matTabGroup?.realignInkBar(); + matTabGroup?.selectedIndexChange .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((index) => { this.snippetCode.set(this.exampleMetadata()?.files[index]); diff --git a/adev/src/app/core/layout/progress-bar/progress-bar.component.spec.ts b/adev/src/app/core/layout/progress-bar/progress-bar.component.spec.ts index f288ef2c85d..b8d3cf068c5 100644 --- a/adev/src/app/core/layout/progress-bar/progress-bar.component.spec.ts +++ b/adev/src/app/core/layout/progress-bar/progress-bar.component.spec.ts @@ -29,8 +29,7 @@ describe('ProgressBarComponent', () => { // We suspect a racing condition inside the RouterTestingHarness. // Until this has been investigated, we will skip this test. xit('should call progressBar.complete() on route change', async () => { - const progressBar = component.progressBar(); - const progressBarCompleteSpy = spyOn(progressBar, 'complete'); + const progressBarCompleteSpy = spyOn(component.progressBar(), 'complete'); const harness = await RouterTestingHarness.create(); await harness.navigateByUrl('/'); diff --git a/adev/src/app/core/layout/progress-bar/progress-bar.component.ts b/adev/src/app/core/layout/progress-bar/progress-bar.component.ts index f6ed7df97b9..9cf66b44c2b 100644 --- a/adev/src/app/core/layout/progress-bar/progress-bar.component.ts +++ b/adev/src/app/core/layout/progress-bar/progress-bar.component.ts @@ -12,7 +12,6 @@ import { inject, OnInit, PLATFORM_ID, - Signal, viewChild, } from '@angular/core'; import {isPlatformBrowser} from '@angular/common'; @@ -41,7 +40,7 @@ export const PROGRESS_BAR_DELAY = 30; export class ProgressBarComponent implements OnInit { private readonly router = inject(Router); - progressBar = viewChild.required(NgProgressRef); + readonly progressBar = viewChild.required(NgProgressRef); isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); diff --git a/adev/src/app/editor/code-editor/code-editor.component.spec.ts b/adev/src/app/editor/code-editor/code-editor.component.spec.ts index 55922866192..99b0d299201 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.spec.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.spec.ts @@ -97,7 +97,7 @@ describe('CodeEditor', () => { component.ngAfterViewInit(); expect(codeMirrorEditorInitSpy).toHaveBeenCalledWith( - component['codeEditorWrapperRef'].nativeElement, + component.codeEditorWrapperRef().nativeElement, ); }); diff --git a/adev/src/app/editor/code-editor/code-editor.component.ts b/adev/src/app/editor/code-editor/code-editor.component.ts index f360e9d6519..6e00a45e85f 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.ts @@ -15,9 +15,10 @@ import { ElementRef, EnvironmentInjector, OnDestroy, - ViewChild, + afterRenderEffect, inject, signal, + viewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {MatTabGroup, MatTabsModule} from '@angular/material/tabs'; @@ -60,28 +61,13 @@ const ANGULAR_DEV = 'https://angular.dev'; ], }) export class CodeEditor implements AfterViewInit, OnDestroy { - @ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef; - @ViewChild(MatTabGroup) private matTabGroup!: MatTabGroup; + readonly codeEditorWrapperRef = + viewChild.required>('codeEditorWrapper'); + readonly matTabGroup = viewChild.required(MatTabGroup); - private createFileInputRef?: ElementRef; - @ViewChild('createFileInput') protected set setFileInputRef( - element: ElementRef, - ) { - if (element) { - element.nativeElement.focus(); - this.createFileInputRef = element; - } - } + readonly createFileInputRef = viewChild>('createFileInput'); - private renameFileInputRef?: ElementRef; - @ViewChild('renameFileInput') protected set setRenameFileInputRef( - element: ElementRef, - ) { - if (element) { - element.nativeElement.focus(); - this.renameFileInputRef = element; - } - } + readonly renameFileInputRef = viewChild>('renameFileInput'); private readonly destroyRef = inject(DestroyRef); @@ -117,8 +103,20 @@ export class CodeEditor implements AfterViewInit, OnDestroy { readonly isCreatingFile = signal(false); readonly isRenamingFile = signal(false); + constructor() { + afterRenderEffect(() => { + const createFileInput = this.createFileInputRef(); + createFileInput?.nativeElement.focus(); + }); + + afterRenderEffect(() => { + const renameFileInput = this.renameFileInputRef(); + renameFileInput?.nativeElement.focus(); + }); + } + ngAfterViewInit() { - this.codeMirrorEditor.init(this.codeEditorWrapperRef.nativeElement); + this.codeMirrorEditor.init(this.codeEditorWrapperRef().nativeElement); this.listenToDiagnosticsChange(); this.listenToTabChange(); @@ -164,12 +162,12 @@ export class CodeEditor implements AfterViewInit, OnDestroy { async deleteFile(filename: string) { await this.codeMirrorEditor.deleteFile(filename); - this.matTabGroup.selectedIndex = 0; + this.matTabGroup().selectedIndex = 0; } onAddButtonClick() { this.isCreatingFile.set(true); - this.matTabGroup.selectedIndex = this.files().length; + this.matTabGroup().selectedIndex = this.files().length; } onRenameButtonClick() { @@ -177,11 +175,12 @@ export class CodeEditor implements AfterViewInit, OnDestroy { } async renameFile(event: SubmitEvent, oldPath: string) { - if (!this.renameFileInputRef) return; + const renameFileInput = this.renameFileInputRef(); + if (!renameFileInput) return; event.preventDefault(); - const renameFileInputValue = this.renameFileInputRef.nativeElement.value; + const renameFileInputValue = renameFileInput.nativeElement.value; if (renameFileInputValue) { if (renameFileInputValue.includes('..')) { @@ -204,11 +203,12 @@ export class CodeEditor implements AfterViewInit, OnDestroy { } async createFile(event: SubmitEvent) { - if (!this.createFileInputRef) return; + const fileInput = this.createFileInputRef(); + if (!fileInput) return; event.preventDefault(); - const newFileInputValue = this.createFileInputRef.nativeElement.value; + const newFileInputValue = fileInput.nativeElement.value; if (newFileInputValue) { if (newFileInputValue.includes('..')) { @@ -248,13 +248,13 @@ export class CodeEditor implements AfterViewInit, OnDestroy { ) .subscribe(() => { // selected file on project change is always the first - this.matTabGroup.selectedIndex = 0; + this.matTabGroup().selectedIndex = 0; }); } private listenToTabChange() { - this.matTabGroup.selectedIndexChange - .pipe(takeUntilDestroyed(this.destroyRef)) + this.matTabGroup() + .selectedIndexChange.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((index) => { const selectedFile = this.files()[index]; diff --git a/adev/src/app/editor/embedded-editor.component.ts b/adev/src/app/editor/embedded-editor.component.ts index fed6ee5de1a..785c44a904d 100644 --- a/adev/src/app/editor/embedded-editor.component.ts +++ b/adev/src/app/editor/embedded-editor.component.ts @@ -17,10 +17,10 @@ import { OnDestroy, OnInit, PLATFORM_ID, - ViewChild, computed, inject, signal, + viewChild, } from '@angular/core'; import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop'; import {IconComponent} from '@angular/docs'; @@ -53,8 +53,8 @@ export const LARGE_EDITOR_HEIGHT_BREAKPOINT = 550; providers: [EditorUiState], }) export class EmbeddedEditor implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('editorContainer') editorContainer!: ElementRef; - @ViewChild(MatTabGroup) matTabGroup!: MatTabGroup; + readonly editorContainer = viewChild.required>('editorContainer'); + readonly matTabGroup = viewChild(MatTabGroup); private readonly platformId = inject(PLATFORM_ID); private readonly changeDetector = inject(ChangeDetectorRef); @@ -120,7 +120,10 @@ export class EmbeddedEditor implements OnInit, AfterViewInit, OnDestroy { private setFirstTabAsActiveAfterResize(): void { this.displayPreviewInMatTabGroup$.subscribe(() => { this.changeDetector.detectChanges(); - this.matTabGroup.selectedIndex = 0; + const matTabGroup = this.matTabGroup(); + if (matTabGroup) { + matTabGroup.selectedIndex = 0; + } }); } @@ -138,11 +141,11 @@ export class EmbeddedEditor implements OnInit, AfterViewInit, OnDestroy { this.splitDirection = this.isLargeEmbeddedEditor() ? 'horizontal' : 'vertical'; }); - this.resizeObserver.observe(this.editorContainer.nativeElement); + this.resizeObserver.observe(this.editorContainer().nativeElement); } private isLargeEmbeddedEditor(): boolean { - const editorContainer = this.editorContainer.nativeElement; + const editorContainer = this.editorContainer().nativeElement; const width = editorContainer.offsetWidth; const height = editorContainer.offsetHeight; diff --git a/adev/src/app/editor/terminal/terminal.component.spec.ts b/adev/src/app/editor/terminal/terminal.component.spec.ts index 5e715a05107..e2dae9a29ec 100644 --- a/adev/src/app/editor/terminal/terminal.component.spec.ts +++ b/adev/src/app/editor/terminal/terminal.component.spec.ts @@ -47,16 +47,4 @@ describe('Terminal', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - it('should register the terminal element on afterViewInit', () => { - const terminalDebugElement = fixture.debugElement.query(By.css('.adev-terminal-output')); - - component['terminalElementRef'] = terminalDebugElement; - component.ngAfterViewInit(); - - expect(terminalHandlerSpy.registerTerminal).toHaveBeenCalledWith( - TerminalType.READONLY, - terminalDebugElement.nativeElement, - ); - }); }); diff --git a/adev/src/app/editor/terminal/terminal.component.ts b/adev/src/app/editor/terminal/terminal.component.ts index 14c2847dc63..43ee4f11fc2 100644 --- a/adev/src/app/editor/terminal/terminal.component.ts +++ b/adev/src/app/editor/terminal/terminal.component.ts @@ -13,9 +13,9 @@ import { DestroyRef, ElementRef, Input, - ViewChild, ViewEncapsulation, inject, + viewChild, } from '@angular/core'; import {debounceTime} from 'rxjs/operators'; @@ -35,7 +35,7 @@ import {Subject} from 'rxjs'; }) export class Terminal implements AfterViewInit { @Input({required: true}) type!: TerminalType; - @ViewChild('terminalOutput') private terminalElementRef!: ElementRef; + readonly terminalElementRef = viewChild.required>('terminalOutput'); private readonly destroyRef = inject(DestroyRef); private readonly terminalHandler = inject(TerminalHandler); @@ -43,7 +43,7 @@ export class Terminal implements AfterViewInit { private readonly resize$ = new Subject(); ngAfterViewInit() { - this.terminalHandler.registerTerminal(this.type, this.terminalElementRef.nativeElement); + this.terminalHandler.registerTerminal(this.type, this.terminalElementRef().nativeElement); this.setResizeObserver(); @@ -57,7 +57,7 @@ export class Terminal implements AfterViewInit { this.resize$.next(); }); - resizeObserver.observe(this.terminalElementRef.nativeElement); + resizeObserver.observe(this.terminalElementRef().nativeElement); this.destroyRef.onDestroy(() => resizeObserver.disconnect()); } diff --git a/adev/src/app/features/tutorial/tutorial.component.spec.ts b/adev/src/app/features/tutorial/tutorial.component.spec.ts index 249fb219fe9..8bbe8a2557f 100644 --- a/adev/src/app/features/tutorial/tutorial.component.spec.ts +++ b/adev/src/app/features/tutorial/tutorial.component.spec.ts @@ -137,12 +137,13 @@ describe('Tutorial', () => { setupResetRevealAnswerValues(); fixture.detectChanges(); - if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined'); + const revealAnswerButton = component.revealAnswerButton(); + if (!revealAnswerButton) throw new Error('revealAnswerButton is undefined'); const revealAnswerSpy = spyOn(component['embeddedTutorialManager'], 'revealAnswer'); const resetRevealAnswerSpy = spyOn(component['embeddedTutorialManager'], 'resetRevealAnswer'); - component.revealAnswerButton.nativeElement.click(); + revealAnswerButton.nativeElement.click(); expect(revealAnswerSpy).not.toHaveBeenCalled(); expect(resetRevealAnswerSpy).toHaveBeenCalled(); @@ -152,35 +153,37 @@ describe('Tutorial', () => { setupRevealAnswerValues(); fixture.detectChanges(); - if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined'); + const revealAnswerButton = component.revealAnswerButton(); + if (!revealAnswerButton) throw new Error('revealAnswerButton is undefined'); const embeddedTutorialManagerRevealAnswerSpy = spyOn( component['embeddedTutorialManager'], 'revealAnswer', ); - component.revealAnswerButton.nativeElement.click(); + revealAnswerButton.nativeElement.click(); expect(embeddedTutorialManagerRevealAnswerSpy).toHaveBeenCalled(); await fixture.whenStable(); fixture.detectChanges(); - expect(component.revealAnswerButton.nativeElement.textContent?.trim()).toBe('Reset'); + expect(revealAnswerButton.nativeElement.textContent?.trim()).toBe('Reset'); }); it('should not reveal the answer when button is disabled', async () => { setupDisabledRevealAnswerValues(); fixture.detectChanges(); - if (!component.revealAnswerButton) throw new Error('revealAnswerButton is undefined'); + const revealAnswerButton = component.revealAnswerButton(); + if (!revealAnswerButton) throw new Error('revealAnswerButton is undefined'); spyOn(component, 'canRevealAnswer').and.returnValue(false); const handleRevealAnswerSpy = spyOn(component, 'handleRevealAnswer'); - component.revealAnswerButton.nativeElement.click(); + revealAnswerButton.nativeElement.click(); - expect(component.revealAnswerButton.nativeElement.getAttribute('disabled')).toBeDefined(); + expect(revealAnswerButton.nativeElement.getAttribute('disabled')).toBeDefined(); expect(handleRevealAnswerSpy).not.toHaveBeenCalled(); }); @@ -188,6 +191,6 @@ describe('Tutorial', () => { setupNoRevealAnswerValues(); fixture.detectChanges(); - expect(component.revealAnswerButton).toBe(undefined); + expect(component.revealAnswerButton()).toBe(undefined); }); }); diff --git a/adev/src/app/features/tutorial/tutorial.component.ts b/adev/src/app/features/tutorial/tutorial.component.ts index cdb3e6f17db..710791fdb00 100644 --- a/adev/src/app/features/tutorial/tutorial.component.ts +++ b/adev/src/app/features/tutorial/tutorial.component.ts @@ -21,7 +21,7 @@ import { Signal, signal, Type, - ViewChild, + viewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import { @@ -72,11 +72,10 @@ const INTRODUCTION_LABEL = 'Introduction'; providers: [SplitResizerHandler], }) export default class Tutorial { - @ViewChild('content') content!: ElementRef; - @ViewChild('editor') editor: ElementRef | undefined; - @ViewChild('resizer') resizer!: ElementRef; - @ViewChild('revealAnswerButton') - readonly revealAnswerButton: ElementRef | undefined; + readonly content = viewChild>('content'); + readonly editor = viewChild>('editor'); + readonly resizer = viewChild.required>('resizer'); + readonly revealAnswerButton = viewChild>('revealAnswerButton'); private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly environmentInjector = inject(EnvironmentInjector); @@ -124,7 +123,12 @@ export default class Tutorial { const destroyRef = inject(DestroyRef); afterNextRender(() => { - this.splitResizerHandler.init(this.elementRef, this.content, this.resizer, this.editor); + this.splitResizerHandler.init( + this.elementRef, + this.content()!, + this.resizer(), + this.editor(), + ); from(this.loadEmbeddedEditorComponent()) .pipe(takeUntilDestroyed(destroyRef))