fix(ivy): walk declaration views in listener (#25228)

PR Close #25228
This commit is contained in:
Kara Erickson 2018-07-30 19:43:56 -07:00 committed by Igor Minar
parent 64516da6b0
commit c8a4fb1faf
13 changed files with 323 additions and 107 deletions

View File

@ -58,4 +58,121 @@ describe('compiler compliance: listen()', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should create multiple listener instructions that share a view snapshot', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'my-component',
template: \`
<div *ngIf="showing">
<div (click)="onClick(foo)"></div>
<button (click)="onClick2(bar)"></button>
</div>
\`
})
export class MyComponent {
onClick(name: any) {}
onClick2(name: any) {}
}
@NgModule({declarations: [MyComponent], imports: [CommonModule]})
export class MyModule {}
`
}
};
const template = `
const $c0$ = ["ngIf",""];
function MyComponent_div_Template_0(rf, ctx) {
if (rf & 1) {
const $s$ = $r3$.ɵgV();
$r3$.ɵE(0, "div");
$r3$.ɵE(1, "div");
$r3$.ɵL("click", function MyComponent_div_Template_0_div_click_listener($event) {
$r3$.ɵrV($s$);
const $comp$ = $r3$.ɵx();
return $comp$.onClick($comp$.foo);
});
$r3$.ɵe();
$r3$.ɵE(2, "button");
$r3$.ɵL("click", function MyComponent_div_Template_0_button_click_listener($event) {
$r3$.ɵrV($s$);
const $comp2$ = $r3$.ɵx();
return $comp2$.onClick2($comp2$.bar);
});
$r3$.ɵe();
$r3$.ɵe();
}
}
// ...
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵC(0, MyComponent_div_Template_0, null, $c0$);
}
if (rf & 2) {
$i0$.ɵp(0, "ngIf", $i0$.ɵb(ctx.showing));
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('local refs in listeners defined before the local refs', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<button (click)="onClick(user.value)">Save</button>
<input #user>
\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const MyComponentDefinition = `
const $c0$ = ["user", ""];
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
selectors: [["my-component"]],
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵE(0, "button");
$r3$.ɵL("click", function MyComponent_Template_button_click_listener($event) {
const $user$ = $r3$.ɵr(3);
return ctx.onClick($user$.value);
});
$r3$.ɵT(1, "Save");
$r3$.ɵe();
$r3$.ɵEe(2, "input", null, $c0$);
}
}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
});
}); });

View File

@ -55,12 +55,14 @@ describe('compiler compliance: template', () => {
function MyComponent_ul_li_div_Template_1(rf, ctx) { function MyComponent_ul_li_div_Template_1(rf, ctx) {
if (rf & 1) { if (rf & 1) {
const $s$ = $i0$.ɵgV();
$i0$.ɵE(0, "div");
$i0$.ɵL("click", function MyComponent_ul_li_div_Template_1_div_click_listener($event){
$i0$.ɵrV($s$);
const $inner$ = ctx.$implicit; const $inner$ = ctx.$implicit;
const $middle$ = $i0$.ɵx().$implicit; const $middle$ = $i0$.ɵx().$implicit;
const $outer$ = $i0$.ɵx().$implicit; const $outer$ = $i0$.ɵx().$implicit;
const $myComp$ = $i0$.ɵx(); const $myComp$ = $i0$.ɵx();
$i0$.ɵE(0, "div");
$i0$.ɵL("click", function MyComponent_ul_li_div_Template_1_div_click_listener($event){
return $myComp$.onClick($outer$, $middle$, $inner$); return $myComp$.onClick($outer$, $middle$, $inner$);
}); });
$i0$.ɵT(1); $i0$.ɵT(1);

View File

@ -53,6 +53,10 @@ export class Identifiers {
static bind: o.ExternalReference = {name: 'ɵb', moduleName: CORE}; static bind: o.ExternalReference = {name: 'ɵb', moduleName: CORE};
static getCurrentView: o.ExternalReference = {name: 'ɵgV', moduleName: CORE};
static restoreView: o.ExternalReference = {name: 'ɵrV', moduleName: CORE};
static interpolation1: o.ExternalReference = {name: 'ɵi1', moduleName: CORE}; static interpolation1: o.ExternalReference = {name: 'ɵi1', moduleName: CORE};
static interpolation2: o.ExternalReference = {name: 'ɵi2', moduleName: CORE}; static interpolation2: o.ExternalReference = {name: 'ɵi2', moduleName: CORE};
static interpolation3: o.ExternalReference = {name: 'ɵi3', moduleName: CORE}; static interpolation3: o.ExternalReference = {name: 'ɵi3', moduleName: CORE};

View File

@ -56,7 +56,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private _dataIndex = 0; private _dataIndex = 0;
private _bindingContext = 0; private _bindingContext = 0;
private _prefixCode: o.Statement[] = []; private _prefixCode: o.Statement[] = [];
private _creationCode: o.Statement[] = []; /**
* List of callbacks to generate creation mode instructions. We store them here as we process
* the template so bindings in listeners are resolved only once all nodes have been visited.
* This ensures all local refs and context variables are available for matching.
*/
private _creationCodeFns: (() => o.Statement)[] = [];
/** /**
* List of callbacks to generate update mode instructions. We store them here as we process * List of callbacks to generate update mode instructions. We store them here as we process
* the template so bindings are resolved only once all nodes have been visited. This ensures * the template so bindings are resolved only once all nodes have been visited. This ensures
@ -76,12 +81,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
* This scope contains local variables declared in the update mode block of the template. * This scope contains local variables declared in the update mode block of the template.
* (e.g. refs and context vars in bindings) * (e.g. refs and context vars in bindings)
*/ */
private _updateScope: BindingScope; private _bindingScope: BindingScope;
/**
* This scope contains local variables declared in the creation mode block of the template
* (e.g. refs and context vars in listeners)
*/
private _creationScope: BindingScope;
private _valueConverter: ValueConverter; private _valueConverter: ValueConverter;
private _unsupported = unsupported; private _unsupported = unsupported;
@ -104,9 +104,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// function) // function)
this._dataIndex = viewQueries.length; this._dataIndex = viewQueries.length;
// TODO(kara): generate restore instruction in listener to replace creation scope this._bindingScope = parentBindingScope.nestedScope(level);
this._creationScope = parentBindingScope.nestedScope(level);
this._updateScope = parentBindingScope.nestedScope(level);
this._valueConverter = new ValueConverter( this._valueConverter = new ValueConverter(
constantPool, () => this.allocateDataSlot(), constantPool, () => this.allocateDataSlot(),
@ -116,17 +114,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (pipeType) { if (pipeType) {
this.pipes.add(pipeType); this.pipes.add(pipeType);
} }
this._updateScope.set(this.level, localName, value); this._bindingScope.set(this.level, localName, value);
this._creationCode.push( this.creationInstruction(null, R3.pipe, [o.literal(slot), o.literal(name)]);
o.importExpr(R3.pipe).callFn([o.literal(slot), o.literal(name)]).toStmt());
}); });
} }
registerContextVariables(variable: t.Variable, retrievalScope: BindingScope) { registerContextVariables(variable: t.Variable) {
const scopedName = retrievalScope.freshReferenceName(); const scopedName = this._bindingScope.freshReferenceName();
const retrievalLevel = this.level; const retrievalLevel = this.level;
const lhs = o.variable(variable.name + scopedName); const lhs = o.variable(variable.name + scopedName);
retrievalScope.set( this._bindingScope.set(
retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT, retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT,
(scope: BindingScope, relativeLevel: number) => { (scope: BindingScope, relativeLevel: number) => {
let rhs: o.Expression; let rhs: o.Expression;
@ -151,11 +148,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
// Create variable bindings // Create variable bindings
for (const variable of variables) { variables.forEach(v => this.registerContextVariables(v));
// Add the reference to the local scope.
this.registerContextVariables(variable, this._creationScope);
this.registerContextVariables(variable, this._updateScope);
}
// Output a `ProjectionDef` instruction when some `<ng-content>` are present // Output a `ProjectionDef` instruction when some `<ng-content>` are present
if (hasNgContent) { if (hasNgContent) {
@ -170,34 +163,38 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
parameters.push(parsed, unParsed); parameters.push(parsed, unParsed);
} }
this.creationInstruction(null, R3.projectionDef, ...parameters); this.creationInstruction(null, R3.projectionDef, parameters);
} }
// This is the initial pass through the nodes of this template. In this pass, we // This is the initial pass through the nodes of this template. In this pass, we
// generate all creation mode instructions & queue all update mode instructions for // queue all creation mode and update mode instructions for generation in the second
// generation in the second pass. It's necessary to separate the passes to ensure // pass. It's necessary to separate the passes to ensure local refs are defined before
// local refs are defined before resolving bindings. // resolving bindings.
t.visitAll(this, nodes); t.visitAll(this, nodes);
// Generate all the update mode instructions as the second pass (e.g. resolve bindings) // Generate all the creation mode instructions (e.g. resolve bindings in listeners)
const creationStatements = this._creationCodeFns.map((fn: () => o.Statement) => fn());
// Generate all the update mode instructions (e.g. resolve property or text bindings)
const updateStatements = this._updateCodeFns.map((fn: () => o.Statement) => fn()); const updateStatements = this._updateCodeFns.map((fn: () => o.Statement) => fn());
// To count slots for the reserveSlots() instruction, all bindings must have been visited. // To count slots for the reserveSlots() instruction, all bindings must have been visited.
if (this._pureFunctionSlots > 0) { if (this._pureFunctionSlots > 0) {
this.creationInstruction(null, R3.reserveSlots, o.literal(this._pureFunctionSlots)); creationStatements.push(
instruction(null, R3.reserveSlots, [o.literal(this._pureFunctionSlots)]).toStmt());
} }
const creationCode = this._creationCode.length > 0 ? // Variable declaration must occur after binding resolution so we can generate context
// instructions that build on each other. e.g. const b = x().$implicit(); const b = x();
const creationVariables = this._bindingScope.viewSnapshotStatements();
const updateVariables = this._bindingScope.variableDeclarations().concat(this._tempVariables);
const creationBlock = creationStatements.length > 0 ?
[renderFlagCheckIfStmt( [renderFlagCheckIfStmt(
core.RenderFlags.Create, core.RenderFlags.Create, creationVariables.concat(creationStatements))] :
this._creationScope.variableDeclarations().concat(this._creationCode))] :
[]; [];
// This must occur after binding resolution so we can generate context instructions that const updateBlock = updateStatements.length > 0 ?
// build on each other. e.g. const row = x().$implicit; const table = x().$implicit();
const updateVariables = this._updateScope.variableDeclarations().concat(this._tempVariables);
const updateCode = this._updateCodeFns.length > 0 ?
[renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] : [renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] :
[]; [];
@ -205,7 +202,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// TODO(vicb): This is a WIP, not fully supported yet // TODO(vicb): This is a WIP, not fully supported yet
for (const phToNodeIdx of this._phToNodeIdxes) { for (const phToNodeIdx of this._phToNodeIdxes) {
if (Object.keys(phToNodeIdx).length > 0) { if (Object.keys(phToNodeIdx).length > 0) {
const scopedName = this._updateScope.freshReferenceName(); const scopedName = this._bindingScope.freshReferenceName();
const phMap = o.variable(scopedName).set(mapToExpression(phToNodeIdx, true)).toConstDecl(); const phMap = o.variable(scopedName).set(mapToExpression(phToNodeIdx, true)).toConstDecl();
this._prefixCode.push(phMap); this._prefixCode.push(phMap);
@ -221,15 +218,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Temporary variable declarations for query refresh (i.e. let _t: any;) // Temporary variable declarations for query refresh (i.e. let _t: any;)
...this._prefixCode, ...this._prefixCode,
// Creating mode (i.e. if (rf & RenderFlags.Create) { ... }) // Creating mode (i.e. if (rf & RenderFlags.Create) { ... })
...creationCode, ...creationBlock,
// Binding and refresh mode (i.e. if (rf & RenderFlags.Update) {...}) // Binding and refresh mode (i.e. if (rf & RenderFlags.Update) {...})
...updateCode, ...updateBlock,
], ],
o.INFERRED_TYPE, null, this.templateName); o.INFERRED_TYPE, null, this.templateName);
} }
// LocalResolver // LocalResolver
getLocal(name: string): o.Expression|null { return this._updateScope.get(name); } getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
visitContent(ngContent: t.Content) { visitContent(ngContent: t.Content) {
const slot = this.allocateDataSlot(); const slot = this.allocateDataSlot();
@ -251,7 +248,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
parameters.push(o.literal(selectorIndex)); parameters.push(o.literal(selectorIndex));
} }
this.creationInstruction(ngContent.sourceSpan, R3.projection, ...parameters); this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters);
} }
@ -325,7 +322,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
]; ];
// Add the attributes // Add the attributes
const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = []; const attributes: o.Expression[] = [];
const initialStyleDeclarations: o.Expression[] = []; const initialStyleDeclarations: o.Expression[] = [];
const initialClassDeclarations: o.Expression[] = []; const initialClassDeclarations: o.Expression[] = [];
@ -460,10 +456,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const references = flatten(element.references.map(reference => { const references = flatten(element.references.map(reference => {
const slot = this.allocateDataSlot(); const slot = this.allocateDataSlot();
// Generate the update temporary. // Generate the update temporary.
const variableName = this._updateScope.freshReferenceName(); const variableName = this._bindingScope.freshReferenceName();
const retrievalLevel = this.level; const retrievalLevel = this.level;
const lhs = o.variable(variableName); const lhs = o.variable(variableName);
this._updateScope.set( this._bindingScope.set(
retrievalLevel, reference.name, lhs, DeclarationPriority.DEFAULT, retrievalLevel, reference.name, lhs, DeclarationPriority.DEFAULT,
(scope: BindingScope, relativeLevel: number) => { (scope: BindingScope, relativeLevel: number) => {
// e.g. x(2); // e.g. x(2);
@ -481,11 +477,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
parameters.push(o.TYPED_NULL_EXPR); parameters.push(o.TYPED_NULL_EXPR);
} }
// Generate the instruction create element instruction
if (i18nMessages.length > 0) {
this._creationCode.push(...i18nMessages);
}
const wasInNamespace = this._namespace; const wasInNamespace = this._namespace;
const currentNamespace = this.getNamespaceInstruction(namespaceKey); const currentNamespace = this.getNamespaceInstruction(namespaceKey);
@ -501,14 +492,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
!hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0; !hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0;
if (createSelfClosingInstruction) { if (createSelfClosingInstruction) {
this.creationInstruction(element.sourceSpan, R3.element, ...trimTrailingNulls(parameters)); this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
} else { } else {
// Generate the instruction create element instruction this.creationInstruction(element.sourceSpan, R3.elementStart, trimTrailingNulls(parameters));
if (i18nMessages.length > 0) {
this._creationCode.push(...i18nMessages);
}
this.creationInstruction(
element.sourceSpan, R3.elementStart, ...trimTrailingNulls(parameters));
// initial styling for static style="..." attributes // initial styling for static style="..." attributes
if (hasStylingInstructions) { if (hasStylingInstructions) {
@ -538,12 +524,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
paramsList.push(o.NULL_EXPR); paramsList.push(o.NULL_EXPR);
} }
if (useDefaultStyleSanitizer) { if (useDefaultStyleSanitizer) {
paramsList.push(o.importExpr(R3.defaultStyleSanitizer)); paramsList.push(o.importExpr(R3.defaultStyleSanitizer));
} }
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt()); this.creationInstruction(null, R3.elementStyling, paramsList);
} }
// Generate Listeners (outputs) // Generate Listeners (outputs)
@ -551,14 +536,25 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const elName = sanitizeIdentifier(element.name); const elName = sanitizeIdentifier(element.name);
const evName = sanitizeIdentifier(outputAst.name); const evName = sanitizeIdentifier(outputAst.name);
const functionName = `${this.templateName}_${elName}_${evName}_listener`; const functionName = `${this.templateName}_${elName}_${evName}_listener`;
this.creationInstruction(outputAst.sourceSpan, R3.listener, () => {
const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
const bindingExpr = convertActionBinding( const bindingExpr = convertActionBinding(
this._creationScope, implicit, outputAst.handler, 'b', listenerScope, implicit, outputAst.handler, 'b',
() => error('Unexpected interpolation')); () => error('Unexpected interpolation'));
const statements = [
...listenerScope.restoreViewStatement(), ...listenerScope.variableDeclarations(),
...bindingExpr.render3Stmts
];
const handler = o.fn( const handler = o.fn(
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts], [new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
o.INFERRED_TYPE, null, functionName); functionName);
this.creationInstruction(
outputAst.sourceSpan, R3.listener, o.literal(outputAst.name), handler); return [o.literal(outputAst.name), handler];
});
}); });
} }
@ -636,8 +632,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
lastInputCommand = classInputs[classInputs.length - 1]; lastInputCommand = classInputs[classInputs.length - 1];
} }
this.updateInstruction( this.updateInstruction(lastInputCommand !.sourceSpan, R3.elementStylingApply, [indexLiteral]);
lastInputCommand !.sourceSpan, R3.elementStylingApply, () => [indexLiteral]);
} }
// Generate element input bindings // Generate element input bindings
@ -725,7 +720,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// e.g. C(1, C1Template) // e.g. C(1, C1Template)
this.creationInstruction( this.creationInstruction(
template.sourceSpan, R3.containerCreate, ...trimTrailingNulls(parameters)); template.sourceSpan, R3.containerCreate, trimTrailingNulls(parameters));
// e.g. p(1, 'forOf', ɵb(ctx.items)); // e.g. p(1, 'forOf', ɵb(ctx.items));
const context = o.variable(CONTEXT_NAME); const context = o.variable(CONTEXT_NAME);
@ -741,7 +736,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Create the template function // Create the template function
const templateVisitor = new TemplateDefinitionBuilder( const templateVisitor = new TemplateDefinitionBuilder(
this.constantPool, this._updateScope, this.level + 1, contextName, templateName, [], this.constantPool, this._bindingScope, this.level + 1, contextName, templateName, [],
this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace); this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace);
// Nested templates must not be visited until after their parent templates have completed // Nested templates must not be visited until after their parent templates have completed
@ -765,7 +760,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
visitBoundText(text: t.BoundText) { visitBoundText(text: t.BoundText) {
const nodeIndex = this.allocateDataSlot(); const nodeIndex = this.allocateDataSlot();
this.creationInstruction(text.sourceSpan, R3.text, o.literal(nodeIndex)); this.creationInstruction(text.sourceSpan, R3.text, [o.literal(nodeIndex)]);
const value = text.value.visit(this._valueConverter); const value = text.value.visit(this._valueConverter);
this.updateInstruction( this.updateInstruction(
@ -775,7 +770,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
visitText(text: t.Text) { visitText(text: t.Text) {
this.creationInstruction( this.creationInstruction(
text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), o.literal(text.value)); text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), o.literal(text.value)]);
} }
// When the content of the element is a single text node the translation can be inlined: // When the content of the element is a single text node the translation can be inlined:
@ -794,30 +789,35 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const meta = parseI18nMeta(i18nMeta); const meta = parseI18nMeta(i18nMeta);
const variable = this.constantPool.getTranslation(text.value, meta); const variable = this.constantPool.getTranslation(text.value, meta);
this.creationInstruction( this.creationInstruction(
text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), variable); text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), variable]);
} }
private allocateDataSlot() { return this._dataIndex++; } private allocateDataSlot() { return this._dataIndex++; }
private bindingContext() { return `${this._bindingContext++}`; } private bindingContext() { return `${this._bindingContext++}`; }
private instruction( // Bindings must only be resolved after all local refs have been visited, so all
span: ParseSourceSpan|null, reference: o.ExternalReference,
params: o.Expression[]): o.Statement {
return o.importExpr(reference, null, span).callFn(params, span).toStmt();
}
private creationInstruction(
span: ParseSourceSpan|null, reference: o.ExternalReference, ...params: o.Expression[]) {
this._creationCode.push(this.instruction(span, reference, params));
}
// Bindings must only be resolved after all local refs have been visited, so update mode
// instructions are queued in callbacks that execute once the initial pass has completed. // instructions are queued in callbacks that execute once the initial pass has completed.
// Otherwise, we wouldn't be able to support local refs that are defined after their // Otherwise, we wouldn't be able to support local refs that are defined after their
// bindings. e.g. {{ foo }} <div #foo></div> // bindings. e.g. {{ foo }} <div #foo></div>
private instructionFn(
fns: (() => o.Statement)[], span: ParseSourceSpan|null, reference: o.ExternalReference,
paramsOrFn: o.Expression[]|(() => o.Expression[])): void {
fns.push(() => {
const params = Array.isArray(paramsOrFn) ? paramsOrFn : paramsOrFn();
return instruction(span, reference, params).toStmt();
});
}
private creationInstruction(
span: ParseSourceSpan|null, reference: o.ExternalReference,
paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || []);
}
private updateInstruction( private updateInstruction(
span: ParseSourceSpan|null, reference: o.ExternalReference, paramsFn: () => o.Expression[]) { span: ParseSourceSpan|null, reference: o.ExternalReference,
this._updateCodeFns.push(() => { return this.instruction(span, reference, paramsFn()); }); paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []);
} }
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean): private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
@ -915,6 +915,12 @@ function pureFunctionCallInfo(args: o.Expression[]) {
}; };
} }
function instruction(
span: ParseSourceSpan | null, reference: o.ExternalReference,
params: o.Expression[]): o.Expression {
return o.importExpr(reference, null, span).callFn(params, span);
}
// e.g. x(2); // e.g. x(2);
function generateNextContextExpr(relativeLevelDiff: number): o.Expression { function generateNextContextExpr(relativeLevelDiff: number): o.Expression {
return o.importExpr(R3.nextContext) return o.importExpr(R3.nextContext)
@ -986,8 +992,9 @@ export class BindingScope implements LocalResolver {
/** Keeps a map from local variables to their BindingData. */ /** Keeps a map from local variables to their BindingData. */
private map = new Map<string, BindingData>(); private map = new Map<string, BindingData>();
private referenceNameIndex = 0; private referenceNameIndex = 0;
private restoreViewVariable: o.ReadVarExpr|null = null;
static ROOT_SCOPE = new BindingScope().set(-1, '$event', o.variable('$event')); static ROOT_SCOPE = new BindingScope().set(0, '$event', o.variable('$event'));
private constructor(public bindingLevel: number = 0, private parent: BindingScope|null = null) {} private constructor(public bindingLevel: number = 0, private parent: BindingScope|null = null) {}
@ -1010,6 +1017,7 @@ export class BindingScope implements LocalResolver {
this.map.set(name, value); this.map.set(name, value);
// Possibly generate a shared context var // Possibly generate a shared context var
this.maybeGenerateSharedContextVar(value); this.maybeGenerateSharedContextVar(value);
this.maybeRestoreView(value.retrievalLevel);
} }
if (value.declareLocalCallback && !value.declare) { if (value.declareLocalCallback && !value.declare) {
@ -1092,9 +1100,37 @@ export class BindingScope implements LocalResolver {
getComponentProperty(name: string): o.Expression { getComponentProperty(name: string): o.Expression {
const componentValue = this.map.get(SHARED_CONTEXT_KEY + 0) !; const componentValue = this.map.get(SHARED_CONTEXT_KEY + 0) !;
componentValue.declare = true; componentValue.declare = true;
this.maybeRestoreView(0);
return componentValue.lhs.prop(name); return componentValue.lhs.prop(name);
} }
maybeRestoreView(retrievalLevel: number) {
if (this.isListenerScope() && retrievalLevel < this.bindingLevel) {
if (!this.parent !.restoreViewVariable) {
// parent saves variable to generate a shared `const $s$ = gV();` instruction
this.parent !.restoreViewVariable = o.variable(this.parent !.freshReferenceName());
}
this.restoreViewVariable = this.parent !.restoreViewVariable;
}
}
restoreViewStatement(): o.Statement[] {
// rV($state$);
return this.restoreViewVariable ?
[instruction(null, R3.restoreView, [this.restoreViewVariable]).toStmt()] :
[];
}
viewSnapshotStatements(): o.Statement[] {
// const $state$ = gV();
const getCurrentViewInstruction = instruction(null, R3.getCurrentView, []);
return this.restoreViewVariable ?
[this.restoreViewVariable.set(getCurrentViewInstruction).toConstDecl()] :
[];
}
isListenerScope() { return this.parent && this.parent.bindingLevel === this.bindingLevel; }
variableDeclarations(): o.Statement[] { variableDeclarations(): o.Statement[] {
let currentContextLevel = 0; let currentContextLevel = 0;
return Array.from(this.map.values()) return Array.from(this.map.values())

View File

@ -70,6 +70,8 @@ export {
f7 as ɵf7, f7 as ɵf7,
f8 as ɵf8, f8 as ɵf8,
fV as ɵfV, fV as ɵfV,
gV as ɵgV,
rV as ɵrV,
cR as ɵcR, cR as ɵcR,
cr as ɵcr, cr as ɵcr,
qR as ɵqR, qR as ɵqR,

View File

@ -7,7 +7,7 @@
*/ */
import {assertEqual, assertLessThan} from './assert'; import {assertEqual, assertLessThan} from './assert';
import {NO_CHANGE, bindingUpdated, bindingUpdated2, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load, resetApplicationState} from './instructions'; import {NO_CHANGE, _getViewData, bindingUpdated, bindingUpdated2, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, load, resetApplicationState} from './instructions';
import {RENDER_PARENT} from './interfaces/container'; import {RENDER_PARENT} from './interfaces/container';
import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node'; import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view'; import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
@ -250,7 +250,7 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
ngDevMode.rendererMoveNode++; ngDevMode.rendererMoveNode++;
} }
const viewData = getViewData(); const viewData = _getViewData();
appendChild(parentNode, node.native || null, viewData); appendChild(parentNode, node.native || null, viewData);
@ -291,7 +291,7 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
* @param instructions The list of instructions to apply on the current view. * @param instructions The list of instructions to apply on the current view.
*/ */
export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void { export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void {
const viewData = getViewData(); const viewData = _getViewData();
if (ngDevMode) { if (ngDevMode) {
assertEqual(viewData[BINDING_INDEX], -1, 'i18nApply should be called before any binding'); assertEqual(viewData[BINDING_INDEX], -1, 'i18nApply should be called before any binding');
} }

View File

@ -63,6 +63,9 @@ export {
elementStyleProp as sp, elementStyleProp as sp,
elementStylingApply as sa, elementStylingApply as sa,
getCurrentView as gV,
restoreView as rV,
listener as L, listener as L,
store as st, store as st,
load as ld, load as ld,

View File

@ -22,7 +22,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LEleme
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_VIEW, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_VIEW, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -108,11 +108,40 @@ export function getCurrentSanitizer(): Sanitizer|null {
return viewData && viewData[SANITIZER]; return viewData && viewData[SANITIZER];
} }
export function getViewData(): LViewData { /**
* Returns the current OpaqueViewState instance.
*
* Used in conjunction with the restoreView() instruction to save a snapshot
* of the current view and restore it when listeners are invoked. This allows
* walking the declaration view tree in listeners to get vars from parent views.
*/
export function getCurrentView(): OpaqueViewState {
return (viewData as any) as OpaqueViewState;
}
/**
* Internal function that returns the current LViewData instance.
*
* The getCurrentView() instruction should be used for anything public.
*/
export function _getViewData(): LViewData {
// top level variables should not be exported for performance reasons (PERF_NOTES.md) // top level variables should not be exported for performance reasons (PERF_NOTES.md)
return viewData; return viewData;
} }
/**
* Restores `contextViewData` to the given OpaqueViewState instance.
*
* Used in conjunction with the getCurrentView() instruction to save a snapshot
* of the current view and restore it when listeners are invoked. This allows
* walking the declaration view tree in listeners to get vars from parent views.
*
* @param viewToRestore The LViewData instance to restore.
*/
export function restoreView(viewToRestore: OpaqueViewState) {
contextViewData = (viewToRestore as any) as LViewData;
}
/** Used to set the parent property when nodes are created. */ /** Used to set the parent property when nodes are created. */
let previousOrParentNode: LNode; let previousOrParentNode: LNode;
@ -583,9 +612,9 @@ export function renderEmbeddedTemplate<T>(
* @param level The relative level of the view from which to grab context compared to contextVewData * @param level The relative level of the view from which to grab context compared to contextVewData
* @returns context * @returns context
*/ */
export function nextContext(level: number = 1): any { export function nextContext<T = any>(level: number = 1): T {
contextViewData = walkUpViews(level, contextViewData !); contextViewData = walkUpViews(level, contextViewData !);
return contextViewData[CONTEXT]; return contextViewData[CONTEXT] as T;
} }
export function renderComponentOrTemplate<T>( export function renderComponentOrTemplate<T>(

View File

@ -40,6 +40,14 @@ export const CONTAINER_INDEX = 14;
export const CONTENT_QUERIES = 15; export const CONTENT_QUERIES = 15;
export const DECLARATION_VIEW = 16; export const DECLARATION_VIEW = 16;
// This interface replaces the real LViewData interface if it is an arg or a
// return value of a public instruction. This ensures we don't need to expose
// the actual interface, which should be kept private.
export interface OpaqueViewState {
'__brand__': 'Brand for OpaqueViewState that nothing will match';
}
/** /**
* `LViewData` stores all of the information needed to process the instructions as * `LViewData` stores all of the information needed to process the instructions as
* they are invoked from the template. Each embedded view and component view has its * they are invoked from the template. Each embedded view and component view has its

View File

@ -57,6 +57,8 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵf7': r3.f7, 'ɵf7': r3.f7,
'ɵf8': r3.f8, 'ɵf8': r3.f8,
'ɵfV': r3.fV, 'ɵfV': r3.fV,
'ɵgV': r3.gV,
'ɵrV': r3.rV,
'ɵi1': r3.i1, 'ɵi1': r3.i1,
'ɵi2': r3.i2, 'ɵi2': r3.i2,
'ɵi3': r3.i3, 'ɵi3': r3.i3,

View File

@ -521,6 +521,9 @@
{ {
"name": "getCurrentSanitizer" "name": "getCurrentSanitizer"
}, },
{
"name": "getCurrentView"
},
{ {
"name": "getInitialIndex" "name": "getInitialIndex"
}, },
@ -761,6 +764,9 @@
{ {
"name": "readElementValue" "name": "readElementValue"
}, },
{
"name": "reference"
},
{ {
"name": "refreshChildComponents" "name": "refreshChildComponents"
}, },
@ -800,6 +806,9 @@
{ {
"name": "resolveRendererType2" "name": "resolveRendererType2"
}, },
{
"name": "restoreView"
},
{ {
"name": "sameHostView" "name": "sameHostView"
}, },

View File

@ -10,7 +10,7 @@ import {NgForOfContext} from '@angular/common';
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
import {AttributeMarker, defineComponent} from '../../src/render3/index'; import {AttributeMarker, defineComponent} from '../../src/render3/index';
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, text, textBinding} from '../../src/render3/instructions'; import {bind, container, elementEnd, elementProperty, elementStart, getCurrentView, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, restoreView, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def';
@ -337,13 +337,17 @@ describe('@angular/common integration', () => {
function pTemplate(rf: RenderFlags, ctx: any) { function pTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
const row = nextContext().$implicit as any; const state = getCurrentView();
const app = nextContext();
elementStart(0, 'p'); elementStart(0, 'p');
{ {
elementStart(1, 'span'); elementStart(1, 'span');
{ {
listener('click', () => { app.onClick(row.value, app.name); }); listener('click', () => {
restoreView(state);
const row = nextContext().$implicit as any;
const app = nextContext();
app.onClick(row.value, app.name);
});
} }
elementEnd(); elementEnd();
text(2); text(2);