feat(ivy): support ng-content projection in the ivy compiler (#21764)
PR Close #21764
This commit is contained in:
parent
72265f796f
commit
18174e5564
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue