Skip to content

Commit bf9db7f

Browse files
committed
fix(core): ViewProviders are injected into projected content if new flow-syntax is used
Prevent viewProviders from leaking into projected content when using new control flow syntax Fixed angular#63312
1 parent 47cd604 commit bf9db7f

2 files changed

Lines changed: 150 additions & 4 deletions

File tree

packages/core/src/render3/di.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,9 +567,13 @@ function lookupTokenUsingNodeInjector<T>(
567567
if (parentLocation === NO_PARENT_INJECTOR || !shouldSearchParent(flags, false)) {
568568
injectorIndex = -1;
569569
} else {
570+
const viewOffset = parentLocation >> RelativeInjectorLocationFlags.ViewOffsetShift;
570571
previousTView = lView[TVIEW];
572+
for (let i = 0; i < viewOffset; i++) {
573+
previousTView = lView[TVIEW];
574+
lView = lView[DECLARATION_VIEW]!;
575+
}
571576
injectorIndex = getParentInjectorIndex(parentLocation);
572-
lView = getParentInjectorView(parentLocation, lView);
573577
}
574578
}
575579

@@ -609,9 +613,13 @@ function lookupTokenUsingNodeInjector<T>(
609613
) {
610614
// The def wasn't found anywhere on this node, so it was a false positive.
611615
// Traverse up the tree and continue searching.
612-
previousTView = tView;
616+
const viewOffset = parentLocation >> RelativeInjectorLocationFlags.ViewOffsetShift;
617+
previousTView = lView[TVIEW];
618+
for (let i = 0; i < viewOffset; i++) {
619+
previousTView = lView[TVIEW];
620+
lView = lView[DECLARATION_VIEW]!;
621+
}
613622
injectorIndex = getParentInjectorIndex(parentLocation);
614-
lView = getParentInjectorView(parentLocation, lView);
615623
} else {
616624
// If we should not search parent OR If the ancestor bloom filter value does not have the
617625
// bit corresponding to the directive we can give up on traversing up to find the specific
@@ -652,7 +660,9 @@ function searchTokensOnInjector<T>(
652660
// - AND the parent TNode is an Element.
653661
// This means that we just came from the Component's View and therefore are allowed to see
654662
// into the ViewProviders.
655-
previousTView != currentTView && (tNode.type & TNodeType.AnyRNode) !== 0;
663+
previousTView != currentTView &&
664+
(tNode.type & TNodeType.Element) !== 0 &&
665+
previousTView.type === TViewType.Component;
656666

657667
// This special case happens when there is a @host on the inject and when we are searching
658668
// on the host element node.

packages/core/test/acceptance/di_spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7193,4 +7193,140 @@ describe('di', () => {
71937193
);
71947194
});
71957195
});
7196+
7197+
describe('control flow + viewProviders', () => {
7198+
it('should not allow projected content to see viewProviders when wrapped in @if', () => {
7199+
const token = new InjectionToken<string>('token');
7200+
7201+
@Component({
7202+
selector: 'app-child-component',
7203+
standalone: true,
7204+
template: '{{value}}',
7205+
})
7206+
class ChildComponent {
7207+
value = inject(token);
7208+
}
7209+
7210+
@Component({
7211+
selector: 'app-provider-component',
7212+
standalone: true,
7213+
imports: [],
7214+
providers: [{provide: token, useValue: 'provider'}],
7215+
viewProviders: [{provide: token, useValue: 'viewProvider'}],
7216+
template: `
7217+
<div>
7218+
Projected<br />
7219+
<ng-content></ng-content>
7220+
</div>
7221+
`,
7222+
})
7223+
class ProviderComponent {}
7224+
7225+
@Component({
7226+
selector: 'app-test-new-flow',
7227+
standalone: true,
7228+
imports: [ProviderComponent, ChildComponent],
7229+
template: `
7230+
<app-provider-component>
7231+
@if (flag) {
7232+
<app-child-component />
7233+
}
7234+
</app-provider-component>
7235+
`,
7236+
})
7237+
class TestNewFlowComponent {
7238+
flag = true;
7239+
}
7240+
7241+
@Component({
7242+
selector: 'app-test-old-flow',
7243+
standalone: true,
7244+
imports: [ProviderComponent, ChildComponent, CommonModule],
7245+
template: `
7246+
<app-provider-component>
7247+
<app-child-component *ngIf="flag" />
7248+
</app-provider-component>
7249+
`,
7250+
})
7251+
class TestOldFlowComponent {
7252+
flag = true;
7253+
}
7254+
7255+
// Test old syntax it's ok
7256+
const oldFixture = TestBed.createComponent(TestOldFlowComponent);
7257+
oldFixture.detectChanges();
7258+
expect(oldFixture.nativeElement.textContent).toContain('provider');
7259+
expect(oldFixture.nativeElement.textContent).not.toContain('viewProvider');
7260+
7261+
// Test that the new syntax behaves like the old one
7262+
const newFixture = TestBed.createComponent(TestNewFlowComponent);
7263+
newFixture.detectChanges();
7264+
expect(newFixture.nativeElement.textContent).toContain('provider');
7265+
expect(newFixture.nativeElement.textContent).not.toContain('viewProvider');
7266+
});
7267+
7268+
it('should allow a directive in an @if block to inject a token from viewProviders', () => {
7269+
const TOKEN = new InjectionToken<string>('token');
7270+
7271+
@Directive({
7272+
selector: '[testDir]',
7273+
standalone: true,
7274+
})
7275+
class TestDir {
7276+
value = inject(TOKEN);
7277+
}
7278+
7279+
@Component({
7280+
selector: 'test-comp',
7281+
standalone: true,
7282+
imports: [TestDir],
7283+
viewProviders: [{provide: TOKEN, useValue: 'view-value'}],
7284+
template: `
7285+
@if (true) {
7286+
<div testDir></div>
7287+
}
7288+
`,
7289+
})
7290+
class TestComp {
7291+
@ViewChild(TestDir) testDir!: TestDir;
7292+
}
7293+
7294+
const fixture = TestBed.createComponent(TestComp);
7295+
fixture.detectChanges();
7296+
7297+
expect(fixture.componentInstance.testDir.value).toBe('view-value');
7298+
});
7299+
7300+
it('should allow a directive in nested @if blocks to inject a token from viewProviders', () => {
7301+
const TOKEN = new InjectionToken<string>('token');
7302+
7303+
@Directive({
7304+
selector: '[testDir]',
7305+
standalone: true,
7306+
})
7307+
class TestDir {
7308+
value = inject(TOKEN);
7309+
}
7310+
7311+
@Component({
7312+
selector: 'test-comp',
7313+
standalone: true,
7314+
imports: [TestDir],
7315+
viewProviders: [{provide: TOKEN, useValue: 'view-value'}],
7316+
template: `
7317+
@if (true) {
7318+
<div testDir></div>
7319+
}
7320+
`,
7321+
})
7322+
class TestComp {
7323+
@ViewChild(TestDir) testDir!: TestDir;
7324+
}
7325+
7326+
const fixture = TestBed.createComponent(TestComp);
7327+
fixture.detectChanges();
7328+
7329+
expect(fixture.componentInstance.testDir.value).toBe('view-value');
7330+
});
7331+
});
71967332
});

0 commit comments

Comments
 (0)