fix(ivy): unable to project into multiple slots with default selector (#30561)

With View engine it was possible to declare multiple projection
definitions and to programmatically project nodes into the slots.

e.g.

```html
<ng-content></ng-content>
<ng-content></ng-content>
```

Using `ViewContainerRef#createComponent` allowed projecting
nodes into one of the projection defs (through index)

This no longer works with Ivy as the `projectionDef` instruction only
retrieves a list of selectors instead of also retrieving entries for
reserved projection slots which appear when using the default
selector multiple times (as seen above).

In order to fix this issue, the Ivy compiler now passes all
projection slots to the `projectionDef` instruction. Meaning that
there can be multiple projection slots with the same wildcard
selector. This allows multi-slot projection as seen in the
example above, and it also allows us to match the multi-slot node
projection order from View Engine (to avoid breaking changes).

It basically ensures that Ivy fully matches the View Engine behavior
except of a very small edge case that has already been discussed
in FW-886 (with the conclusion of working as intended).

Read more here: https://hackmd.io/s/Sy2kQlgTE

PR Close #30561
This commit is contained in:
Paul Gschwendtner
2019-05-21 22:00:47 +02:00
committed by Miško Hevery
parent f4cd3740b2
commit aca339e864
13 changed files with 219 additions and 131 deletions

View File

@ -1159,7 +1159,7 @@ describe('compiler compliance', () => {
type: SimpleComponent,
selectors: [["simple"]],
factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); },
ngContentSelectors: _c0,
ngContentSelectors: $c0$,
consts: 2,
vars: 0,
template: function SimpleComponent_Template(rf, ctx) {
@ -1189,10 +1189,10 @@ describe('compiler compliance', () => {
if (rf & 1) {
$r3$.ɵɵprojectionDef($c1$);
$r3$.ɵɵelementStart(0, "div", $c3$);
$r3$.ɵɵprojection(1, 1);
$r3$.ɵɵprojection(1);
$r3$.ɵɵelementEnd();
$r3$.ɵɵelementStart(2, "div", $c4$);
$r3$.ɵɵprojection(3, 2);
$r3$.ɵɵprojection(3, 1);
$r3$.ɵɵelementEnd();
}
},
@ -1209,6 +1209,54 @@ describe('compiler compliance', () => {
result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition');
});
it('should support multi-slot content projection with multiple wildcard slots', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
template: \`
<ng-content></ng-content>
<ng-content select="[spacer]"></ng-content>
<ng-content></ng-content>
\`,
})
class Cmp {}
@NgModule({ declarations: [Cmp] })
class Module {}
`,
}
};
const output = `
const $c0$ = ["*", [["", "spacer", ""]], "*"];
const $c1$ = ["*", "[spacer]", "*"];
Cmp.ngComponentDef = $r3$.ɵɵdefineComponent({
type: Cmp,
selectors: [["ng-component"]],
factory: function Cmp_Factory(t) { return new (t || Cmp)(); },
ngContentSelectors: $c1$,
consts: 3,
vars: 0,
template: function Cmp_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵprojectionDef($c0$);
i0.ɵɵprojection(0);
i0.ɵɵprojection(1, 1);
i0.ɵɵprojection(2, 2);
}
},
encapsulation: 2
});
`;
const {source} = compile(files, angularFiles);
expectEmit(source, output, 'Invalid content projection instructions generated');
});
it('should support content projection in nested templates', () => {
const files = {
app: {
@ -1241,7 +1289,7 @@ describe('compiler compliance', () => {
const $_c2$ = ["id", "second"];
function Cmp_div_0_Template(rf, ctx) { if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", $_c2$);
$r3$.ɵɵprojection(1, 1);
$r3$.ɵɵprojection(1);
$r3$.ɵɵelementEnd();
} }
const $_c3$ = ["id", "third"];
@ -1255,10 +1303,10 @@ describe('compiler compliance', () => {
function Cmp_ng_template_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " '*' selector: ");
$r3$.ɵɵprojection(1);
$r3$.ɵɵprojection(1, 1);
}
}
const $_c4$ = [[["span", "title", "tofirst"]]];
const $_c4$ = [[["span", "title", "tofirst"]], "*"];
template: function Cmp_Template(rf, ctx) {
if (rf & 1) {
@ -1312,31 +1360,31 @@ describe('compiler compliance', () => {
const output = `
function Cmp_ng_template_1_ng_template_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojection(0, 4);
$r3$.ɵɵprojection(0, 3);
}
}
function Cmp_ng_template_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojection(0, 3);
$r3$.ɵɵprojection(0, 2);
$r3$.ɵɵtemplate(1, Cmp_ng_template_1_ng_template_1_Template, 1, 0, "ng-template");
}
}
function Cmp_ng_template_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " '*' selector in a template: ");
$r3$.ɵɵprojection(1);
$r3$.ɵɵprojection(1, 4);
}
}
const $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]]];
const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]"];
const $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]], "*"];
const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]", "*"];
template: function Cmp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojectionDef($_c2$);
$r3$.ɵɵprojection(0, 1);
$r3$.ɵɵprojectionDef($_c0$);
$r3$.ɵɵprojection(0);
$r3$.ɵɵtemplate(1, Cmp_ng_template_1_Template, 2, 0, "ng-template");
$r3$.ɵɵtemplate(2, Cmp_ng_template_2_Template, 2, 0, "ng-template");
$r3$.ɵɵprojection(3, 2);
$r3$.ɵɵprojection(3, 1);
}
}
`;

View File

@ -332,7 +332,7 @@ describe('template source-mapping', () => {
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '<ng-content select="title">',
generated: 'i0.ɵɵprojection(1, 1)',
generated: 'i0.ɵɵprojection(1)',
sourceUrl: '../test.ts'
});
expect(mappings).toContain(
@ -340,7 +340,7 @@ describe('template source-mapping', () => {
expect(mappings).toContain(
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '<ng-content>', generated: 'i0.ɵɵprojection(3)', sourceUrl: '../test.ts'});
{source: '<ng-content>', generated: 'i0.ɵɵprojection(3, 1)', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});