Skip to content

Commit

Permalink
fix(core): collect providers from NgModules while rendering @defer
Browse files Browse the repository at this point in the history
…block

Currently, when a `@defer` block contains standalone components that import NgModules with providers, those providers are not available to components declared within the same NgModule. The problem is that the standalone injector is not created for the host component (that hosts this `@defer` block), since dependencies become defer-loaded, thus no information is available at host component creation time.

This commit updates the logic to collect all providers from all NgModules used as a dependency for standalone components used within a `@defer` block. When an instance of a defer block is created, a new environment injector instance with those providers is created.

Resolves angular#52876.
  • Loading branch information
AndrewKushnir committed Nov 14, 2023
1 parent 15a825c commit 27a62a6
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 6 deletions.
37 changes: 32 additions & 5 deletions packages/core/src/defer/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

import {setActiveConsumer} from '@angular/core/primitives/signals';

import {InjectionToken, Injector} from '../di';
import {EnvironmentInjector, InjectionToken, Injector} from '../di';
import {internalImportProvidersFrom} from '../di/provider_collection';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {findMatchingDehydratedView} from '../hydration/views';
import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref';
import {createEnvironmentInjector} from '../render3';
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
import {bindingUpdated} from '../render3/bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition';
Expand Down Expand Up @@ -142,6 +144,7 @@ export function ɵɵdefer(
dependencyResolverFn: dependencyResolverFn ?? null,
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
loadingPromise: null,
providers: null,
};
enableTimerScheduling?.(tView, tDetails, placeholderConfigIndex, loadingConfigIndex);
setTDeferBlockDetails(tView, adjustedIndex, tDetails);
Expand Down Expand Up @@ -507,17 +510,35 @@ function applyDeferBlockState(
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;

// TNode that represents a block that should be rendered.
const blockTNode = getTNode(hostTView, adjustedIndex) as TContainerNode;

// There is only 1 view that can be present in an LContainer that
// represents a defer block, so always refer to the first one.
const viewIndex = 0;

removeLViewFromLContainer(lContainer, viewIndex);
const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});

let injector: Injector|undefined;
if (newState === DeferBlockState.Complete) {
// When we render a defer block in completed state, there might be
// new standalone components using within the block, which may import
// NgModules with providers. In order to make those providers available
// for components declared in that NgModule, we create an instance of
// the environment injector and pass it to the logic that creates a view.
const tDetails = getTDeferBlockDetails(hostTView, tNode);
const providers = tDetails.providers;
if (Array.isArray(providers) && providers.length > 0) {
injector = createEnvironmentInjector(
[providers], hostLView[INJECTOR] as EnvironmentInjector, `DeferBlock`);
}
}
const dehydratedView = findMatchingDehydratedView(lContainer, blockTNode.tView!.ssrId);
const embeddedLView =
createAndRenderEmbeddedLView(hostLView, blockTNode, null, {dehydratedView, injector});
addLViewToLContainer(
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(blockTNode, dehydratedView));
markViewDirty(embeddedLView);
}
}
Expand Down Expand Up @@ -712,6 +733,12 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
if (directiveDefs.length > 0) {
primaryBlockTView.directiveRegistry =
addDepsToRegistry<DirectiveDefList>(primaryBlockTView.directiveRegistry, directiveDefs);

// Extract providers from all NgModules imported by standalone components
// used within this defer block.
const directiveTypes = directiveDefs.map(def => def.type);
const providers = internalImportProvidersFrom(false, ...directiveTypes);
tDetails.providers = providers;
}
if (pipeDefs.length > 0) {
primaryBlockTView.pipeRegistry =
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/defer/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Provider} from '../di/interface/provider';
import type {DependencyType} from '../render3/interfaces/definition';

/**
Expand Down Expand Up @@ -109,6 +110,12 @@ export interface TDeferBlockDetails {
* which all await the same set of dependencies.
*/
loadingPromise: Promise<unknown>|null;

/**
* List of providers collected from all NgModules that were imported by
* standalone components used within this defer block.
*/
providers: Provider[]|null;
}

/**
Expand Down
75 changes: 74 additions & 1 deletion packages/core/test/acceptance/defer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
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 {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, ErrorHandler, inject, Injectable, Input, NgModule, 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';

Expand Down Expand Up @@ -4027,4 +4027,77 @@ describe('@defer', () => {
.toHaveBeenCalledWith('focusin', jasmine.any(Function), jasmine.any(Object));
});
});

describe('NgModules', () => {
it('should provide access to tokens from imported NgModules', async () => {
@Injectable()
class Service {
id = 'service';
}

@Component({
selector: 'chart',
template: '{{ svc.id }}',
})
class Chart {
svc = inject(Service);
}

@NgModule({
providers: [Service],
declarations: [Chart],
exports: [Chart],
})
class ChartsModule {
}

@Component({
selector: 'chart-collection',
template: '<chart />',
standalone: true,
imports: [ChartsModule],
})
class ChartCollectionComponent {
}

@Component({
selector: 'app-root',
standalone: true,
template: `
@defer (when isVisible) {
<chart-collection />
}
`,
imports: [ChartCollectionComponent],
})
class MyCmp {
isVisible = true;
}

const deferDepsInterceptor = {
intercept() {
return () => {
return [dynamicImportOf(ChartCollectionComponent)];
};
}
};

TestBed.configureTestingModule({
providers: [
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});

clearDirectiveDefs(MyCmp);

const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();

await allPendingDynamicImports();
fixture.detectChanges();

expect(fixture.nativeElement.innerHTML).toContain('<chart>service</chart>');
});
});
});

0 comments on commit 27a62a6

Please sign in to comment.