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 * *