Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
120 changes: 111 additions & 9 deletions packages/compiler/src/render3/r3_view_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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});

Expand All @@ -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`);
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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] : [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document the format on the type ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reference to the documentation in core.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's more about the class part [..., class, ...classNames]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That is documented as part of the CssSelector in the referenced file.

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);
}
76 changes: 76 additions & 0 deletions packages/compiler/test/render3/r3_view_compiler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down