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:
parent
f4cd3740b2
commit
aca339e864
|
@ -1159,7 +1159,7 @@ describe('compiler compliance', () => {
|
||||||
type: SimpleComponent,
|
type: SimpleComponent,
|
||||||
selectors: [["simple"]],
|
selectors: [["simple"]],
|
||||||
factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); },
|
factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); },
|
||||||
ngContentSelectors: _c0,
|
ngContentSelectors: $c0$,
|
||||||
consts: 2,
|
consts: 2,
|
||||||
vars: 0,
|
vars: 0,
|
||||||
template: function SimpleComponent_Template(rf, ctx) {
|
template: function SimpleComponent_Template(rf, ctx) {
|
||||||
|
@ -1189,10 +1189,10 @@ describe('compiler compliance', () => {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵprojectionDef($c1$);
|
$r3$.ɵɵprojectionDef($c1$);
|
||||||
$r3$.ɵɵelementStart(0, "div", $c3$);
|
$r3$.ɵɵelementStart(0, "div", $c3$);
|
||||||
$r3$.ɵɵprojection(1, 1);
|
$r3$.ɵɵprojection(1);
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(2, "div", $c4$);
|
$r3$.ɵɵelementStart(2, "div", $c4$);
|
||||||
$r3$.ɵɵprojection(3, 2);
|
$r3$.ɵɵprojection(3, 1);
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1209,6 +1209,54 @@ describe('compiler compliance', () => {
|
||||||
result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition');
|
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', () => {
|
it('should support content projection in nested templates', () => {
|
||||||
const files = {
|
const files = {
|
||||||
app: {
|
app: {
|
||||||
|
@ -1241,7 +1289,7 @@ describe('compiler compliance', () => {
|
||||||
const $_c2$ = ["id", "second"];
|
const $_c2$ = ["id", "second"];
|
||||||
function Cmp_div_0_Template(rf, ctx) { if (rf & 1) {
|
function Cmp_div_0_Template(rf, ctx) { if (rf & 1) {
|
||||||
$r3$.ɵɵelementStart(0, "div", $_c2$);
|
$r3$.ɵɵelementStart(0, "div", $_c2$);
|
||||||
$r3$.ɵɵprojection(1, 1);
|
$r3$.ɵɵprojection(1);
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
} }
|
} }
|
||||||
const $_c3$ = ["id", "third"];
|
const $_c3$ = ["id", "third"];
|
||||||
|
@ -1255,10 +1303,10 @@ describe('compiler compliance', () => {
|
||||||
function Cmp_ng_template_2_Template(rf, ctx) {
|
function Cmp_ng_template_2_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵtext(0, " '*' selector: ");
|
$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) {
|
template: function Cmp_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
|
@ -1312,31 +1360,31 @@ describe('compiler compliance', () => {
|
||||||
const output = `
|
const output = `
|
||||||
function Cmp_ng_template_1_ng_template_1_Template(rf, ctx) {
|
function Cmp_ng_template_1_ng_template_1_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵprojection(0, 4);
|
$r3$.ɵɵprojection(0, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function Cmp_ng_template_1_Template(rf, ctx) {
|
function Cmp_ng_template_1_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
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");
|
$r3$.ɵɵtemplate(1, Cmp_ng_template_1_ng_template_1_Template, 1, 0, "ng-template");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function Cmp_ng_template_2_Template(rf, ctx) {
|
function Cmp_ng_template_2_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵtext(0, " '*' selector in a template: ");
|
$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 $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]], "*"];
|
||||||
const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]"];
|
const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]", "*"];
|
||||||
…
|
…
|
||||||
template: function Cmp_Template(rf, ctx) {
|
template: function Cmp_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵprojectionDef($_c2$);
|
$r3$.ɵɵprojectionDef($_c0$);
|
||||||
$r3$.ɵɵprojection(0, 1);
|
$r3$.ɵɵprojection(0);
|
||||||
$r3$.ɵɵtemplate(1, Cmp_ng_template_1_Template, 2, 0, "ng-template");
|
$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$.ɵɵtemplate(2, Cmp_ng_template_2_Template, 2, 0, "ng-template");
|
||||||
$r3$.ɵɵprojection(3, 2);
|
$r3$.ɵɵprojection(3, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -332,7 +332,7 @@ describe('template source-mapping', () => {
|
||||||
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
|
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
|
||||||
expect(mappings).toContain({
|
expect(mappings).toContain({
|
||||||
source: '<ng-content select="title">',
|
source: '<ng-content select="title">',
|
||||||
generated: 'i0.ɵɵprojection(1, 1)',
|
generated: 'i0.ɵɵprojection(1)',
|
||||||
sourceUrl: '../test.ts'
|
sourceUrl: '../test.ts'
|
||||||
});
|
});
|
||||||
expect(mappings).toContain(
|
expect(mappings).toContain(
|
||||||
|
@ -340,7 +340,7 @@ describe('template source-mapping', () => {
|
||||||
expect(mappings).toContain(
|
expect(mappings).toContain(
|
||||||
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
|
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
|
||||||
expect(mappings).toContain(
|
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(
|
expect(mappings).toContain(
|
||||||
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
|
{source: '</div>', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.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';
|
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||||
|
|
||||||
|
|
||||||
// Default selector used by `<ng-content>` if none specified
|
|
||||||
const DEFAULT_NG_CONTENT_SELECTOR = '*';
|
|
||||||
|
|
||||||
// Selector attribute name of `<ng-content>`
|
// Selector attribute name of `<ng-content>`
|
||||||
const NG_CONTENT_SELECT_ATTR = 'select';
|
const NG_CONTENT_SELECT_ATTR = 'select';
|
||||||
|
|
||||||
|
@ -146,14 +143,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
private fileBasedI18nSuffix: string;
|
private fileBasedI18nSuffix: string;
|
||||||
|
|
||||||
// Whether the template includes <ng-content> tags.
|
// Projection slots found in the template. Projection slots can distribute projected
|
||||||
private _hasNgContent: boolean = false;
|
// nodes based on a selector, or can just use the wildcard selector to match
|
||||||
|
// all nodes which aren't matching any selector.
|
||||||
// Selectors found in the <ng-content> tags in the template.
|
private _ngContentReservedSlots: (string|'*')[] = [];
|
||||||
private _ngContentSelectors: string[] = [];
|
|
||||||
|
|
||||||
// Number of non-default selectors found in all parent templates of this template. We need to
|
// 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;
|
private _ngContentSelectorsOffset = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -247,16 +243,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
// instructions can be generated with the correct internal const count.
|
// instructions can be generated with the correct internal const count.
|
||||||
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
|
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
|
||||||
|
|
||||||
// Output the `projectionDef` instruction when some `<ng-content>` are present.
|
// Output the `projectionDef` instruction when some `<ng-content>` tags are present.
|
||||||
// The `projectionDef` instruction only emitted for the component template and it is skipped for
|
// The `projectionDef` instruction is only emitted for the component template and
|
||||||
// nested templates (<ng-template> tags).
|
// is skipped for nested templates (<ng-template> tags).
|
||||||
if (this.level === 0 && this._hasNgContent) {
|
if (this.level === 0 && this._ngContentReservedSlots.length) {
|
||||||
const parameters: o.Expression[] = [];
|
const parameters: o.Expression[] = [];
|
||||||
|
|
||||||
// Only selectors with a non-default value are generated
|
// By default the `projectionDef` instructions creates one slot for the wildcard
|
||||||
if (this._ngContentSelectors.length) {
|
// selector if no parameters are passed. Therefore we only want to allocate a new
|
||||||
const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s));
|
// array for the projection slots if the default projection slot is not sufficient.
|
||||||
parameters.push(this.constantPool.getConstLiteral(asLiteral(r3Selectors), true));
|
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,
|
// Since we accumulate ngContent selectors while processing template elements,
|
||||||
|
@ -461,14 +460,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
visitContent(ngContent: t.Content) {
|
visitContent(ngContent: t.Content) {
|
||||||
this._hasNgContent = true;
|
|
||||||
const slot = this.allocateDataSlot();
|
const slot = this.allocateDataSlot();
|
||||||
let selectorIndex = ngContent.selector === DEFAULT_NG_CONTENT_SELECTOR ?
|
const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length;
|
||||||
0 :
|
|
||||||
this._ngContentSelectors.push(ngContent.selector) + this._ngContentSelectorsOffset;
|
|
||||||
const parameters: o.Expression[] = [o.literal(slot)];
|
const parameters: o.Expression[] = [o.literal(slot)];
|
||||||
const attributes: o.Expression[] = [];
|
const attributes: o.Expression[] = [];
|
||||||
|
|
||||||
|
this._ngContentReservedSlots.push(ngContent.selector);
|
||||||
|
|
||||||
ngContent.attributes.forEach((attribute) => {
|
ngContent.attributes.forEach((attribute) => {
|
||||||
const {name, value} = attribute;
|
const {name, value} = attribute;
|
||||||
if (name === NG_PROJECT_AS_ATTR_NAME) {
|
if (name === NG_PROJECT_AS_ATTR_NAME) {
|
||||||
|
@ -479,9 +477,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
});
|
});
|
||||||
|
|
||||||
if (attributes.length > 0) {
|
if (attributes.length > 0) {
|
||||||
parameters.push(o.literal(selectorIndex), o.literalArr(attributes));
|
parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes));
|
||||||
} else if (selectorIndex !== 0) {
|
} else if (projectionSlotIdx !== 0) {
|
||||||
parameters.push(o.literal(selectorIndex));
|
parameters.push(o.literal(projectionSlotIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters);
|
this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters);
|
||||||
|
@ -887,11 +885,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this._nestedTemplateFns.push(() => {
|
this._nestedTemplateFns.push(() => {
|
||||||
const templateFunctionExpr = templateVisitor.buildTemplateFunction(
|
const templateFunctionExpr = templateVisitor.buildTemplateFunction(
|
||||||
template.children, template.variables,
|
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));
|
this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null));
|
||||||
if (templateVisitor._hasNgContent) {
|
if (templateVisitor._ngContentReservedSlots.length) {
|
||||||
this._hasNgContent = true;
|
this._ngContentReservedSlots.push(...templateVisitor._ngContentReservedSlots);
|
||||||
this._ngContentSelectors.push(...templateVisitor._ngContentSelectors);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1011,8 +1008,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
getVarCount() { return this._pureFunctionSlots; }
|
getVarCount() { return this._pureFunctionSlots; }
|
||||||
|
|
||||||
getNgContentSelectors(): o.Expression|null {
|
getNgContentSelectors(): o.Expression|null {
|
||||||
return this._hasNgContent ?
|
return this._ngContentReservedSlots.length ?
|
||||||
this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true) :
|
this.constantPool.getConstLiteral(asLiteral(this._ngContentReservedSlots), true) :
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,10 +123,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||||
super();
|
super();
|
||||||
this.componentType = componentDef.type;
|
this.componentType = componentDef.type;
|
||||||
this.selector = componentDef.selectors[0][0] as string;
|
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 =
|
this.ngContentSelectors =
|
||||||
componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : [];
|
componentDef.ngContentSelectors ? componentDef.ngContentSelectors : [];
|
||||||
this.isBoundToModule = !!ngModule;
|
this.isBoundToModule = !!ngModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ export {
|
||||||
ɵɵtextInterpolateV,
|
ɵɵtextInterpolateV,
|
||||||
} from './instructions/all';
|
} from './instructions/all';
|
||||||
export {RenderFlags} from './interfaces/definition';
|
export {RenderFlags} from './interfaces/definition';
|
||||||
export {CssSelectorList} from './interfaces/projection';
|
export {CssSelectorList, ProjectionSlots} from './interfaces/projection';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ɵɵrestoreView,
|
ɵɵrestoreView,
|
||||||
|
|
|
@ -5,18 +5,47 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {assertDataInRange} from '../../util/assert';
|
|
||||||
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
|
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
|
||||||
import {CssSelectorList} from '../interfaces/projection';
|
import {ProjectionSlots} from '../interfaces/projection';
|
||||||
import {HEADER_OFFSET, TVIEW, T_HOST} from '../interfaces/view';
|
import {TVIEW, T_HOST} from '../interfaces/view';
|
||||||
import {appendProjectedNodes} from '../node_manipulation';
|
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 {getLView, setIsNotParent} from '../state';
|
||||||
import {findComponentView} from '../util/view_traversal_utils';
|
import {findComponentView} from '../util/view_traversal_utils';
|
||||||
|
|
||||||
import {getOrCreateTNode} from './shared';
|
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 <ng-content> occurrences in a given template.
|
* Instruction to distribute projectable nodes among <ng-content> 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
|
* - we can't have only a parsed as we can't re-construct textual form from it (as entered by a
|
||||||
* template author).
|
* template author).
|
||||||
*
|
*
|
||||||
* @param selectors A collection of parsed CSS selectors
|
* @param projectionSlots? A collection of projection slots. A projection slot can be based
|
||||||
* @param rawSelectors A collection of CSS selectors in the raw, un-parsed form
|
* 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
|
* @codeGenApi
|
||||||
*/
|
*/
|
||||||
export function ɵɵprojectionDef(selectors?: CssSelectorList[]): void {
|
export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
|
||||||
const componentNode = findComponentView(getLView())[T_HOST] as TElementNode;
|
const componentNode = findComponentView(getLView())[T_HOST] as TElementNode;
|
||||||
|
|
||||||
if (!componentNode.projection) {
|
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 =
|
const projectionHeads: (TNode | null)[] = componentNode.projection =
|
||||||
new Array(noOfNodeBuckets).fill(null);
|
new Array(numProjectionSlots).fill(null);
|
||||||
const tails: (TNode | null)[] = projectionHeads.slice();
|
const tails: (TNode | null)[] = projectionHeads.slice();
|
||||||
|
|
||||||
let componentChild: TNode|null = componentNode.child;
|
let componentChild: TNode|null = componentNode.child;
|
||||||
|
|
||||||
while (componentChild !== null) {
|
while (componentChild !== null) {
|
||||||
const bucketIndex =
|
const slotIndex =
|
||||||
selectors ? matchingProjectionSelectorIndex(componentChild, selectors) : 0;
|
projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;
|
||||||
|
|
||||||
if (tails[bucketIndex]) {
|
if (slotIndex !== null) {
|
||||||
tails[bucketIndex] !.projectionNext = componentChild;
|
if (tails[slotIndex]) {
|
||||||
} else {
|
tails[slotIndex] !.projectionNext = componentChild;
|
||||||
projectionHeads[bucketIndex] = componentChild;
|
} else {
|
||||||
|
projectionHeads[slotIndex] = componentChild;
|
||||||
|
}
|
||||||
|
tails[slotIndex] = componentChild;
|
||||||
}
|
}
|
||||||
tails[bucketIndex] = componentChild;
|
|
||||||
|
|
||||||
componentChild = componentChild.next;
|
componentChild = componentChild.next;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,16 @@ export type CssSelector = (string | SelectorFlags)[];
|
||||||
*/
|
*/
|
||||||
export type CssSelectorList = CssSelector[];
|
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 */
|
/** Flags used to build up CssSelectors */
|
||||||
export const enum SelectorFlags {
|
export const enum SelectorFlags {
|
||||||
/** Indicates this is the beginning of a new negative selector */
|
/** Indicates this is the beginning of a new negative selector */
|
||||||
|
|
|
@ -257,29 +257,6 @@ export function getProjectAsAttrValue(tNode: TNode): CssSelector|null {
|
||||||
return 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) {
|
function getNameOnlyMarkerIndex(nodeAttrs: TAttributes) {
|
||||||
for (let i = 0; i < nodeAttrs.length; i++) {
|
for (let i = 0; i < nodeAttrs.length; i++) {
|
||||||
const nodeAttr = nodeAttrs[i];
|
const nodeAttr = nodeAttrs[i];
|
||||||
|
@ -307,7 +284,7 @@ function matchTemplateAttribute(attrs: TAttributes, name: string): number {
|
||||||
* @param selector Selector to be checked.
|
* @param selector Selector to be checked.
|
||||||
* @param list List in which to look for the selector.
|
* @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++) {
|
selectorListLoop: for (let i = 0; i < list.length; i++) {
|
||||||
const currentSelectorInList = list[i];
|
const currentSelectorInList = list[i];
|
||||||
if (selector.length !== currentSelectorInList.length) {
|
if (selector.length !== currentSelectorInList.length) {
|
||||||
|
|
|
@ -963,18 +963,10 @@ describe('ViewContainerRef', () => {
|
||||||
[[myNode]]);
|
[[myNode]]);
|
||||||
fixture.detectChanges();
|
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.
|
expect(getElementHtml(fixture.nativeElement))
|
||||||
// View Engine correctly respects the passed index of "projectedNodes". See: FW-1331.
|
.toEqual(
|
||||||
if (ivyEnabled) {
|
'<p vcref=""></p><embedded-cmp-with-ngcontent><div>barbaz</div><hr></embedded-cmp-with-ngcontent>');
|
||||||
expect(getElementHtml(fixture.nativeElement))
|
|
||||||
.toEqual(
|
|
||||||
'<p vcref=""></p><embedded-cmp-with-ngcontent><hr><div>barbaz</div></embedded-cmp-with-ngcontent>');
|
|
||||||
} else {
|
|
||||||
expect(getElementHtml(fixture.nativeElement))
|
|
||||||
.toEqual(
|
|
||||||
'<p vcref=""></p><embedded-cmp-with-ngcontent><div>barbaz</div><hr></embedded-cmp-with-ngcontent>');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support reprojection of projectable nodes', () => {
|
it('should support reprojection of projectable nodes', () => {
|
||||||
|
@ -1039,17 +1031,9 @@ describe('ViewContainerRef', () => {
|
||||||
]);
|
]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
// With Ivy multi-slot projection is currently not working. This is a bug that
|
expect(getElementHtml(fixture.nativeElement))
|
||||||
// is tracked with FW-1333.
|
.toEqual(
|
||||||
if (ivyEnabled) {
|
'<p vcref=""></p><embedded-cmp-with-ngcontent>12<hr>34</embedded-cmp-with-ngcontent>');
|
||||||
expect(getElementHtml(fixture.nativeElement))
|
|
||||||
.toEqual(
|
|
||||||
'<p vcref=""></p><embedded-cmp-with-ngcontent><hr>12</embedded-cmp-with-ngcontent>');
|
|
||||||
} else {
|
|
||||||
expect(getElementHtml(fixture.nativeElement))
|
|
||||||
.toEqual(
|
|
||||||
'<p vcref=""></p><embedded-cmp-with-ngcontent>12<hr>34</embedded-cmp-with-ngcontent>');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||||
|
@ -111,13 +112,48 @@ describe('projection', () => {
|
||||||
expect(main.nativeElement).toHaveText('(A, BC)');
|
expect(main.nativeElement).toHaveText('(A, BC)');
|
||||||
});
|
});
|
||||||
|
|
||||||
modifiedInIvy(
|
it('should support passing projectable nodes via factory function', () => {
|
||||||
'FW-886: `projectableNodes` passed to a componentFactory should be in the order of declaration')
|
@Component({
|
||||||
.it('should support passing projectable nodes via factory function', () => {
|
selector: 'multiple-content-tags',
|
||||||
|
template: '(<ng-content SELECT="h1"></ng-content>, <ng-content></ng-content>)',
|
||||||
|
})
|
||||||
|
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({
|
@Component({
|
||||||
selector: 'multiple-content-tags',
|
selector: 'multiple-content-tags',
|
||||||
template: '(<ng-content SELECT="h1"></ng-content>, <ng-content></ng-content>)',
|
template: `
|
||||||
|
1<ng-content select="h1"></ng-content>
|
||||||
|
2<ng-template [ngIf]="true"><ng-content></ng-content></ng-template>
|
||||||
|
3<ng-content select="h2"></ng-content>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
class MultipleContentTagsComponent {
|
class MultipleContentTagsComponent {
|
||||||
}
|
}
|
||||||
|
@ -125,6 +161,7 @@ describe('projection', () => {
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [MultipleContentTagsComponent],
|
declarations: [MultipleContentTagsComponent],
|
||||||
entryComponents: [MultipleContentTagsComponent],
|
entryComponents: [MultipleContentTagsComponent],
|
||||||
|
imports: [CommonModule],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
})
|
})
|
||||||
class MyModule {
|
class MyModule {
|
||||||
|
@ -137,12 +174,14 @@ describe('projection', () => {
|
||||||
injector.get(ComponentFactoryResolver);
|
injector.get(ComponentFactoryResolver);
|
||||||
const componentFactory =
|
const componentFactory =
|
||||||
componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent);
|
componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent);
|
||||||
expect(componentFactory.ngContentSelectors).toEqual(['h1', '*']);
|
expect(componentFactory.ngContentSelectors).toEqual(['h1', '*', 'h2']);
|
||||||
|
|
||||||
const nodeOne = getDOM().createTextNode('one');
|
const nodeOne = getDOM().createTextNode('one');
|
||||||
const nodeTwo = getDOM().createTextNode('two');
|
const nodeTwo = getDOM().createTextNode('two');
|
||||||
const component = componentFactory.create(injector, [[nodeOne], [nodeTwo]]);
|
const nodeThree = getDOM().createTextNode('three');
|
||||||
expect(component.location.nativeElement).toHaveText('(one, two)');
|
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', () => {
|
it('should redistribute only direct children', () => {
|
||||||
|
|
|
@ -48,7 +48,7 @@ describe('ComponentFactory', () => {
|
||||||
consts: 0,
|
consts: 0,
|
||||||
vars: 0,
|
vars: 0,
|
||||||
template: () => undefined,
|
template: () => undefined,
|
||||||
ngContentSelectors: ['a', 'b'],
|
ngContentSelectors: ['*', 'a', 'b'],
|
||||||
factory: () => new TestComponent(),
|
factory: () => new TestComponent(),
|
||||||
inputs: {
|
inputs: {
|
||||||
in1: 'in1',
|
in1: 'in1',
|
||||||
|
|
|
@ -913,7 +913,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]);
|
ɵɵprojectionDef(['*', [['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
{ ɵɵprojection(1, 1); }
|
{ ɵɵprojection(1, 1); }
|
||||||
ɵɵelementEnd();
|
ɵɵelementEnd();
|
||||||
|
@ -958,7 +958,7 @@ describe('content projection', () => {
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([
|
ɵɵprojectionDef([
|
||||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
'*', [['span', SelectorFlags.CLASS, 'toFirst']],
|
||||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||||
]);
|
]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
|
@ -1005,7 +1005,7 @@ describe('content projection', () => {
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([
|
ɵɵprojectionDef([
|
||||||
[['span', SelectorFlags.CLASS, 'toFirst']],
|
'*', [['span', SelectorFlags.CLASS, 'toFirst']],
|
||||||
[['span', SelectorFlags.CLASS, 'toSecond']]
|
[['span', SelectorFlags.CLASS, 'toSecond']]
|
||||||
]);
|
]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
|
@ -1051,7 +1051,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]);
|
ɵɵprojectionDef(['*', [['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
{ ɵɵprojection(1, 1); }
|
{ ɵɵprojection(1, 1); }
|
||||||
ɵɵelementEnd();
|
ɵɵelementEnd();
|
||||||
|
@ -1095,7 +1095,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]]);
|
ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toFirst']]]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
{ ɵɵprojection(1, 1); }
|
{ ɵɵprojection(1, 1); }
|
||||||
ɵɵelementEnd();
|
ɵɵelementEnd();
|
||||||
|
@ -1140,7 +1140,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]]);
|
ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toSecond']]]);
|
||||||
ɵɵelementStart(0, 'div', ['id', 'first']);
|
ɵɵelementStart(0, 'div', ['id', 'first']);
|
||||||
{ ɵɵprojection(1); }
|
{ ɵɵprojection(1); }
|
||||||
ɵɵelementEnd();
|
ɵɵelementEnd();
|
||||||
|
@ -1192,7 +1192,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) {
|
const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['span']]]);
|
ɵɵprojectionDef(['*', [['span']]]);
|
||||||
ɵɵprojection(0, 1);
|
ɵɵprojection(0, 1);
|
||||||
ɵɵelement(1, 'hr');
|
ɵɵelement(1, 'hr');
|
||||||
ɵɵprojection(2);
|
ɵɵprojection(2);
|
||||||
|
@ -1253,7 +1253,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Card = createComponent('card', function(rf: RenderFlags, ctx: any) {
|
const Card = createComponent('card', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['', 'card-title', '']], [['', 'card-content', '']]]);
|
ɵɵprojectionDef(['*', [['', 'card-title', '']], [['', 'card-content', '']]]);
|
||||||
ɵɵprojection(0, 1);
|
ɵɵprojection(0, 1);
|
||||||
ɵɵelement(1, 'hr');
|
ɵɵelement(1, 'hr');
|
||||||
ɵɵprojection(2, 2);
|
ɵɵprojection(2, 2);
|
||||||
|
@ -1306,7 +1306,7 @@ describe('content projection', () => {
|
||||||
*/
|
*/
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
ɵɵprojectionDef([[['div']]]);
|
ɵɵprojectionDef(['*', [['div']]]);
|
||||||
ɵɵprojection(0, 1);
|
ɵɵprojection(0, 1);
|
||||||
}
|
}
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
|
@ -940,7 +940,7 @@ export declare type ɵɵPipeDefWithMeta<T, Name extends string> = PipeDef<T>;
|
||||||
|
|
||||||
export declare function ɵɵprojection(nodeIndex: number, selectorIndex?: number, attrs?: TAttributes): void;
|
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<T>(propName: string, value: T, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009;
|
export declare function ɵɵproperty<T>(propName: string, value: T, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue