diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
index 705b619340..a43ef0e09b 100644
--- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
+++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
@@ -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: \`
+
+
+
+ \`,
+ })
+ 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);
}
}
`;
diff --git a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
index f8899c36da..4b6bbdc537 100644
--- a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
@@ -332,7 +332,7 @@ describe('template source-mapping', () => {
{source: '
', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '',
- 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: '', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
expect(mappings).toContain(
- {source: '', generated: 'i0.ɵɵprojection(3)', sourceUrl: '../test.ts'});
+ {source: '', generated: 'i0.ɵɵprojection(3, 1)', sourceUrl: '../test.ts'});
expect(mappings).toContain(
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index 6b19902457..552dfbd675 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -40,9 +40,6 @@ import {Instruction, StylingBuilder} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
-// Default selector used by `` if none specified
-const DEFAULT_NG_CONTENT_SELECTOR = '*';
-
// Selector attribute name of ``
const NG_CONTENT_SELECT_ATTR = 'select';
@@ -146,14 +143,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
private fileBasedI18nSuffix: string;
- // Whether the template includes tags.
- private _hasNgContent: boolean = false;
-
- // Selectors found in the tags in the template.
- private _ngContentSelectors: string[] = [];
+ // Projection slots found in the template. Projection slots can distribute projected
+ // nodes based on a selector, or can just use the wildcard selector to match
+ // all nodes which aren't matching any selector.
+ private _ngContentReservedSlots: (string|'*')[] = [];
// Number of non-default selectors found in all parent templates of this template. We need to
- // track it to properly adjust projection bucket index in the `projection` instruction.
+ // track it to properly adjust projection slot index in the `projection` instruction.
private _ngContentSelectorsOffset = 0;
constructor(
@@ -247,16 +243,19 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// instructions can be generated with the correct internal const count.
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
- // Output the `projectionDef` instruction when some `` are present.
- // The `projectionDef` instruction only emitted for the component template and it is skipped for
- // nested templates ( tags).
- if (this.level === 0 && this._hasNgContent) {
+ // Output the `projectionDef` instruction when some `` tags are present.
+ // The `projectionDef` instruction is only emitted for the component template and
+ // is skipped for nested templates ( tags).
+ if (this.level === 0 && this._ngContentReservedSlots.length) {
const parameters: o.Expression[] = [];
- // Only selectors with a non-default value are generated
- if (this._ngContentSelectors.length) {
- const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
- parameters.push(this.constantPool.getConstLiteral(asLiteral(r3Selectors), true));
+ // By default the `projectionDef` instructions creates one slot for the wildcard
+ // selector if no parameters are passed. Therefore we only want to allocate a new
+ // array for the projection slots if the default projection slot is not sufficient.
+ if (this._ngContentReservedSlots.length > 1 || this._ngContentReservedSlots[0] !== '*') {
+ const r3ReservedSlots = this._ngContentReservedSlots.map(
+ s => s !== '*' ? core.parseSelectorToR3Selector(s) : s);
+ parameters.push(this.constantPool.getConstLiteral(asLiteral(r3ReservedSlots), true));
}
// Since we accumulate ngContent selectors while processing template elements,
@@ -461,14 +460,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
}
visitContent(ngContent: t.Content) {
- this._hasNgContent = true;
const slot = this.allocateDataSlot();
- let selectorIndex = ngContent.selector === DEFAULT_NG_CONTENT_SELECTOR ?
- 0 :
- this._ngContentSelectors.push(ngContent.selector) + this._ngContentSelectorsOffset;
+ const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length;
const parameters: o.Expression[] = [o.literal(slot)];
const attributes: o.Expression[] = [];
+ this._ngContentReservedSlots.push(ngContent.selector);
+
ngContent.attributes.forEach((attribute) => {
const {name, value} = attribute;
if (name === NG_PROJECT_AS_ATTR_NAME) {
@@ -479,9 +477,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
});
if (attributes.length > 0) {
- parameters.push(o.literal(selectorIndex), o.literalArr(attributes));
- } else if (selectorIndex !== 0) {
- parameters.push(o.literal(selectorIndex));
+ parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes));
+ } else if (projectionSlotIdx !== 0) {
+ parameters.push(o.literal(projectionSlotIdx));
}
this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters);
@@ -887,11 +885,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
this._nestedTemplateFns.push(() => {
const templateFunctionExpr = templateVisitor.buildTemplateFunction(
template.children, template.variables,
- this._ngContentSelectors.length + this._ngContentSelectorsOffset, template.i18n);
+ this._ngContentReservedSlots.length + this._ngContentSelectorsOffset, template.i18n);
this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null));
- if (templateVisitor._hasNgContent) {
- this._hasNgContent = true;
- this._ngContentSelectors.push(...templateVisitor._ngContentSelectors);
+ if (templateVisitor._ngContentReservedSlots.length) {
+ this._ngContentReservedSlots.push(...templateVisitor._ngContentReservedSlots);
}
});
@@ -1011,8 +1008,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
getVarCount() { return this._pureFunctionSlots; }
getNgContentSelectors(): o.Expression|null {
- return this._hasNgContent ?
- this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true) :
+ return this._ngContentReservedSlots.length ?
+ this.constantPool.getConstLiteral(asLiteral(this._ngContentReservedSlots), true) :
null;
}
diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts
index 77fca80daf..803ab65891 100644
--- a/packages/core/src/render3/component_ref.ts
+++ b/packages/core/src/render3/component_ref.ts
@@ -123,10 +123,8 @@ export class ComponentFactory extends viewEngine_ComponentFactory {
super();
this.componentType = componentDef.type;
this.selector = componentDef.selectors[0][0] as string;
- // The component definition does not include the wildcard ('*') selector in its list.
- // It is implicitly expected as the first item in the projectable nodes array.
this.ngContentSelectors =
- componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : [];
+ componentDef.ngContentSelectors ? componentDef.ngContentSelectors : [];
this.isBoundToModule = !!ngModule;
}
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index 22af60ac7a..73b79f84fb 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -121,7 +121,7 @@ export {
ɵɵtextInterpolateV,
} from './instructions/all';
export {RenderFlags} from './interfaces/definition';
-export {CssSelectorList} from './interfaces/projection';
+export {CssSelectorList, ProjectionSlots} from './interfaces/projection';
export {
ɵɵrestoreView,
diff --git a/packages/core/src/render3/instructions/projection.ts b/packages/core/src/render3/instructions/projection.ts
index 9d8f699734..4a76d6e843 100644
--- a/packages/core/src/render3/instructions/projection.ts
+++ b/packages/core/src/render3/instructions/projection.ts
@@ -5,18 +5,47 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-import {assertDataInRange} from '../../util/assert';
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
-import {CssSelectorList} from '../interfaces/projection';
-import {HEADER_OFFSET, TVIEW, T_HOST} from '../interfaces/view';
+import {ProjectionSlots} from '../interfaces/projection';
+import {TVIEW, T_HOST} from '../interfaces/view';
import {appendProjectedNodes} from '../node_manipulation';
-import {matchingProjectionSelectorIndex} from '../node_selector_matcher';
+import {getProjectAsAttrValue, isNodeMatchingSelectorList, isSelectorInSelectorList} from '../node_selector_matcher';
import {getLView, setIsNotParent} from '../state';
import {findComponentView} from '../util/view_traversal_utils';
import {getOrCreateTNode} from './shared';
+/**
+ * Checks a given node against matching projection slots and returns the
+ * determined slot index. Returns "null" if no slot matched the given node.
+ *
+ * This function takes into account the parsed ngProjectAs selector from the
+ * node's attributes. If present, it will check whether the ngProjectAs selector
+ * matches any of the projection slot selectors.
+ */
+export function matchingProjectionSlotIndex(tNode: TNode, projectionSlots: ProjectionSlots): number|
+ null {
+ let wildcardNgContentIndex = null;
+ const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
+ for (let i = 0; i < projectionSlots.length; i++) {
+ const slotValue = projectionSlots[i];
+ // The last wildcard projection slot should match all nodes which aren't matching
+ // any selector. This is necessary to be backwards compatible with view engine.
+ if (slotValue === '*') {
+ wildcardNgContentIndex = i;
+ continue;
+ }
+ // If we ran into an `ngProjectAs` attribute, we should match its parsed selector
+ // to the list of selectors, otherwise we fall back to matching against the node.
+ if (ngProjectAsAttrVal === null ?
+ isNodeMatchingSelectorList(tNode, slotValue, /* isProjectionMode */ true) :
+ isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)) {
+ return i; // first matching selector "captures" a given node
+ }
+ }
+ return wildcardNgContentIndex;
+}
/**
* Instruction to distribute projectable nodes among occurrences in a given template.
@@ -36,32 +65,38 @@ import {getOrCreateTNode} from './shared';
* - we can't have only a parsed as we can't re-construct textual form from it (as entered by a
* template author).
*
- * @param selectors A collection of parsed CSS selectors
- * @param rawSelectors A collection of CSS selectors in the raw, un-parsed form
+ * @param projectionSlots? A collection of projection slots. A projection slot can be based
+ * on a parsed CSS selectors or set to the wildcard selector ("*") in order to match
+ * all nodes which do not match any selector. If not specified, a single wildcard
+ * selector projection slot will be defined.
*
* @codeGenApi
*/
-export function ɵɵprojectionDef(selectors?: CssSelectorList[]): void {
+export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
const componentNode = findComponentView(getLView())[T_HOST] as TElementNode;
if (!componentNode.projection) {
- const noOfNodeBuckets = selectors ? selectors.length + 1 : 1;
+ // If no explicit projection slots are defined, fall back to a single
+ // projection slot with the wildcard selector.
+ const numProjectionSlots = projectionSlots ? projectionSlots.length : 1;
const projectionHeads: (TNode | null)[] = componentNode.projection =
- new Array(noOfNodeBuckets).fill(null);
+ new Array(numProjectionSlots).fill(null);
const tails: (TNode | null)[] = projectionHeads.slice();
let componentChild: TNode|null = componentNode.child;
while (componentChild !== null) {
- const bucketIndex =
- selectors ? matchingProjectionSelectorIndex(componentChild, selectors) : 0;
+ const slotIndex =
+ projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;
- if (tails[bucketIndex]) {
- tails[bucketIndex] !.projectionNext = componentChild;
- } else {
- projectionHeads[bucketIndex] = componentChild;
+ if (slotIndex !== null) {
+ if (tails[slotIndex]) {
+ tails[slotIndex] !.projectionNext = componentChild;
+ } else {
+ projectionHeads[slotIndex] = componentChild;
+ }
+ tails[slotIndex] = componentChild;
}
- tails[bucketIndex] = componentChild;
componentChild = componentChild.next;
}
diff --git a/packages/core/src/render3/interfaces/projection.ts b/packages/core/src/render3/interfaces/projection.ts
index 891dd28135..6657ab7ff9 100644
--- a/packages/core/src/render3/interfaces/projection.ts
+++ b/packages/core/src/render3/interfaces/projection.ts
@@ -50,6 +50,16 @@ export type CssSelector = (string | SelectorFlags)[];
*/
export type CssSelectorList = CssSelector[];
+/**
+ * List of slots for a projection. A slot can be either based on a parsed CSS selector
+ * which will be used to determine nodes which are projected into that slot.
+ *
+ * When set to "*", the slot is reserved and can be used for multi-slot projection
+ * using {@link ViewContainerRef#createComponent}. The last slot that specifies the
+ * wildcard selector will retrieve all projectable nodes which do not match any selector.
+ */
+export type ProjectionSlots = (CssSelectorList | '*')[];
+
/** Flags used to build up CssSelectors */
export const enum SelectorFlags {
/** Indicates this is the beginning of a new negative selector */
diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts
index a65facee12..33c88df692 100644
--- a/packages/core/src/render3/node_selector_matcher.ts
+++ b/packages/core/src/render3/node_selector_matcher.ts
@@ -257,29 +257,6 @@ export function getProjectAsAttrValue(tNode: TNode): CssSelector|null {
return null;
}
-/**
- * Checks a given node against matching projection selectors and returns
- * selector index (or 0 if none matched).
- *
- * This function takes into account the parsed ngProjectAs selector from the node's attributes.
- * If present, it will check whether the ngProjectAs selector matches any of the projection
- * selectors.
- */
-export function matchingProjectionSelectorIndex(
- tNode: TNode, selectors: CssSelectorList[]): number {
- const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
- for (let i = 0; i < selectors.length; i++) {
- // If we ran into an `ngProjectAs` attribute, we should match its parsed selector
- // to the list of selectors, otherwise we fall back to matching against the node.
- if (ngProjectAsAttrVal === null ?
- isNodeMatchingSelectorList(tNode, selectors[i], /* isProjectionMode */ true) :
- isSelectorInSelectorList(ngProjectAsAttrVal, selectors[i])) {
- return i + 1; // first matching selector "captures" a given node
- }
- }
- return 0;
-}
-
function getNameOnlyMarkerIndex(nodeAttrs: TAttributes) {
for (let i = 0; i < nodeAttrs.length; i++) {
const nodeAttr = nodeAttrs[i];
@@ -307,7 +284,7 @@ function matchTemplateAttribute(attrs: TAttributes, name: string): number {
* @param selector Selector to be checked.
* @param list List in which to look for the selector.
*/
-function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean {
+export function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean {
selectorListLoop: for (let i = 0; i < list.length; i++) {
const currentSelectorInList = list[i];
if (selector.length !== currentSelectorInList.length) {
diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts
index 45d4a5d10f..97eb4f8db0 100644
--- a/packages/core/test/acceptance/view_container_ref_spec.ts
+++ b/packages/core/test/acceptance/view_container_ref_spec.ts
@@ -963,18 +963,10 @@ describe('ViewContainerRef', () => {
[[myNode]]);
fixture.detectChanges();
- // With Ivy the projected content is inserted into the last ng-content container,
- // while with View Engine the content is projected into the first ng-content slot.
- // View Engine correctly respects the passed index of "projectedNodes". See: FW-1331.
- if (ivyEnabled) {
- expect(getElementHtml(fixture.nativeElement))
- .toEqual(
- '
barbaz
');
- } else {
- expect(getElementHtml(fixture.nativeElement))
- .toEqual(
- 'barbaz
');
- }
+
+ expect(getElementHtml(fixture.nativeElement))
+ .toEqual(
+ 'barbaz
');
});
it('should support reprojection of projectable nodes', () => {
@@ -1039,17 +1031,9 @@ describe('ViewContainerRef', () => {
]);
fixture.detectChanges();
- // With Ivy multi-slot projection is currently not working. This is a bug that
- // is tracked with FW-1333.
- if (ivyEnabled) {
- expect(getElementHtml(fixture.nativeElement))
- .toEqual(
- '
12');
- } else {
- expect(getElementHtml(fixture.nativeElement))
- .toEqual(
- '12
34');
- }
+ expect(getElementHtml(fixture.nativeElement))
+ .toEqual(
+ '12
34');
});
});
diff --git a/packages/core/test/linker/projection_integration_spec.ts b/packages/core/test/linker/projection_integration_spec.ts
index 7829aaf48a..8e528141fd 100644
--- a/packages/core/test/linker/projection_integration_spec.ts
+++ b/packages/core/test/linker/projection_integration_spec.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {CommonModule} from '@angular/common';
import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, Injector, Input, NO_ERRORS_SCHEMA, NgModule, OnInit, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
@@ -111,13 +112,48 @@ describe('projection', () => {
expect(main.nativeElement).toHaveText('(A, BC)');
});
- modifiedInIvy(
- 'FW-886: `projectableNodes` passed to a componentFactory should be in the order of declaration')
- .it('should support passing projectable nodes via factory function', () => {
+ it('should support passing projectable nodes via factory function', () => {
+ @Component({
+ selector: 'multiple-content-tags',
+ template: '(, )',
+ })
+ class MultipleContentTagsComponent {
+ }
+ @NgModule({
+ declarations: [MultipleContentTagsComponent],
+ entryComponents: [MultipleContentTagsComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ })
+ class MyModule {
+ }
+
+ TestBed.configureTestingModule({imports: [MyModule]});
+ const injector: Injector = TestBed.get(Injector);
+
+ const componentFactoryResolver: ComponentFactoryResolver =
+ injector.get(ComponentFactoryResolver);
+ const componentFactory =
+ componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent);
+ expect(componentFactory.ngContentSelectors).toEqual(['h1', '*']);
+
+ const nodeOne = getDOM().createTextNode('one');
+ const nodeTwo = getDOM().createTextNode('two');
+ const component = componentFactory.create(injector, [[nodeOne], [nodeTwo]]);
+ expect(component.location.nativeElement).toHaveText('(one, two)');
+ });
+
+ modifiedInIvy(
+ 'FW-886: `projectableNodes` passed to a componentFactory should be in the order of' +
+ 'declaration. In Ivy, the ng-content slots are determined with breadth-first search.')
+ .it('should respect order of declaration for projectable nodes', () => {
@Component({
selector: 'multiple-content-tags',
- template: '(, )',
+ template: `
+ 1
+ 2
+ 3
+ `,
})
class MultipleContentTagsComponent {
}
@@ -125,6 +161,7 @@ describe('projection', () => {
@NgModule({
declarations: [MultipleContentTagsComponent],
entryComponents: [MultipleContentTagsComponent],
+ imports: [CommonModule],
schemas: [NO_ERRORS_SCHEMA],
})
class MyModule {
@@ -137,12 +174,14 @@ describe('projection', () => {
injector.get(ComponentFactoryResolver);
const componentFactory =
componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent);
- expect(componentFactory.ngContentSelectors).toEqual(['h1', '*']);
+ expect(componentFactory.ngContentSelectors).toEqual(['h1', '*', 'h2']);
const nodeOne = getDOM().createTextNode('one');
const nodeTwo = getDOM().createTextNode('two');
- const component = componentFactory.create(injector, [[nodeOne], [nodeTwo]]);
- expect(component.location.nativeElement).toHaveText('(one, two)');
+ const nodeThree = getDOM().createTextNode('three');
+ const component = componentFactory.create(injector, [[nodeOne], [nodeTwo], [nodeThree]]);
+ component.changeDetectorRef.detectChanges();
+ expect(component.location.nativeElement.textContent.trim()).toBe('1one 2two 3three');
});
it('should redistribute only direct children', () => {
diff --git a/packages/core/test/render3/component_ref_spec.ts b/packages/core/test/render3/component_ref_spec.ts
index 9af77d2d46..71c61c6d4f 100644
--- a/packages/core/test/render3/component_ref_spec.ts
+++ b/packages/core/test/render3/component_ref_spec.ts
@@ -48,7 +48,7 @@ describe('ComponentFactory', () => {
consts: 0,
vars: 0,
template: () => undefined,
- ngContentSelectors: ['a', 'b'],
+ ngContentSelectors: ['*', 'a', 'b'],
factory: () => new TestComponent(),
inputs: {
in1: 'in1',
diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts
index 49aa362f8f..2f1beaa77b 100644
--- a/packages/core/test/render3/content_spec.ts
+++ b/packages/core/test/render3/content_spec.ts
@@ -913,7 +913,7 @@ describe('content projection', () => {
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]);
+ ɵɵprojectionDef(['*', [['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]);
ɵɵelementStart(0, 'div', ['id', 'first']);
{ ɵɵprojection(1, 1); }
ɵɵelementEnd();
@@ -958,7 +958,7 @@ describe('content projection', () => {
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵprojectionDef([
- [['span', SelectorFlags.CLASS, 'toFirst']],
+ '*', [['span', SelectorFlags.CLASS, 'toFirst']],
[['span', SelectorFlags.CLASS, 'toSecond']]
]);
ɵɵelementStart(0, 'div', ['id', 'first']);
@@ -1005,7 +1005,7 @@ describe('content projection', () => {
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵprojectionDef([
- [['span', SelectorFlags.CLASS, 'toFirst']],
+ '*', [['span', SelectorFlags.CLASS, 'toFirst']],
[['span', SelectorFlags.CLASS, 'toSecond']]
]);
ɵɵelementStart(0, 'div', ['id', 'first']);
@@ -1051,7 +1051,7 @@ describe('content projection', () => {
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]);
+ ɵɵprojectionDef(['*', [['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]);
ɵɵelementStart(0, 'div', ['id', 'first']);
{ ɵɵprojection(1, 1); }
ɵɵelementEnd();
@@ -1095,7 +1095,7 @@ describe('content projection', () => {
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]]);
+ ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toFirst']]]);
ɵɵelementStart(0, 'div', ['id', 'first']);
{ ɵɵprojection(1, 1); }
ɵɵelementEnd();
@@ -1140,7 +1140,7 @@ describe('content projection', () => {
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]]);
+ ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toSecond']]]);
ɵɵelementStart(0, 'div', ['id', 'first']);
{ ɵɵprojection(1); }
ɵɵelementEnd();
@@ -1192,7 +1192,7 @@ describe('content projection', () => {
*/
const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['span']]]);
+ ɵɵprojectionDef(['*', [['span']]]);
ɵɵprojection(0, 1);
ɵɵelement(1, 'hr');
ɵɵprojection(2);
@@ -1253,7 +1253,7 @@ describe('content projection', () => {
*/
const Card = createComponent('card', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['', 'card-title', '']], [['', 'card-content', '']]]);
+ ɵɵprojectionDef(['*', [['', 'card-title', '']], [['', 'card-content', '']]]);
ɵɵprojection(0, 1);
ɵɵelement(1, 'hr');
ɵɵprojection(2, 2);
@@ -1306,7 +1306,7 @@ describe('content projection', () => {
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- ɵɵprojectionDef([[['div']]]);
+ ɵɵprojectionDef(['*', [['div']]]);
ɵɵprojection(0, 1);
}
}, 1);
diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts
index d72f29d65e..f07ece41d6 100644
--- a/tools/public_api_guard/core/core.d.ts
+++ b/tools/public_api_guard/core/core.d.ts
@@ -940,7 +940,7 @@ export declare type ɵɵPipeDefWithMeta = PipeDef;
export declare function ɵɵprojection(nodeIndex: number, selectorIndex?: number, attrs?: TAttributes): void;
-export declare function ɵɵprojectionDef(selectors?: CssSelectorList[]): void;
+export declare function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void;
export declare function ɵɵproperty(propName: string, value: T, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009;