feat(ivy): adding support for ngNonBindable attribute

This commit is contained in:
Andrew Kushnir 2018-09-26 13:19:04 -07:00 committed by Alex Rickabaugh
parent eeebe28c0f
commit b286abeabe
9 changed files with 256 additions and 4 deletions

View File

@ -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(`
<b ngNonBindable #myRef id="my-id">
<i>Hello {{ name }}!</i>
</b>
{{ 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(`
<div ngNonBindable>
<input value="one" #myInput> {{ myInput.value }}
</div>
`);
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');
});
});
}); });

View File

@ -60,6 +60,10 @@ export class Identifiers {
static bind: o.ExternalReference = {name: 'ɵbind', moduleName: CORE}; 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 getCurrentView: o.ExternalReference = {name: 'ɵgetCurrentView', moduleName: CORE};
static restoreView: o.ExternalReference = {name: 'ɵrestoreView', moduleName: CORE}; static restoreView: o.ExternalReference = {name: 'ɵrestoreView', moduleName: CORE};

View File

@ -30,7 +30,7 @@ import {htmlAstToRender3Ast} from '../r3_template_transform';
import {R3QueryMetadata} from './api'; import {R3QueryMetadata} from './api';
import {parseStyle} from './styling'; 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 { function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
switch (type) { switch (type) {
@ -304,11 +304,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex); this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex);
} }
let isNonBindableMode: boolean = false;
// Handle i18n attributes // Handle i18n attributes
for (const attr of element.attributes) { for (const attr of element.attributes) {
const name = attr.name; const name = attr.name;
const value = attr.value; const value = attr.value;
if (name === I18N_ATTR) { if (name === NON_BINDABLE_ATTR) {
isNonBindableMode = true;
} else if (name === I18N_ATTR) {
if (this._inI18nSection) { if (this._inI18nSection) {
throw new Error( throw new Error(
`Could not mark an element as translatable inside of a translatable section`); `Could not mark an element as translatable inside of a translatable section`);
@ -487,6 +491,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart, element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
trimTrailingNulls(parameters)); trimTrailingNulls(parameters));
if (isNonBindableMode) {
this.creationInstruction(element.sourceSpan, R3.setBindingsDisabled);
}
// initial styling for static style="..." attributes // initial styling for static style="..." attributes
if (hasStylingInstructions) { if (hasStylingInstructions) {
const paramsList: (o.Expression)[] = []; const paramsList: (o.Expression)[] = [];
@ -652,6 +660,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (!createSelfClosingInstruction) { if (!createSelfClosingInstruction) {
// Finish element construction mode. // Finish element construction mode.
if (isNonBindableMode) {
this.creationInstruction(
element.endSourceSpan || element.sourceSpan,
R3.setBindingsEnabled);
}
this.creationInstruction( this.creationInstruction(
element.endSourceSpan || element.sourceSpan, element.endSourceSpan || element.sourceSpan,
isNgContainer ? R3.elementContainerEnd : R3.elementEnd); isNgContainer ? R3.elementContainerEnd : R3.elementEnd);

View File

@ -35,6 +35,9 @@ export const I18N_ATTR_PREFIX = 'i18n-';
export const MEANING_SEPARATOR = '|'; export const MEANING_SEPARATOR = '|';
export const ID_SEPARATOR = '@@'; export const ID_SEPARATOR = '@@';
/** Non bindable attribute name **/
export const NON_BINDABLE_ATTR = 'ngNonBindable';
/** /**
* Creates an allocator for a temporary variable. * Creates an allocator for a temporary variable.
* *

View File

@ -84,6 +84,8 @@ export {
elementProperty as ɵelementProperty, elementProperty as ɵelementProperty,
projectionDef as ɵprojectionDef, projectionDef as ɵprojectionDef,
reference as ɵreference, reference as ɵreference,
setBindingsEnabled as ɵsetBindingsEnabled,
setBindingsDisabled as ɵsetBindingsDisabled,
elementAttribute as ɵelementAttribute, elementAttribute as ɵelementAttribute,
elementStyling as ɵelementStyling, elementStyling as ɵelementStyling,
elementStylingMap as ɵelementStylingMap, elementStylingMap as ɵelementStylingMap,

View File

@ -67,6 +67,9 @@ export {
namespaceMathML, namespaceMathML,
namespaceSVG, namespaceSVG,
setBindingsEnabled,
setBindingsDisabled,
projection, projection,
projectionDef, projectionDef,

View File

@ -99,6 +99,8 @@ export function getCurrentSanitizer(): Sanitizer|null {
*/ */
let elementDepthCount !: number; let elementDepthCount !: number;
let bindingsEnabled !: boolean;
/** /**
* Returns the current OpaqueViewState instance. * Returns the current OpaqueViewState instance.
* *
@ -538,6 +540,7 @@ export function resetComponentState() {
isParent = false; isParent = false;
previousOrParentTNode = null !; previousOrParentTNode = null !;
elementDepthCount = 0; elementDepthCount = 0;
bindingsEnabled = true;
} }
/** /**
@ -862,6 +865,7 @@ function nativeNodeLocalRefExtractor(tNode: TNode, currentView: LViewData): RNod
function createDirectivesAndLocals( function createDirectivesAndLocals(
localRefs: string[] | null | undefined, localRefs: string[] | null | undefined,
localRefExtractor: LocalRefExtractor = nativeNodeLocalRefExtractor) { localRefExtractor: LocalRefExtractor = nativeNodeLocalRefExtractor) {
if (!bindingsEnabled) return;
if (firstTemplatePass) { if (firstTemplatePass) {
ngDevMode && ngDevMode.firstTemplatePass++; ngDevMode && ngDevMode.firstTemplatePass++;
cacheMatchingDirectivesForNode(previousOrParentTNode, tView, localRefs || null); cacheMatchingDirectivesForNode(previousOrParentTNode, tView, localRefs || null);
@ -1394,6 +1398,22 @@ export function elementProperty<T>(
} }
} }
/**
* 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. * Constructs a TNode object from the arguments.
* *

View File

@ -46,6 +46,8 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵnamespaceHTML': r3.namespaceHTML, 'ɵnamespaceHTML': r3.namespaceHTML,
'ɵnamespaceMathML': r3.namespaceMathML, 'ɵnamespaceMathML': r3.namespaceMathML,
'ɵnamespaceSVG': r3.namespaceSVG, 'ɵnamespaceSVG': r3.namespaceSVG,
'ɵsetBindingsEnabled': r3.setBindingsEnabled,
'ɵsetBindingsDisabled': r3.setBindingsDisabled,
'ɵelementStart': r3.elementStart, 'ɵelementStart': r3.elementStart,
'ɵelementEnd': r3.elementEnd, 'ɵelementEnd': r3.elementEnd,
'ɵelement': r3.element, 'ɵelement': r3.element,

View File

@ -7,12 +7,12 @@
*/ */
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; 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 {RendererStyleFlags2, RendererType2} from '../../src/render/api';
import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index'; 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 {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3, RText, RComment, RNode, RendererStyleFlags3, ProceduralRenderer3} from '../../src/render3/interfaces/renderer'; 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'; 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', () => {
/**
* <b ngNonBindable #myRef id="my-id">
* <i>Hello {{ name }}!</i>
* </b>
* {{ 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('<b id="my-id"><i>Hello {{ name }}!</i></b> my-id ');
});
it('should not have local refs for nested elements', () => {
/**
* <div ngNonBindable>
* <input value="one" #myInput> {{ myInput.value }}
* </div>
*/
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('<div><input value="one">{{ myInput.value }}</div>');
});
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()
});
}
/**
* <b ngNonBindable directive>
* <i>Hello {{ name }}!</i>
* </b>
*/
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('<b directive=""><i>Hello {{ name }}!</i></b>');
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()
});
}
/**
* <b ngNonBindable>
* <i directive>Hello {{ name }}!</i>
* </b>
*/
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('<b><i directive="">Hello {{ name }}!</i></b>');
expect(directiveInvoked).toEqual(false);
});
});
describe('Siblings update', () => { describe('Siblings update', () => {
it('should handle a flat list of static/bound text nodes', () => { it('should handle a flat list of static/bound text nodes', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {