mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(core): unable to inject viewProviders when host directive with providers is present
When registering providers, the DI system assumes that `viewProviders` are registered before plain `providers`. This was reinforced by components always being first in the array of directive matches, only one component being allowed per node and the fact that only components can have `viewProviders`. This breaks down if there are host directives with `providers` on the component, because they'll execute earlier, throwing off the order of operations. These changes fix the issue by separating out the resolvers for `viewProviders` and plain `providers` and explicitly running the component's `viewProviders` resolver before any others. This also has the benefit of not attempting to resolve `viewProviders` for directives which are guaranteed not to have them. Fixes #65724.
This commit is contained in:
parent
d6087841e3
commit
886cf6c452
7 changed files with 102 additions and 32 deletions
|
|
@ -625,6 +625,7 @@ function getNgDirectiveDef<T>(directiveDefinition: DirectiveDefinition<T>): Dire
|
|||
return {
|
||||
type: directiveDefinition.type,
|
||||
providersResolver: null,
|
||||
viewProvidersResolver: null,
|
||||
factory: null,
|
||||
hostBindings: directiveDefinition.hostBindings || null,
|
||||
hostVars: directiveDefinition.hostVars || 0,
|
||||
|
|
|
|||
|
|
@ -56,17 +56,11 @@ import {getCurrentTNode, getLView, getTView} from './state';
|
|||
export function providersResolver<T>(
|
||||
def: DirectiveDef<T>,
|
||||
providers: Provider[],
|
||||
viewProviders: Provider[],
|
||||
isViewProviders: boolean,
|
||||
): void {
|
||||
const tView = getTView();
|
||||
if (tView.firstCreatePass) {
|
||||
const isComponent = isComponentDef(def);
|
||||
|
||||
// The list of view providers is processed first, and the flags are updated
|
||||
resolveProvider(viewProviders, tView.data, tView.blueprint, isComponent, true);
|
||||
|
||||
// Then, the list of providers is processed, and the flags are updated
|
||||
resolveProvider(providers, tView.data, tView.blueprint, isComponent, false);
|
||||
resolveProvider(providers, tView.data, tView.blueprint, isComponentDef(def), isViewProviders);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,17 +41,18 @@ import {DirectiveDef} from '../interfaces/definition';
|
|||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵProvidersFeature<T>(providers: Provider[], viewProviders: Provider[] = []) {
|
||||
export function ɵɵProvidersFeature<T>(providers: Provider[], viewProviders: Provider[]) {
|
||||
return (definition: DirectiveDef<T>) => {
|
||||
definition.providersResolver = (
|
||||
def: DirectiveDef<T>,
|
||||
processProvidersFn?: ProcessProvidersFunction,
|
||||
) => {
|
||||
return providersResolver(
|
||||
def, //
|
||||
processProvidersFn ? processProvidersFn(providers) : providers, //
|
||||
viewProviders,
|
||||
);
|
||||
};
|
||||
definition.providersResolver = (def, processProvidersFn) =>
|
||||
providersResolver(def, processProvidersFn ? processProvidersFn(providers) : providers, false);
|
||||
|
||||
if (viewProviders) {
|
||||
definition.viewProvidersResolver = (def, processProvidersFn) =>
|
||||
providersResolver(
|
||||
def,
|
||||
processProvidersFn ? processProvidersFn(viewProviders) : viewProviders,
|
||||
true,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ export type ComponentTemplate<T> = {
|
|||
*/
|
||||
export type ViewQueriesFunction<T> = <U extends T>(rf: RenderFlags, ctx: U) => void;
|
||||
|
||||
/** Function that resolves providers and publishes them to the DI system. */
|
||||
export type ProvidersResolver = (
|
||||
def: DirectiveDef<unknown>,
|
||||
processProvidersFn?: ProcessProvidersFunction,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Definition of what a content queries function should look like.
|
||||
*/
|
||||
|
|
@ -196,10 +202,11 @@ export interface DirectiveDef<T> {
|
|||
/** Token representing the directive. Used by DI. */
|
||||
readonly type: Type<T>;
|
||||
|
||||
/** Function that resolves providers and publishes them into the DI system. */
|
||||
providersResolver:
|
||||
| (<U extends T>(def: DirectiveDef<U>, processProvidersFn?: ProcessProvidersFunction) => void)
|
||||
| null;
|
||||
/** Function that resolves `providers` and publishes them into the DI system. */
|
||||
providersResolver: ProvidersResolver | null;
|
||||
|
||||
/** Function that resolves `viewProviders` and publishes them into the DI system. */
|
||||
viewProvidersResolver: ProvidersResolver | null;
|
||||
|
||||
/** The selectors that will be used to match nodes to this directive. */
|
||||
readonly selectors: CssSelectorList;
|
||||
|
|
|
|||
|
|
@ -145,14 +145,14 @@ function initializeDirectives(
|
|||
ngDevMode && assertFirstCreatePass(tView);
|
||||
|
||||
const directivesLength = directives.length;
|
||||
let hasSeenComponent = false;
|
||||
let componentDef: ComponentDef<unknown> | null = null;
|
||||
|
||||
// Publishes the directive types to DI so they can be injected. Needs to
|
||||
// happen in a separate pass before the TNode flags have been initialized.
|
||||
for (let i = 0; i < directivesLength; i++) {
|
||||
const def = directives[i];
|
||||
if (!hasSeenComponent && isComponentDef(def)) {
|
||||
hasSeenComponent = true;
|
||||
if (componentDef === null && isComponentDef(def)) {
|
||||
componentDef = def;
|
||||
markAsComponentHost(tView, tNode, i);
|
||||
}
|
||||
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, lView), tView, def.type);
|
||||
|
|
@ -166,9 +166,17 @@ function initializeDirectives(
|
|||
// - the last directive in NgModule.declarations has priority over the previous one
|
||||
// So to match these rules, the order in which providers are added in the arrays is very
|
||||
// important.
|
||||
if (componentDef?.viewProvidersResolver) {
|
||||
// Only components can have `viewProviders`, `viewProviders` are registered first and there
|
||||
// can only be one component so we check for it specifically.
|
||||
componentDef.viewProvidersResolver(componentDef);
|
||||
}
|
||||
|
||||
for (let i = 0; i < directivesLength; i++) {
|
||||
const def = directives[i];
|
||||
if (def.providersResolver) def.providersResolver(def);
|
||||
if (def.providersResolver) {
|
||||
def.providersResolver(def);
|
||||
}
|
||||
}
|
||||
let preOrderHooksFound = false;
|
||||
let preOrderCheckHooksFound = false;
|
||||
|
|
|
|||
|
|
@ -1230,6 +1230,50 @@ describe('host directives', () => {
|
|||
hostDirectiveCdr.detectChanges();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
// See #65724.
|
||||
it('should be able to inject host tokens defined through `viewProviders` in a component using host directives', () => {
|
||||
const token = new InjectionToken<ProvidesExisting>('token');
|
||||
let value: ProvidesExisting | undefined | null;
|
||||
|
||||
@Directive({
|
||||
// These providers aren't injected, but they help hit the relevant code path.
|
||||
providers: [{provide: new InjectionToken('unusedToken'), useValue: true}],
|
||||
})
|
||||
class HostDirective {}
|
||||
|
||||
@Directive({selector: '[injectsExisting]'})
|
||||
class InjectsExisting {
|
||||
constructor() {
|
||||
value = inject(token, {host: true, optional: true});
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: '[providesExisting]'})
|
||||
class ProvidesExisting {}
|
||||
|
||||
@Component({
|
||||
selector: 'comp-with-host-directive',
|
||||
template: '<div injectsExisting></div>',
|
||||
imports: [InjectsExisting],
|
||||
hostDirectives: [HostDirective],
|
||||
viewProviders: [{provide: token, useExisting: ProvidesExisting}],
|
||||
})
|
||||
class CompWithHostDirective {}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<comp-with-host-directive providesExisting/>',
|
||||
imports: [ProvidesExisting, CompWithHostDirective],
|
||||
})
|
||||
class App {}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(value).toBeTruthy();
|
||||
expect(value instanceof ProvidesExisting).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
|
|
|
|||
|
|
@ -1070,15 +1070,30 @@ export class TestBedCompiler {
|
|||
|
||||
private patchDefWithProviderOverrides(declaration: Type<any>, field: string): void {
|
||||
const def = (declaration as any)[field];
|
||||
if (def && def.providersResolver) {
|
||||
this.maybeStoreNgDef(field, declaration);
|
||||
|
||||
const resolver = def.providersResolver;
|
||||
const processProvidersFn = (providers: Provider[]) => this.getOverriddenProviders(providers);
|
||||
if (!def) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (def.viewProvidersResolver) {
|
||||
this.maybeStoreNgDef(field, declaration);
|
||||
const viewProvidersResolver = def.viewProvidersResolver;
|
||||
this.storeFieldOfDefOnType(declaration, field, 'viewProvidersResolver');
|
||||
def.viewProvidersResolver = (ngDef: DirectiveDef<any>) =>
|
||||
viewProvidersResolver(ngDef, this.processProviderOverrides);
|
||||
}
|
||||
|
||||
if (def.providersResolver) {
|
||||
this.maybeStoreNgDef(field, declaration);
|
||||
const providersResolver = def.providersResolver;
|
||||
this.storeFieldOfDefOnType(declaration, field, 'providersResolver');
|
||||
def.providersResolver = (ngDef: DirectiveDef<any>) => resolver(ngDef, processProvidersFn);
|
||||
def.providersResolver = (ngDef: DirectiveDef<any>) =>
|
||||
providersResolver(ngDef, this.processProviderOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
private processProviderOverrides = (providers: Provider[]) =>
|
||||
this.getOverriddenProviders(providers);
|
||||
}
|
||||
|
||||
function initResolvers(): Resolvers {
|
||||
|
|
|
|||
Loading…
Reference in a new issue