fix(ivy): ensure component/directive class selectors are properly understood (#27849)

Angular allows for `<ng-content>` elements to include a selector which
filters which content-projected entries are inserted into the container
depending on whether or not the selector is matched.

With Ivy this feature has not fully worked due to the massive changes
that took place inside of Ivy's styling algorithm code (which is
responsible for assigning classes and styles to an element). This
fix ensures that content-projection can correctly identify which slot
an element should be placed into when class-based selectors are used.

PR Close #27849
This commit is contained in:
Matias Niemelä
2019-01-11 08:37:51 -08:00
committed by Andrew Kushnir
parent 06e5bf1661
commit e62eeed7d4
4 changed files with 179 additions and 113 deletions

View File

@ -81,22 +81,35 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('');
});
fixmeIvy('FW-833: Directive / projected node matching against class name')
.it('should support multiple content tags', () => {
TestBed.configureTestingModule({declarations: [MultipleContentTagsComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<div>B</div>' +
'<div>C</div>' +
'<div class="left">A</div>' +
'</multiple-content-tags>'
}
});
const main = TestBed.createComponent(MainComp);
it('should project a single class-based tag', () => {
TestBed.configureTestingModule({declarations: [SingleContentTagComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<single-content-tag>' +
'<div class="target">I AM PROJECTED</div>' +
'</single-content-tag>'
}
});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('(A, BC)');
});
expect(main.nativeElement).toHaveText('I AM PROJECTED');
});
fixmeIvy('unknown').it('should support multiple content tags', () => {
TestBed.configureTestingModule({declarations: [MultipleContentTagsComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<div>B</div>' +
'<div>C</div>' +
'<div class="left">A</div>' +
'</multiple-content-tags>'
}
});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('(A, BC)');
});
it('should redistribute only direct children', () => {
TestBed.configureTestingModule({declarations: [MultipleContentTagsComponent]});
@ -182,35 +195,34 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(A,BC)))');
});
fixmeIvy('FW-833: Directive / projected node matching against class name')
.it('should redistribute when the shadow dom changes', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'<div>C</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
fixmeIvy('unknown').it('should redistribute when the shadow dom changes', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'<div>C</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
const viewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const viewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('(, BC)');
expect(main.nativeElement).toHaveText('(, BC)');
viewportDirective.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(A, BC)');
viewportDirective.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(A, BC)');
viewportDirective.hide();
main.detectChanges();
expect(main.nativeElement).toHaveText('(, BC)');
});
viewportDirective.hide();
main.detectChanges();
expect(main.nativeElement).toHaveText('(, BC)');
});
// GH-2095 - https://github.com/angular/angular/issues/2095
// important as we are removing the ng-content element during compilation,
@ -290,38 +302,36 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('SIMPLE()START(A)END');
});
fixmeIvy('FW-833: Directive / projected node matching against class name')
.it('should support moving ng-content around', () => {
TestBed.configureTestingModule({
declarations: [ConditionalContentComponent, ProjectDirective, ManualViewportDirective]
});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'</conditional-content>' +
'START(<div project></div>)END'
}
});
const main = TestBed.createComponent(MainComp);
fixmeIvy('unknown').it('should support moving ng-content around', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ProjectDirective, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'</conditional-content>' +
'START(<div project></div>)END'
}
});
const main = TestBed.createComponent(MainComp);
const sourceDirective: ManualViewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const projectDirective: ProjectDirective =
main.debugElement.queryAllNodes(By.directive(ProjectDirective))[0].injector.get(
ProjectDirective);
expect(main.nativeElement).toHaveText('(, B)START()END');
const sourceDirective: ManualViewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const projectDirective: ProjectDirective =
main.debugElement.queryAllNodes(By.directive(ProjectDirective))[0].injector.get(
ProjectDirective);
expect(main.nativeElement).toHaveText('(, B)START()END');
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
// Stamping ng-content multiple times should not produce the content multiple
// times...
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
});
// Stamping ng-content multiple times should not produce the content multiple
// times...
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
});
// Note: This does not use a ng-content element, but
// is still important as we are merging proto views independent of
@ -533,49 +543,48 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('B(A)');
});
fixmeIvy('FW-833: Directive / projected node matching against class name')
.it('should project filled view containers into a view container', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<ng-template manual class="left">B</ng-template>' +
'<div class="left">C</div>' +
'<div>D</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
fixmeIvy('unknown').it('should project filled view containers into a view container', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<ng-template manual class="left">B</ng-template>' +
'<div class="left">C</div>' +
'<div>D</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
const conditionalComp = main.debugElement.query(By.directive(ConditionalContentComponent));
const conditionalComp = main.debugElement.query(By.directive(ConditionalContentComponent));
const viewViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const viewViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('(, D)');
expect(main.nativeElement).toHaveText('(, D)');
expect(main.nativeElement).toHaveText('(, D)');
expect(main.nativeElement).toHaveText('(, D)');
viewViewportDir.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(AC, D)');
viewViewportDir.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(AC, D)');
const contentViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[1].injector.get(
ManualViewportDirective);
const contentViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[1].injector.get(
ManualViewportDirective);
contentViewportDir.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(ABC, D)');
contentViewportDir.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('(ABC, D)');
// hide view viewport, and test that it also hides
// the content viewport's views
viewViewportDir.hide();
main.detectChanges();
expect(main.nativeElement).toHaveText('(, D)');
});
// hide view viewport, and test that it also hides
// the content viewport's views
viewViewportDir.hide();
main.detectChanges();
expect(main.nativeElement).toHaveText('(, D)');
});
});
@Component({selector: 'main', template: ''})
@ -626,6 +635,13 @@ class Empty {
class MultipleContentTagsComponent {
}
@Component({
selector: 'single-content-tag',
template: '<ng-content SELECT=".target"></ng-content>',
})
class SingleContentTagComponent {
}
@Directive({selector: '[manual]'})
class ManualViewportDirective {
constructor(public vc: ViewContainerRef, public templateRef: TemplateRef<Object>) {}