diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index 24923b575f..6be8065019 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -122,4 +122,83 @@ describe('compiler compliance: bindings', () => { }); }); + describe('non bindable behaviour', () => { + const getAppFiles = (template: string = ''): MockDirectory => ({ + app: { + 'example.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-app', + template: \`${template}\` + }) + export class MyComponent { + name = 'John Doe'; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {}` + } + }); + + it('should keep local ref for host element', () => { + const files: MockDirectory = getAppFiles(` + + Hello {{ name }}! + + {{ myRef.id }} + `); + + const template = ` + const $_c0$ = ["id", "my-id"]; + const $_c1$ = ["myRef", ""]; + … + template:function MyComponent_Template(rf, $ctx$){ + if (rf & 1) { + $i0$.ɵelementStart(0, "b", $_c0$, $_c1$); + $i0$.ɵsetBindingsDisabled(); + $i0$.ɵelementStart(2, "i"); + $i0$.ɵtext(3, "Hello {{ name }}!"); + $i0$.ɵelementEnd(); + $i0$.ɵsetBindingsEnabled(); + $i0$.ɵelementEnd(); + $i0$.ɵtext(4); + } + if (rf & 2) { + const $_r0$ = $i0$.ɵreference(1); + $i0$.ɵtextBinding(4, $i0$.ɵinterpolation1(" ", $_r0$.id, " ")); + } + } + `; + const result = compile(files, angularFiles); + expectEmit(result.source, template, + 'Incorrect handling of local refs for host element'); + }); + + it('should not have local refs for nested elements', () => { + const files: MockDirectory = getAppFiles(` +
+ {{ myInput.value }} +
+ `); + + const template = ` + const $_c0$ = ["value", "one", "#myInput", ""]; + … + template:function MyComponent_Template(rf, $ctx$){ + if (rf & 1) { + $i0$.ɵelementStart(0, "div"); + $i0$.ɵsetBindingsDisabled(); + $i0$.ɵelement(1, "input", $_c0$); + $i0$.ɵtext(2, " {{ myInput.value }} "); + $i0$.ɵsetBindingsEnabled(); + $i0$.ɵelementEnd(); + } + `; + const result = compile(files, angularFiles); + expectEmit(result.source, template, + 'Incorrect handling of local refs for nested elements'); + }); + }); + }); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 2c96728765..5faddc5b20 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -60,6 +60,10 @@ export class Identifiers { static bind: o.ExternalReference = {name: 'ɵbind', moduleName: CORE}; + static setBindingsEnabled: o.ExternalReference = {name: 'ɵsetBindingsEnabled', moduleName: CORE}; + + static setBindingsDisabled: o.ExternalReference = {name: 'ɵsetBindingsDisabled', moduleName: CORE}; + static getCurrentView: o.ExternalReference = {name: 'ɵgetCurrentView', moduleName: CORE}; static restoreView: o.ExternalReference = {name: 'ɵrestoreView', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index c3c84fe164..37ade39b8a 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -30,7 +30,7 @@ import {htmlAstToRender3Ast} from '../r3_template_transform'; import {R3QueryMetadata} from './api'; import {parseStyle} from './styling'; -import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, invalid, isI18NAttribute, mapToExpression, trimTrailingNulls, unsupported} from './util'; +import {CONTEXT_NAME, NON_BINDABLE_ATTR, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, invalid, isI18NAttribute, mapToExpression, trimTrailingNulls, unsupported} from './util'; function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined { switch (type) { @@ -304,11 +304,15 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex); } + let isNonBindableMode: boolean = false; + // Handle i18n attributes for (const attr of element.attributes) { const name = attr.name; const value = attr.value; - if (name === I18N_ATTR) { + if (name === NON_BINDABLE_ATTR) { + isNonBindableMode = true; + } else if (name === I18N_ATTR) { if (this._inI18nSection) { throw new Error( `Could not mark an element as translatable inside of a translatable section`); @@ -487,6 +491,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart, trimTrailingNulls(parameters)); + if (isNonBindableMode) { + this.creationInstruction(element.sourceSpan, R3.setBindingsDisabled); + } + // initial styling for static style="..." attributes if (hasStylingInstructions) { const paramsList: (o.Expression)[] = []; @@ -652,6 +660,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (!createSelfClosingInstruction) { // Finish element construction mode. + if (isNonBindableMode) { + this.creationInstruction( + element.endSourceSpan || element.sourceSpan, + R3.setBindingsEnabled); + } this.creationInstruction( element.endSourceSpan || element.sourceSpan, isNgContainer ? R3.elementContainerEnd : R3.elementEnd); diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index c5462e90c7..ab748de7c7 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -35,6 +35,9 @@ export const I18N_ATTR_PREFIX = 'i18n-'; export const MEANING_SEPARATOR = '|'; export const ID_SEPARATOR = '@@'; +/** Non bindable attribute name **/ +export const NON_BINDABLE_ATTR = 'ngNonBindable'; + /** * Creates an allocator for a temporary variable. * diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 1182a8cdf9..0fde206469 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -84,6 +84,8 @@ export { elementProperty as ɵelementProperty, projectionDef as ɵprojectionDef, reference as ɵreference, + setBindingsEnabled as ɵsetBindingsEnabled, + setBindingsDisabled as ɵsetBindingsDisabled, elementAttribute as ɵelementAttribute, elementStyling as ɵelementStyling, elementStylingMap as ɵelementStylingMap, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 66f8e29eb9..8530c2e8f1 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -67,6 +67,9 @@ export { namespaceMathML, namespaceSVG, + setBindingsEnabled, + setBindingsDisabled, + projection, projectionDef, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 72a707c331..3669f20381 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -99,6 +99,8 @@ export function getCurrentSanitizer(): Sanitizer|null { */ let elementDepthCount !: number; +let bindingsEnabled !: boolean; + /** * Returns the current OpaqueViewState instance. * @@ -538,6 +540,7 @@ export function resetComponentState() { isParent = false; previousOrParentTNode = null !; elementDepthCount = 0; + bindingsEnabled = true; } /** @@ -862,6 +865,7 @@ function nativeNodeLocalRefExtractor(tNode: TNode, currentView: LViewData): RNod function createDirectivesAndLocals( localRefs: string[] | null | undefined, localRefExtractor: LocalRefExtractor = nativeNodeLocalRefExtractor) { + if (!bindingsEnabled) return; if (firstTemplatePass) { ngDevMode && ngDevMode.firstTemplatePass++; cacheMatchingDirectivesForNode(previousOrParentTNode, tView, localRefs || null); @@ -1394,6 +1398,22 @@ export function elementProperty( } } +/** + * Enables bindings processing for further instructions + * (used while processing "ngNonBindable" element's attribute). + */ +export function setBindingsEnabled(): void { + bindingsEnabled = true; +} + +/** + * Disables bindings processing for further instructions + * (used while processing "ngNonBindable" element's attribute). + */ +export function setBindingsDisabled(): void { + bindingsEnabled = false; +} + /** * Constructs a TNode object from the arguments. * diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 2851737dcc..0693075718 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -46,6 +46,8 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵnamespaceHTML': r3.namespaceHTML, 'ɵnamespaceMathML': r3.namespaceMathML, 'ɵnamespaceSVG': r3.namespaceSVG, + 'ɵsetBindingsEnabled': r3.setBindingsEnabled, + 'ɵsetBindingsDisabled': r3.setBindingsDisabled, 'ɵelementStart': r3.elementStart, 'ɵelementEnd': r3.elementEnd, 'ɵelement': r3.element, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 5e959cb163..617890bc1a 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -7,12 +7,12 @@ */ import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; -import {RenderFlags} from '@angular/core/src/render3'; +import {reference, RenderFlags} from '@angular/core/src/render3'; import {RendererStyleFlags2, RendererType2} from '../../src/render/api'; import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index'; -import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, listener, load, loadDirective, projection, projectionDef, text, textBinding, template} from '../../src/render3/instructions'; +import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, setBindingsEnabled, setBindingsDisabled, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, listener, load, loadDirective, projection, projectionDef, text, textBinding, template} from '../../src/render3/instructions'; import {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3, RText, RComment, RNode, RendererStyleFlags3, ProceduralRenderer3} from '../../src/render3/interfaces/renderer'; import {HEADER_OFFSET, CONTEXT, DIRECTIVES} from '../../src/render3/interfaces/view'; @@ -132,6 +132,132 @@ describe('render3 integration test', () => { }); + + describe('ngNonBindable handling', () => { + it('should keep local ref for host element', () => { + /** + * + * Hello {{ name }}! + * + * {{ myRef.id }} + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'b', ['id', 'my-id'], ['myRef', '']); + setBindingsDisabled(); + elementStart(2, 'i'); + text(3, 'Hello {{ name }}!'); + elementEnd(); + setBindingsEnabled(); + elementEnd(); + text(4); + } + if (rf & RenderFlags.Update) { + const ref = reference(1) as any; + textBinding(4, interpolation1(" ", ref.id, " ")); + } + }, 5, 1); + + const fixture = new ComponentFixture(App); + expect(fixture.html).toEqual('Hello {{ name }}! my-id '); + }); + + it('should not have local refs for nested elements', () => { + /** + *
+ * {{ myInput.value }} + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + setBindingsDisabled(); + element(1, 'input', ['value', 'one']); + text(2, '{{ myInput.value }}'); + setBindingsEnabled(); + elementEnd(); + } + }, 3, 0); + + const fixture = new ComponentFixture(App); + expect(fixture.html).toEqual('
{{ myInput.value }}
'); + }); + + it('should invoke directives for host element', () => { + let directiveInvoked: boolean = false; + + class TestDirective { + ngOnInit() { + directiveInvoked = true; + } + + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'directive', '']], + factory: () => new TestDirective() + }); + } + + /** + * + * Hello {{ name }}! + * + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'b', ['directive', '']); + setBindingsDisabled(); + elementStart(1, 'i'); + text(2, 'Hello {{ name }}!'); + elementEnd(); + setBindingsEnabled(); + elementEnd(); + } + }, 3, 0, [TestDirective]); + + const fixture = new ComponentFixture(App); + expect(fixture.html).toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(true); + }); + + it('should not invoke directives for nested elements', () => { + let directiveInvoked: boolean = false; + + class TestDirective { + ngOnInit() { + directiveInvoked = true; + } + + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'directive', '']], + factory: () => new TestDirective() + }); + } + + /** + * + * Hello {{ name }}! + * + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'b'); + setBindingsDisabled(); + elementStart(1, 'i', ['directive', '']); + text(2, 'Hello {{ name }}!'); + elementEnd(); + setBindingsEnabled(); + elementEnd(); + } + }, 3, 0, [TestDirective]); + + const fixture = new ComponentFixture(App); + expect(fixture.html).toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(false); + }); + }); + describe('Siblings update', () => { it('should handle a flat list of static/bound text nodes', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {