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) {