mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(docs-infra): hold editor diagnostics in a signal
`DiagnosticsState` carried a TODO to move off `BehaviorSubject` once zoneless was turned on. adev has been zoneless for a while now, with no zone.js dependency and an empty `polyfills` array, so the condition is met and the TODO can go. Both consumers now derive from the signal instead of subscribing. The Console tab badge becomes a `computed`, and so does the code editor's error list: the diagnostics are produced by a `linter()` configured with a 400ms delay rather than arriving as a stream, so the rxjs chain was only adding a further second before they were displayed. The errors box stays dismissable through a `linkedSignal`, which reverts as soon as the diagnostics change.
This commit is contained in:
@@ -13,9 +13,11 @@ import {
|
||||
ElementRef,
|
||||
EnvironmentInjector,
|
||||
afterRenderEffect,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
signal,
|
||||
untracked,
|
||||
viewChild,
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatTab, MatTabGroup, MatTabLabel} from '@angular/material/tabs';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {debounceTime, from, map, switchMap} from 'rxjs';
|
||||
import {from, switchMap} from 'rxjs';
|
||||
|
||||
import {TerminalType} from '../terminal/terminal-handler.service';
|
||||
|
||||
@@ -81,25 +83,22 @@ export class CodeEditor {
|
||||
private readonly location = inject(Location);
|
||||
private readonly environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
private readonly errors$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
// Display errors one second after code update
|
||||
debounceTime(1000),
|
||||
map((diagnosticsItem) =>
|
||||
diagnosticsItem
|
||||
.filter((item) => item.severity === 'error')
|
||||
.sort((a, b) =>
|
||||
a.lineNumber != b.lineNumber
|
||||
? a.lineNumber - b.lineNumber
|
||||
: a.characterPosition - b.characterPosition,
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
readonly TerminalType = TerminalType;
|
||||
|
||||
protected readonly displayErrorsBox = signal<boolean>(false);
|
||||
protected readonly errors = signal<DiagnosticWithLocation[]>([]);
|
||||
protected readonly errors = computed(() =>
|
||||
this.nodeRuntimeState.loadingStep() !== LoadingStep.READY
|
||||
? []
|
||||
: this.diagnosticsState
|
||||
.diagnostics()
|
||||
.filter((item) => item.severity === 'error')
|
||||
.sort((a, b) =>
|
||||
a.lineNumber != b.lineNumber
|
||||
? a.lineNumber - b.lineNumber
|
||||
: a.characterPosition - b.characterPosition,
|
||||
),
|
||||
);
|
||||
|
||||
protected readonly displayErrorsBox = linkedSignal(() => this.errors().length > 0);
|
||||
protected readonly files = this.codeMirrorEditor.openFiles;
|
||||
protected readonly isCreatingFile = signal<boolean>(false);
|
||||
protected readonly isRenamingFile = signal<boolean>(false);
|
||||
@@ -120,7 +119,6 @@ export class CodeEditor {
|
||||
|
||||
untracked(() => {
|
||||
this.codeMirrorEditor.init(parent);
|
||||
this.listenToDiagnosticsChange();
|
||||
|
||||
this.listenToTabChange();
|
||||
this.setSelectedTabOnTutorialChange();
|
||||
@@ -308,16 +306,6 @@ export class CodeEditor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private listenToDiagnosticsChange(): void {
|
||||
this.errors$.subscribe((diagnostics) => {
|
||||
if (this.nodeRuntimeState.loadingStep() !== LoadingStep.READY) {
|
||||
return;
|
||||
}
|
||||
this.errors.set(diagnostics);
|
||||
this.displayErrorsBox.set(diagnostics.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
private setSelectedTabOnTutorialChange() {
|
||||
// Using `from` to prevent injecting the embedded tutorial manager once the
|
||||
// injector is destroyed (this may happen in unit tests when the test ends
|
||||
|
||||
@@ -6,13 +6,23 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {WritableSignal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {DiagnosticsState} from './diagnostics-state.service';
|
||||
import {DiagnosticWithLocation, DiagnosticsState} from './diagnostics-state.service';
|
||||
|
||||
describe('DiagnosticsState', () => {
|
||||
let service: DiagnosticsState;
|
||||
|
||||
const diagnostic = (message: string): DiagnosticWithLocation => ({
|
||||
from: 0,
|
||||
to: 1,
|
||||
severity: 'error',
|
||||
message,
|
||||
lineNumber: 1,
|
||||
characterPosition: 0,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(DiagnosticsState);
|
||||
});
|
||||
@@ -20,4 +30,27 @@ describe('DiagnosticsState', () => {
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should start with no diagnostics', () => {
|
||||
expect(service.diagnostics()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should expose the diagnostics it is given', () => {
|
||||
const diagnostics = [diagnostic('first'), diagnostic('second')];
|
||||
|
||||
service.setDiagnostics(diagnostics);
|
||||
|
||||
expect(service.diagnostics()).toEqual(diagnostics);
|
||||
});
|
||||
|
||||
it('should replace the previous diagnostics', () => {
|
||||
service.setDiagnostics([diagnostic('first')]);
|
||||
service.setDiagnostics([]);
|
||||
|
||||
expect(service.diagnostics()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not expose a way to write to the diagnostics signal', () => {
|
||||
expect((service.diagnostics as Partial<WritableSignal<unknown>>).set).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Service} from '@angular/core';
|
||||
import {Service, signal} from '@angular/core';
|
||||
import {Diagnostic} from '@codemirror/lint';
|
||||
import {BehaviorSubject, distinctUntilChanged} from 'rxjs';
|
||||
|
||||
export interface DiagnosticWithLocation extends Diagnostic {
|
||||
lineNumber: number;
|
||||
@@ -17,12 +16,11 @@ export interface DiagnosticWithLocation extends Diagnostic {
|
||||
|
||||
@Service()
|
||||
export class DiagnosticsState {
|
||||
private readonly _diagnostics$ = new BehaviorSubject<DiagnosticWithLocation[]>([]);
|
||||
private readonly _diagnostics = signal<DiagnosticWithLocation[]>([]);
|
||||
|
||||
// TODO: use signals when zoneless will be turned off
|
||||
diagnostics$ = this._diagnostics$.asObservable().pipe(distinctUntilChanged());
|
||||
readonly diagnostics = this._diagnostics.asReadonly();
|
||||
|
||||
setDiagnostics(diagnostics: DiagnosticWithLocation[]): void {
|
||||
this._diagnostics$.next(diagnostics);
|
||||
this._diagnostics.set(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,8 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {IconComponent, TutorialType} from '@angular/docs';
|
||||
import {MatTab, MatTabGroup, MatTabLabel} from '@angular/material/tabs';
|
||||
import {map} from 'rxjs';
|
||||
|
||||
import {MAX_RECOMMENDED_WEBCONTAINERS_INSTANCES} from './alert-manager.service';
|
||||
|
||||
@@ -93,10 +91,9 @@ export class EmbeddedEditor {
|
||||
!this.nodeRuntimeState.isResetting(),
|
||||
);
|
||||
|
||||
private readonly errorsCount$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
map((diagnosticsItem) => diagnosticsItem.filter((item) => item.severity === 'error').length),
|
||||
protected readonly errorsCount = computed(
|
||||
() => this.diagnosticsState.diagnostics().filter((item) => item.severity === 'error').length,
|
||||
);
|
||||
protected readonly errorsCount = toSignal(this.errorsCount$, {initialValue: 0});
|
||||
|
||||
constructor() {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
|
||||
Reference in New Issue
Block a user