Skip to content

Commit 64adca1

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 64adca1

2 files changed

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

0 commit comments

Comments
 (0)