feat(ivy): support ng-content projection in the ivy compiler (#21764)

PR Close #21764
This commit is contained in:
Chuck Jazdzewski 2018-01-26 17:12:39 -08:00 committed by Jason Aden
parent 72265f796f
commit 18174e5564
3 changed files with 190 additions and 9 deletions

View File

@ -66,6 +66,9 @@ export class Identifiers {
static memory: o.ExternalReference = {name: 'ɵm', moduleName: CORE};
static projection: o.ExternalReference = {name: 'ɵP', moduleName: CORE};
static projectionDef: o.ExternalReference = {name: 'ɵpD', moduleName: CORE};
static refreshComponent: o.ExternalReference = {name: 'ɵr', moduleName: CORE};
static directiveLifeCycle: o.ExternalReference = {name: 'ɵl', moduleName: CORE};

View File

@ -15,11 +15,13 @@ import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast';
import {ParseSourceSpan} from '../parse_util';
import {CssSelector} from '../selector';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {OutputContext, error} from '../util';
import {Identifiers as R3} from './r3_identifiers';
/** Name of the context parameter passed into a template function */
const CONTEXT_NAME = 'ctx';
@ -108,7 +110,7 @@ export function compileComponent(
const templateFunctionExpression =
new TemplateDefinitionBuilder(
outputCtx, outputCtx.constantPool, reflector, CONTEXT_NAME, ROOT_SCOPE.nestedScope(), 0,
templateTypeName, templateName)
component.template !.ngContentSelectors, templateTypeName, templateName)
.buildTemplateFunction(template, []);
definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false});
@ -134,8 +136,10 @@ export function compileComponent(
// TODO: Remove these when the things are fully supported
function unknown<T>(arg: o.Expression | o.Statement | TemplateAst): never {
throw new Error(`Builder ${this.constructor.name} is unable to handle ${o.constructor.name} yet`);
throw new Error(
`Builder ${this.constructor.name} is unable to handle ${arg.constructor.name} yet`);
}
function unsupported(feature: string): never {
if (this) {
throw new Error(`Builder ${this.constructor.name} doesn't support ${feature} yet`);
@ -225,14 +229,16 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
private _hostMode: o.Statement[] = [];
private _refreshMode: o.Statement[] = [];
private _postfix: o.Statement[] = [];
private _contentProjections: Map<NgContentAst, NgContentInfo>;
private _projectionDefinitionIndex = 0;
private unsupported = unsupported;
private invalid = invalid;
constructor(
private outputCtx: OutputContext, private constantPool: ConstantPool,
private reflector: CompileReflector, private contextParameter: string,
private bindingScope: BindingScope, private level = 0, private contextName: string|null,
private templateName: string|null) {}
private bindingScope: BindingScope, private level = 0, private ngContentSelectors: string[],
private contextName: string|null, private templateName: string|null) {}
buildTemplateFunction(asts: TemplateAst[], variables: VariableAst[]): o.FunctionExpr {
// Create variable bindings
@ -252,6 +258,28 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
this._bindingMode.push(declaration);
}
// Collect content projections
if (this.ngContentSelectors && this.ngContentSelectors.length > 0) {
const contentProjections = getContentProjection(asts, this.ngContentSelectors);
this._contentProjections = contentProjections;
if (contentProjections.size > 0) {
const infos: R3CssSelector[] = [];
Array.from(contentProjections.values()).forEach(info => {
if (info.selector) {
infos[info.index - 1] = info.selector;
}
});
const projectionIndex = this._projectionDefinitionIndex = this.allocateDataSlot();
const parameters: o.Expression[] = [o.literal(projectionIndex)];
!infos.some(value => !value) || error(`content project information skipped an index`);
if (infos.length > 1) {
parameters.push(this.outputCtx.constantPool.getConstLiteral(
asLiteral(infos), /* forceShared */ true));
}
this.instruction(this._creationMode, null, R3.projectionDef, ...parameters);
}
}
templateVisitAll(this, asts);
return o.fn(
@ -282,8 +310,16 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
getLocal(name: string): o.Expression|null { return this.bindingScope.get(name); }
// TODO(chuckj): Implement ng-content
visitNgContent = unknown;
visitNgContent(ast: NgContentAst) {
const info = this._contentProjections.get(ast) !;
info || error(`Expected ${ast.sourceSpan} to be included in content projection collection`);
const slot = this.allocateDataSlot();
const parameters = [o.literal(slot), o.literal(this._projectionDefinitionIndex)];
if (info.index !== 0) {
parameters.push(o.literal(info.index));
}
this.instruction(this._creationMode, ast.sourceSpan, R3.projection, ...parameters);
}
private _computeDirectivesArray(directives: DirectiveAst[]) {
const directiveIndexMap = new Map<any, number>();
@ -473,7 +509,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
// Create the template function
const templateVisitor = new TemplateDefinitionBuilder(
this.outputCtx, this.constantPool, this.reflector, templateContext,
this.bindingScope.nestedScope(), this.level + 1, contextName, templateName);
this.bindingScope.nestedScope(), this.level + 1, this.ngContentSelectors, contextName,
templateName);
const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children, ast.variables);
this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null));
}
@ -512,7 +549,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
private bindingContext() { return `${this._bindingContext++}`; }
private instruction(
statements: o.Statement[], span: ParseSourceSpan, reference: o.ExternalReference,
statements: o.Statement[], span: ParseSourceSpan|null, reference: o.ExternalReference,
...params: o.Expression[]) {
statements.push(o.importExpr(reference, null, span).callFn(params, span).toStmt());
}
@ -594,3 +631,68 @@ function invalid<T>(arg: o.Expression | o.Statement | TemplateAst): never {
function findComponent(directives: DirectiveAst[]): DirectiveAst|undefined {
return directives.filter(directive => directive.directive.isComponent)[0];
}
interface NgContentInfo {
index: number;
selector?: R3CssSelector;
}
class ContentProjectionVisitor extends RecursiveTemplateAstVisitor {
private index = 1;
constructor(
private projectionMap: Map<NgContentAst, NgContentInfo>,
private ngContentSelectors: string[]) {
super();
}
visitNgContent(ast: NgContentAst) {
const selectorText = this.ngContentSelectors[ast.index];
selectorText != null || error(`could not find selector for index ${ast.index} in ${ast}`);
if (!selectorText || selectorText === '*') {
this.projectionMap.set(ast, {index: 0});
} else {
const cssSelectors = CssSelector.parse(selectorText);
this.projectionMap.set(
ast, {index: this.index++, selector: parseSelectorsToR3Selector(cssSelectors)});
}
}
}
function getContentProjection(asts: TemplateAst[], ngContentSelectors: string[]) {
const projectIndexMap = new Map<NgContentAst, NgContentInfo>();
const visitor = new ContentProjectionVisitor(projectIndexMap, ngContentSelectors);
templateVisitAll(visitor, asts);
return projectIndexMap;
}
// These are a copy the CSS types from core/src/render3/interfaces/projection.ts
// They are duplicated here as they cannot be directly referenced from core.
type R3SimpleCssSelector = (string | null)[];
type R3CssSelectorWithNegations =
[R3SimpleCssSelector, null] | [R3SimpleCssSelector, R3SimpleCssSelector];
type R3CssSelector = R3CssSelectorWithNegations[];
function parserSelectorToSimpleSelector(selector: CssSelector): R3SimpleCssSelector {
const classes =
selector.classNames && selector.classNames.length ? ['class', ...selector.classNames] : [];
return [selector.element, ...selector.attrs, ...classes];
}
function parserSelectorToR3Selector(selector: CssSelector): R3CssSelectorWithNegations {
const positive = parserSelectorToSimpleSelector(selector);
const negative = selector.notSelectors && selector.notSelectors.length &&
parserSelectorToSimpleSelector(selector.notSelectors[0]);
return negative ? [positive, negative] : [positive, null];
}
function parseSelectorsToR3Selector(selectors: CssSelector[]): R3CssSelector {
return selectors.map(parserSelectorToR3Selector);
}
function asLiteral(value: any): o.Expression {
if (Array.isArray(value)) {
return o.literalArr(value.map(asLiteral));
}
return o.literal(value, o.INFERRED_TYPE);
}

View File

@ -297,6 +297,82 @@ describe('r3_view_compiler', () => {
expectEmit(source, directives, 'Incorrect shared directive constant');
});
it('should support content projection', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule, TemplateRef} from '@angular/core';
@Component({selector: 'simple', template: '<div><ng-content></ng-content></div>'})
export class SimpleComponent {}
@Component({
selector: 'complex',
template: \`
<div id="first"><ng-content select="span[title=toFirst]"></ng-content></div>
<div id="second"><ng-content select="span[title=toSecond]"></ng-content></div>\`
})
export class ComplexComponent { }
@NgModule({declarations: [SimpleComponent, ComplexComponent]})
export class MyModule {}
@Component({
selector: 'my-app',
template: '<simple>content</simple> <complex></complex>'
})
export class MyApp {}
`
}
};
const SimpleComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
type: SimpleComponent,
tag: 'simple',
factory: function SimpleComponent_Factory() { return new SimpleComponent(); },
template: function SimpleComponent_Template(ctx: IDENT, cm: IDENT) {
if (cm) {
IDENT.ɵpD(0);
IDENT.ɵE(1, 'div');
IDENT.ɵP(2, 0);
IDENT.ɵe();
}
}
});`;
const ComplexComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
type: ComplexComponent,
tag: 'complex',
factory: function ComplexComponent_Factory() { return new ComplexComponent(); },
template: function ComplexComponent_Template(ctx: IDENT, cm: IDENT) {
if (cm) {
IDENT.ɵpD(0, IDENT);
IDENT.ɵE(1, 'div', IDENT);
IDENT.ɵP(2, 0, 1);
IDENT.ɵe();
IDENT.ɵE(3, 'div', IDENT);
IDENT.ɵP(4, 0, 2);
IDENT.ɵe();
}
}
});
`;
const ComplexComponent_ProjectionConst = `
const IDENT = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]];
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition');
expectEmit(
result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition');
expectEmit(result.source, ComplexComponent_ProjectionConst, 'Incorrect projection const');
});
it('local reference', () => {
const files = {
app: {