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;