diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts
index 2fc273996e..41bb8e5740 100644
--- a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts
+++ b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts
@@ -58,4 +58,121 @@ describe('compiler compliance: listen()', () => {
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: \`
+
+
+ \`
+ })
+ 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: \`
+
+
+ \`
+ })
+ 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');
+ });
+
});
diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts
index 26a8448c33..9cc32663e8 100644
--- a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts
+++ b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts
@@ -55,12 +55,14 @@ describe('compiler compliance: template', () => {
function MyComponent_ul_li_div_Template_1(rf, ctx) {
if (rf & 1) {
- const $inner$ = ctx.$implicit;
- const $middle$ = $i0$.ɵx().$implicit;
- const $outer$ = $i0$.ɵx().$implicit;
- const $myComp$ = $i0$.ɵx();
+ 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 $middle$ = $i0$.ɵx().$implicit;
+ const $outer$ = $i0$.ɵx().$implicit;
+ const $myComp$ = $i0$.ɵx();
return $myComp$.onClick($outer$, $middle$, $inner$);
});
$i0$.ɵT(1);
diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts
index ff66593f12..41ce176ac4 100644
--- a/packages/compiler/src/render3/r3_identifiers.ts
+++ b/packages/compiler/src/render3/r3_identifiers.ts
@@ -53,6 +53,10 @@ export class Identifiers {
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 interpolation2: o.ExternalReference = {name: 'ɵi2', moduleName: CORE};
static interpolation3: o.ExternalReference = {name: 'ɵi3', moduleName: CORE};
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index a67c4a40fb..52eccac13d 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -56,7 +56,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
private _dataIndex = 0;
private _bindingContext = 0;
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
* 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, LocalResolver
* This scope contains local variables declared in the update mode block of the template.
* (e.g. refs and context vars in bindings)
*/
- private _updateScope: 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 _bindingScope: BindingScope;
private _valueConverter: ValueConverter;
private _unsupported = unsupported;
@@ -104,9 +104,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// function)
this._dataIndex = viewQueries.length;
- // TODO(kara): generate restore instruction in listener to replace creation scope
- this._creationScope = parentBindingScope.nestedScope(level);
- this._updateScope = parentBindingScope.nestedScope(level);
+ this._bindingScope = parentBindingScope.nestedScope(level);
this._valueConverter = new ValueConverter(
constantPool, () => this.allocateDataSlot(),
@@ -116,17 +114,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
if (pipeType) {
this.pipes.add(pipeType);
}
- this._updateScope.set(this.level, localName, value);
- this._creationCode.push(
- o.importExpr(R3.pipe).callFn([o.literal(slot), o.literal(name)]).toStmt());
+ this._bindingScope.set(this.level, localName, value);
+ this.creationInstruction(null, R3.pipe, [o.literal(slot), o.literal(name)]);
});
}
- registerContextVariables(variable: t.Variable, retrievalScope: BindingScope) {
- const scopedName = retrievalScope.freshReferenceName();
+ registerContextVariables(variable: t.Variable) {
+ const scopedName = this._bindingScope.freshReferenceName();
const retrievalLevel = this.level;
const lhs = o.variable(variable.name + scopedName);
- retrievalScope.set(
+ this._bindingScope.set(
retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT,
(scope: BindingScope, relativeLevel: number) => {
let rhs: o.Expression;
@@ -151,11 +148,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
}
// Create variable bindings
- for (const variable of variables) {
- // Add the reference to the local scope.
- this.registerContextVariables(variable, this._creationScope);
- this.registerContextVariables(variable, this._updateScope);
- }
+ variables.forEach(v => this.registerContextVariables(v));
// Output a `ProjectionDef` instruction when some `` are present
if (hasNgContent) {
@@ -170,34 +163,38 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
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
- // generate all creation mode instructions & queue all update mode instructions for
- // generation in the second pass. It's necessary to separate the passes to ensure
- // local refs are defined before resolving bindings.
+ // queue all creation mode and update mode instructions for generation in the second
+ // pass. It's necessary to separate the passes to ensure local refs are defined before
+ // resolving bindings.
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());
// To count slots for the reserveSlots() instruction, all bindings must have been visited.
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(
- core.RenderFlags.Create,
- this._creationScope.variableDeclarations().concat(this._creationCode))] :
+ core.RenderFlags.Create, creationVariables.concat(creationStatements))] :
[];
- // This must occur after binding resolution so we can generate context instructions that
- // 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 ?
+ const updateBlock = updateStatements.length > 0 ?
[renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] :
[];
@@ -205,7 +202,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// TODO(vicb): This is a WIP, not fully supported yet
for (const phToNodeIdx of this._phToNodeIdxes) {
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();
this._prefixCode.push(phMap);
@@ -221,15 +218,15 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// Temporary variable declarations for query refresh (i.e. let _t: any;)
...this._prefixCode,
// Creating mode (i.e. if (rf & RenderFlags.Create) { ... })
- ...creationCode,
+ ...creationBlock,
// Binding and refresh mode (i.e. if (rf & RenderFlags.Update) {...})
- ...updateCode,
+ ...updateBlock,
],
o.INFERRED_TYPE, null, this.templateName);
}
// 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) {
const slot = this.allocateDataSlot();
@@ -251,7 +248,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
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, LocalResolver
];
// Add the attributes
- const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = [];
const initialStyleDeclarations: o.Expression[] = [];
const initialClassDeclarations: o.Expression[] = [];
@@ -460,10 +456,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
const references = flatten(element.references.map(reference => {
const slot = this.allocateDataSlot();
// Generate the update temporary.
- const variableName = this._updateScope.freshReferenceName();
+ const variableName = this._bindingScope.freshReferenceName();
const retrievalLevel = this.level;
const lhs = o.variable(variableName);
- this._updateScope.set(
+ this._bindingScope.set(
retrievalLevel, reference.name, lhs, DeclarationPriority.DEFAULT,
(scope: BindingScope, relativeLevel: number) => {
// e.g. x(2);
@@ -481,11 +477,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
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 currentNamespace = this.getNamespaceInstruction(namespaceKey);
@@ -501,14 +492,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
!hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0;
if (createSelfClosingInstruction) {
- this.creationInstruction(element.sourceSpan, R3.element, ...trimTrailingNulls(parameters));
+ this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
} else {
- // Generate the instruction create element instruction
- if (i18nMessages.length > 0) {
- this._creationCode.push(...i18nMessages);
- }
- this.creationInstruction(
- element.sourceSpan, R3.elementStart, ...trimTrailingNulls(parameters));
+ this.creationInstruction(element.sourceSpan, R3.elementStart, trimTrailingNulls(parameters));
// initial styling for static style="..." attributes
if (hasStylingInstructions) {
@@ -538,12 +524,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
paramsList.push(o.NULL_EXPR);
}
-
if (useDefaultStyleSanitizer) {
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)
@@ -551,14 +536,25 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
const elName = sanitizeIdentifier(element.name);
const evName = sanitizeIdentifier(outputAst.name);
const functionName = `${this.templateName}_${elName}_${evName}_listener`;
- const bindingExpr = convertActionBinding(
- this._creationScope, implicit, outputAst.handler, 'b',
- () => error('Unexpected interpolation'));
- const handler = o.fn(
- [new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts],
- o.INFERRED_TYPE, null, functionName);
- this.creationInstruction(
- outputAst.sourceSpan, R3.listener, o.literal(outputAst.name), handler);
+
+ this.creationInstruction(outputAst.sourceSpan, R3.listener, () => {
+ const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel);
+
+ const bindingExpr = convertActionBinding(
+ listenerScope, implicit, outputAst.handler, 'b',
+ () => error('Unexpected interpolation'));
+
+ const statements = [
+ ...listenerScope.restoreViewStatement(), ...listenerScope.variableDeclarations(),
+ ...bindingExpr.render3Stmts
+ ];
+
+ const handler = o.fn(
+ [new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
+ functionName);
+
+ return [o.literal(outputAst.name), handler];
+ });
});
}
@@ -636,8 +632,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
lastInputCommand = classInputs[classInputs.length - 1];
}
- this.updateInstruction(
- lastInputCommand !.sourceSpan, R3.elementStylingApply, () => [indexLiteral]);
+ this.updateInstruction(lastInputCommand !.sourceSpan, R3.elementStylingApply, [indexLiteral]);
}
// Generate element input bindings
@@ -725,7 +720,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// e.g. C(1, C1Template)
this.creationInstruction(
- template.sourceSpan, R3.containerCreate, ...trimTrailingNulls(parameters));
+ template.sourceSpan, R3.containerCreate, trimTrailingNulls(parameters));
// e.g. p(1, 'forOf', ɵb(ctx.items));
const context = o.variable(CONTEXT_NAME);
@@ -741,7 +736,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// Create the template function
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);
// Nested templates must not be visited until after their parent templates have completed
@@ -765,7 +760,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
visitBoundText(text: t.BoundText) {
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);
this.updateInstruction(
@@ -775,7 +770,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
visitText(text: t.Text) {
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:
@@ -794,30 +789,35 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
const meta = parseI18nMeta(i18nMeta);
const variable = this.constantPool.getTranslation(text.value, meta);
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 bindingContext() { return `${this._bindingContext++}`; }
- private instruction(
- 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
+ // Bindings must only be resolved after all local refs have been visited, so all
// 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
// bindings. e.g. {{ foo }}
+ 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(
- span: ParseSourceSpan|null, reference: o.ExternalReference, paramsFn: () => o.Expression[]) {
- this._updateCodeFns.push(() => { return this.instruction(span, reference, paramsFn()); });
+ span: ParseSourceSpan|null, reference: o.ExternalReference,
+ paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
+ this.instructionFn(this._updateCodeFns, span, reference, paramsOrFn || []);
}
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);
function generateNextContextExpr(relativeLevelDiff: number): o.Expression {
return o.importExpr(R3.nextContext)
@@ -986,8 +992,9 @@ export class BindingScope implements LocalResolver {
/** Keeps a map from local variables to their BindingData. */
private map = new Map();
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) {}
@@ -1010,6 +1017,7 @@ export class BindingScope implements LocalResolver {
this.map.set(name, value);
// Possibly generate a shared context var
this.maybeGenerateSharedContextVar(value);
+ this.maybeRestoreView(value.retrievalLevel);
}
if (value.declareLocalCallback && !value.declare) {
@@ -1092,9 +1100,37 @@ export class BindingScope implements LocalResolver {
getComponentProperty(name: string): o.Expression {
const componentValue = this.map.get(SHARED_CONTEXT_KEY + 0) !;
componentValue.declare = true;
+ this.maybeRestoreView(0);
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[] {
let currentContextLevel = 0;
return Array.from(this.map.values())
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index 3d64d0b715..b34f8b9cff 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -70,6 +70,8 @@ export {
f7 as ɵf7,
f8 as ɵf8,
fV as ɵfV,
+ gV as ɵgV,
+ rV as ɵrV,
cR as ɵcR,
cr as ɵcr,
qR as ɵqR,
diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts
index 78e011c7b4..6687bea279 100644
--- a/packages/core/src/render3/i18n.ts
+++ b/packages/core/src/render3/i18n.ts
@@ -7,7 +7,7 @@
*/
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 {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
@@ -250,7 +250,7 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
ngDevMode.rendererMoveNode++;
}
- const viewData = getViewData();
+ const viewData = _getViewData();
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.
*/
export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void {
- const viewData = getViewData();
+ const viewData = _getViewData();
if (ngDevMode) {
assertEqual(viewData[BINDING_INDEX], -1, 'i18nApply should be called before any binding');
}
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index da441134c4..46d4eedcce 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -63,6 +63,9 @@ export {
elementStyleProp as sp,
elementStylingApply as sa,
+ getCurrentView as gV,
+ restoreView as rV,
+
listener as L,
store as st,
load as ld,
diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts
index d6686250b3..ccce7b5537 100644
--- a/packages/core/src/render3/instructions.ts
+++ b/packages/core/src/render3/instructions.ts
@@ -22,7 +22,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LEleme
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query';
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 {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@@ -108,11 +108,40 @@ export function getCurrentSanitizer(): Sanitizer|null {
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)
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. */
let previousOrParentNode: LNode;
@@ -583,9 +612,9 @@ export function renderEmbeddedTemplate(
* @param level The relative level of the view from which to grab context compared to contextVewData
* @returns context
*/
-export function nextContext(level: number = 1): any {
+export function nextContext(level: number = 1): T {
contextViewData = walkUpViews(level, contextViewData !);
- return contextViewData[CONTEXT];
+ return contextViewData[CONTEXT] as T;
}
export function renderComponentOrTemplate(
diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts
index 168bbabec3..68b5ecb193 100644
--- a/packages/core/src/render3/interfaces/view.ts
+++ b/packages/core/src/render3/interfaces/view.ts
@@ -40,6 +40,14 @@ export const CONTAINER_INDEX = 14;
export const CONTENT_QUERIES = 15;
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
* they are invoked from the template. Each embedded view and component view has its
diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts
index 5c02dd224c..c896c4e367 100644
--- a/packages/core/src/render3/jit/environment.ts
+++ b/packages/core/src/render3/jit/environment.ts
@@ -57,6 +57,8 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵf7': r3.f7,
'ɵf8': r3.f8,
'ɵfV': r3.fV,
+ 'ɵgV': r3.gV,
+ 'ɵrV': r3.rV,
'ɵi1': r3.i1,
'ɵi2': r3.i2,
'ɵi3': r3.i3,
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index f0511e71e2..2f71371532 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -521,6 +521,9 @@
{
"name": "getCurrentSanitizer"
},
+ {
+ "name": "getCurrentView"
+ },
{
"name": "getInitialIndex"
},
@@ -761,6 +764,9 @@
{
"name": "readElementValue"
},
+ {
+ "name": "reference"
+ },
{
"name": "refreshChildComponents"
},
@@ -800,6 +806,9 @@
{
"name": "resolveRendererType2"
},
+ {
+ "name": "restoreView"
+ },
{
"name": "sameHostView"
},
diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts
index 91fea58aa2..cd3a85c0f4 100644
--- a/packages/core/test/render3/common_integration_spec.ts
+++ b/packages/core/test/render3/common_integration_spec.ts
@@ -10,7 +10,7 @@ import {NgForOfContext} from '@angular/common';
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
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 {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def';
@@ -337,13 +337,17 @@ describe('@angular/common integration', () => {
function pTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
- const row = nextContext().$implicit as any;
- const app = nextContext();
+ const state = getCurrentView();
elementStart(0, 'p');
{
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();
text(2);
diff --git a/packages/core/test/render3/listeners_spec.ts b/packages/core/test/render3/listeners_spec.ts
index 8ad92106ae..08c96c7de9 100644
--- a/packages/core/test/render3/listeners_spec.ts
+++ b/packages/core/test/render3/listeners_spec.ts
@@ -233,7 +233,7 @@ describe('event listeners', () => {
it('should destroy listeners in views with renderer2', () => {
/**
- * % if (ctx.showing) {
+ * % if (ctx.showing) {
*
* % }
*/
@@ -292,7 +292,7 @@ describe('event listeners', () => {
it('should destroy listeners in for loops', () => {
/**
- * % for (let i = 0; i < ctx.buttons; i++) {
+ * % for (let i = 0; i < ctx.buttons; i++) {
*
* % }
*/
@@ -353,7 +353,7 @@ describe('event listeners', () => {
it('should destroy listeners in for loops with renderer2', () => {
/**
- * % for (let i = 0; i < ctx.buttons; i++) {
+ * % for (let i = 0; i < ctx.buttons; i++) {
*
* % }
*/
@@ -449,7 +449,7 @@ describe('event listeners', () => {
it('should destroy listeners in nested views', () => {
/**
- * % if (showing) {
+ * % if (showing) {
* Hello
* % if (button) {
*
@@ -511,7 +511,7 @@ describe('event listeners', () => {
it('should destroy listeners in component views', () => {
/**
- * % if (showing) {
+ * % if (showing) {
* Hello
*
*