mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): report @defer errors using ErrorHandler (#52320)
This commit updates the code to report errors via `ErrorHandler` instance. For dependency loading problems, errors are reported only when `@error` block is not provided. PR Close #52320
This commit is contained in:
committed by
Dylan Hunn
parent
a87586538a
commit
ec1e652f8a
@@ -29,6 +29,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
CYCLIC_DI_DEPENDENCY = -200,
|
||||
// (undocumented)
|
||||
DEFER_LOADING_FAILED = 750,
|
||||
// (undocumented)
|
||||
DUPLICATE_DIRECTITVE = 309,
|
||||
// (undocumented)
|
||||
EXPORT_NOT_FOUND = -301,
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
*/
|
||||
|
||||
import {InjectionToken, Injector} from '../di';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {findMatchingDehydratedView} from '../hydration/views';
|
||||
import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref';
|
||||
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
|
||||
import {bindingUpdated} from '../render3/bindings';
|
||||
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition';
|
||||
import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
|
||||
import {markViewDirty} from '../render3/instructions/mark_view_dirty';
|
||||
import {handleError} from '../render3/instructions/shared';
|
||||
import {ɵɵtemplate} from '../render3/instructions/template';
|
||||
import {LContainer} from '../render3/interfaces/container';
|
||||
import {DirectiveDefList, PipeDefList} from '../render3/interfaces/definition';
|
||||
@@ -465,7 +468,11 @@ export function renderDeferBlockState(
|
||||
|
||||
const applyStateFn =
|
||||
needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState;
|
||||
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
|
||||
try {
|
||||
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
|
||||
} catch (error: unknown) {
|
||||
handleError(hostLView, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,6 +674,17 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
|
||||
|
||||
if (failed) {
|
||||
tDetails.loadingState = DeferDependenciesLoadingState.FAILED;
|
||||
|
||||
if (tDetails.errorTmplIndex === null) {
|
||||
const templateLocation = getTemplateLocationDetails(lView);
|
||||
const error = new RuntimeError(
|
||||
RuntimeErrorCode.DEFER_LOADING_FAILED,
|
||||
ngDevMode &&
|
||||
'Loading dependencies for `@defer` block failed, ' +
|
||||
`but no \`@error\` block was configured${templateLocation}. ` +
|
||||
'Consider using the `@error` block to render an error state.');
|
||||
handleError(lView, error);
|
||||
}
|
||||
} else {
|
||||
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
|
||||
|
||||
|
||||
@@ -93,6 +93,9 @@ export const enum RuntimeErrorCode {
|
||||
INVALID_I18N_STRUCTURE = 700,
|
||||
MISSING_LOCALE_DATA = 701,
|
||||
|
||||
// Defer errors (750-799 range)
|
||||
DEFER_LOADING_FAILED = 750,
|
||||
|
||||
// standalone errors
|
||||
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
|
||||
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, inject, Input, NgZone, Pipe, PipeTransform, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
|
||||
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, ErrorHandler, inject, Input, NgZone, Pipe, PipeTransform, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
|
||||
import {getComponentDef} from '@angular/core/src/render3/definition';
|
||||
import {ComponentFixture, DeferBlockBehavior, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
|
||||
|
||||
@@ -863,6 +863,156 @@ describe('@defer', () => {
|
||||
expect(fixture.componentInstance.cmps.length).toBe(1);
|
||||
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('error');
|
||||
});
|
||||
|
||||
it('should report an error to the ErrorHandler if no `@error` block is defined', async () => {
|
||||
@Component({
|
||||
selector: 'nested-cmp',
|
||||
standalone: true,
|
||||
template: 'NestedCmp',
|
||||
})
|
||||
class NestedCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'simple-app',
|
||||
imports: [NestedCmp],
|
||||
template: `
|
||||
@defer (when isVisible) {
|
||||
<nested-cmp />
|
||||
} @loading {
|
||||
Loading...
|
||||
} @placeholder {
|
||||
Placeholder
|
||||
}
|
||||
`
|
||||
})
|
||||
class MyCmp {
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
const deferDepsInterceptor = {
|
||||
intercept() {
|
||||
return () => [failedDynamicImport()];
|
||||
}
|
||||
};
|
||||
|
||||
const reportedErrors: Error[] = [];
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
|
||||
useValue: deferDepsInterceptor,
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler, useClass: class extends ErrorHandler{
|
||||
override handleError(error: Error) {
|
||||
reportedErrors.push(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
deferBlockBehavior: DeferBlockBehavior.Playthrough,
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyCmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
|
||||
|
||||
fixture.componentInstance.isVisible = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Loading');
|
||||
|
||||
// Wait for dependencies to load.
|
||||
await allPendingDynamicImports();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Verify that there was an error reported to the `ErrorHandler`.
|
||||
expect(reportedErrors.length).toBe(1);
|
||||
expect(reportedErrors[0].message).toContain('NG0750');
|
||||
expect(reportedErrors[0].message).toContain(`(used in the 'MyCmp' component template)`);
|
||||
});
|
||||
|
||||
it('should not render `@error` block if loaded component has errors', async () => {
|
||||
@Component({
|
||||
selector: 'cmp-with-error',
|
||||
standalone: true,
|
||||
template: 'CmpWithError',
|
||||
})
|
||||
class CmpWithError {
|
||||
constructor() {
|
||||
throw new Error('CmpWithError produced an error');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'simple-app',
|
||||
imports: [CmpWithError],
|
||||
template: `
|
||||
@defer (when isVisible) {
|
||||
<cmp-with-error />
|
||||
} @loading {
|
||||
Loading...
|
||||
} @error {
|
||||
Error
|
||||
} @placeholder {
|
||||
Placeholder
|
||||
}
|
||||
`
|
||||
})
|
||||
class MyCmp {
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
const deferDepsInterceptor = {
|
||||
intercept() {
|
||||
return () => [dynamicImportOf(CmpWithError)];
|
||||
}
|
||||
};
|
||||
|
||||
const reportedErrors: Error[] = [];
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
|
||||
useValue: deferDepsInterceptor,
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler, useClass: class extends ErrorHandler{
|
||||
override handleError(error: Error) {
|
||||
reportedErrors.push(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
deferBlockBehavior: DeferBlockBehavior.Playthrough,
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(MyCmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
|
||||
|
||||
fixture.componentInstance.isVisible = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Loading');
|
||||
|
||||
// Wait for dependencies to load.
|
||||
await allPendingDynamicImports();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Expect an error to be reported to the `ErrorHandler`.
|
||||
expect(reportedErrors.length).toBe(1);
|
||||
expect(reportedErrors[0].message).toBe('CmpWithError produced an error');
|
||||
|
||||
// Expect that the `@loading` UI is removed, but the `@error` is *not* rendered,
|
||||
// because it was a component initialization error, not resource loading issue.
|
||||
expect(fixture.nativeElement.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('queries', () => {
|
||||
|
||||
@@ -962,6 +962,9 @@
|
||||
{
|
||||
"name": "getTView"
|
||||
},
|
||||
{
|
||||
"name": "handleError"
|
||||
},
|
||||
{
|
||||
"name": "hasInSkipHydrationBlockFlag"
|
||||
},
|
||||
@@ -2363,6 +2366,9 @@
|
||||
{
|
||||
"name": "switchMap"
|
||||
},
|
||||
{
|
||||
"name": "throwError"
|
||||
},
|
||||
{
|
||||
"name": "throwProviderNotFoundError"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user