fix(ivy): set ng-reflect properties for unbound directive inputs (#29973)

We only set ng-reflect properties on directive input bindings.
This PR ensures that we also add ng-reflect properties on unbound inputs for backwards compatibility.

FW-1266 #resolve

PR Close #29973
This commit is contained in:
Olivier Combe 2019-04-18 14:22:32 +02:00 committed by Ben Lesh
parent d9ce8a4ab5
commit 4e13700ad2
4 changed files with 94 additions and 35 deletions

View File

@ -18,14 +18,13 @@ import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view';
import {assertNodeType} from '../node_assert'; import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation'; import {appendChild} from '../node_manipulation';
import {applyOnCreateInstructions} from '../node_util'; import {applyOnCreateInstructions} from '../node_util';
import {decreaseElementDepthCount, getActiveDirectiveId, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state'; import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state';
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings'; import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings';
import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util'; import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils'; import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {renderStringify} from '../util/misc_utils'; import {renderStringify} from '../util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils'; import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared';
import {getActiveDirectiveStylingIndex} from './styling'; import {getActiveDirectiveStylingIndex} from './styling';

View File

@ -781,7 +781,7 @@ export function createTNode(
/** /**
* Consolidates all inputs or outputs of all directives on this logical node. * Consolidates all inputs or outputs of all directives on this logical node.
* *
* @param tNodeFlags node flags * @param tNode
* @param direction whether to consider inputs or outputs * @param direction whether to consider inputs or outputs
* @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise * @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise
*/ */
@ -842,7 +842,18 @@ export function elementPropertyInternal<T>(
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
if (ngDevMode) { if (ngDevMode) {
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
setNgReflectProperties(lView, element, tNode.type, dataValue, value); /**
* dataValue is an array containing runtime input or output names for the directives:
* i+0: directive instance index
* i+1: publicName
* i+2: privateName
*
* e.g. [0, 'change', 'change-minified']
* we want to set the reflected property with the privateName: dataValue[i+2]
*/
for (let i = 0; i < dataValue.length; i += 3) {
setNgReflectProperty(lView, element, tNode.type, dataValue[i + 2] as string, value);
}
} }
} }
} else if (tNode.type === TNodeType.Element) { } else if (tNode.type === TNodeType.Element) {
@ -884,12 +895,10 @@ function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
} }
} }
function setNgReflectProperties( export function setNgReflectProperty(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, lView: LView, element: RElement | RComment, type: TNodeType, attrName: string, value: any) {
value: any) {
for (let i = 0; i < inputs.length; i += 3) {
const renderer = lView[RENDERER]; const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 2] as string); attrName = normalizeDebugBindingName(attrName);
const debugValue = normalizeDebugBindingValue(value); const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) { if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
@ -904,7 +913,6 @@ function setNgReflectProperties(
} }
} }
} }
}
function validateAgainstUnknownProperties( function validateAgainstUnknownProperties(
hostView: LView, element: RElement | RComment, propName: string, tNode: TNode) { hostView: LView, element: RElement | RComment, propName: string, tNode: TNode) {
@ -1306,7 +1314,7 @@ function addComponentLogic<T>(
* *
* @param directiveIndex Index of the directive in directives array * @param directiveIndex Index of the directive in directives array
* @param instance Instance of the directive on which to set the initial inputs * @param instance Instance of the directive on which to set the initial inputs
* @param inputs The list of inputs from the directive def * @param def The directive def that contains the list of inputs
* @param tNode The static data for this node * @param tNode The static data for this node
*/ */
function setInputsFromAttrs<T>( function setInputsFromAttrs<T>(
@ -1328,6 +1336,11 @@ function setInputsFromAttrs<T>(
} else { } else {
(instance as any)[privateName] = value; (instance as any)[privateName] = value;
} }
if (ngDevMode) {
const lView = getLView();
const nativeElement = getNativeByTNode(tNode, lView) as RElement;
setNgReflectProperty(lView, nativeElement, tNode.type, privateName, value);
}
} }
} }
} }
@ -1772,7 +1785,7 @@ export function handleError(lView: LView, error: any): void {
* Set the inputs of directives at the current node to corresponding value. * Set the inputs of directives at the current node to corresponding value.
* *
* @param lView the `LView` which contains the directives. * @param lView the `LView` which contains the directives.
* @param inputAliases mapping between the public "input" name and privately-known, * @param inputs mapping between the public "input" name and privately-known,
* possibly minified, property names to write to. * possibly minified, property names to write to.
* @param value Value to set. * @param value Value to set.
*/ */

View File

@ -67,7 +67,7 @@ export function setUpAttributes(native: RElement, attrs: TAttributes): number {
(renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal, namespaceURI) : (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal, namespaceURI) :
native.setAttributeNS(namespaceURI, attrName, attrVal); native.setAttributeNS(namespaceURI, attrName, attrVal);
} else { } else {
/// attrName is string; // attrName is string;
const attrName = value as string; const attrName = value as string;
const attrVal = attrs[++i]; const attrVal = attrs[++i];
// Standard attributes // Standard attributes

View File

@ -1687,17 +1687,27 @@ function declareTests(config?: {useJit: boolean}) {
describe('logging property updates', () => { describe('logging property updates', () => {
it('should reflect property values as attributes', () => { it('should reflect property values as attributes', () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
const template = '<div>' + TestBed.overrideComponent(
'<div my-dir [elprop]="ctxProp"></div>' + MyComp, {set: {template: `<div my-dir [elprop]="ctxProp"></div>`}});
'</div>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = 'hello'; fixture.componentInstance.ctxProp = 'hello';
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().getInnerHTML(fixture.nativeElement)) const html = getDOM().getInnerHTML(fixture.nativeElement);
.toContain('ng-reflect-dir-prop="hello"'); expect(html).toContain('ng-reflect-dir-prop="hello"');
});
it('should reflect property values on unbound inputs', () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
TestBed.overrideComponent(
MyComp, {set: {template: `<div my-dir elprop="hello" title="Reflect test"></div>`}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const html = getDOM().getInnerHTML(fixture.nativeElement);
expect(html).toContain('ng-reflect-dir-prop="hello"');
expect(html).not.toContain('ng-reflect-title');
}); });
it(`should work with prop names containing '$'`, () => { it(`should work with prop names containing '$'`, () => {
@ -1705,21 +1715,52 @@ function declareTests(config?: {useJit: boolean}) {
const fixture = TestBed.createComponent(ParentCmp); const fixture = TestBed.createComponent(ParentCmp);
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('ng-reflect-test_="hello"'); const html = getDOM().getInnerHTML(fixture.nativeElement);
expect(html).toContain('ng-reflect-test_="hello"');
}); });
it('should reflect property values on template comments', () => { it('should reflect property values on template comments', () => {
const fixture = const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}) TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent( .overrideComponent(
MyComp, {set: {template: '<ng-template [ngIf]="ctxBoolProp"></ng-template>'}}) MyComp, {set: {template: `<ng-template [ngIf]="ctxBoolProp"></ng-template>`}})
.createComponent(MyComp); .createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true; fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().getInnerHTML(fixture.nativeElement)) const html = getDOM().getInnerHTML(fixture.nativeElement);
.toContain('"ng\-reflect\-ng\-if"\: "true"'); expect(html).toContain('"ng-reflect-ng-if": "true"');
});
it('should reflect property values on ng-containers', () => {
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent(
MyComp,
{set: {template: `<ng-container *ngIf="ctxBoolProp">content</ng-container>`}})
.createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges();
const html = getDOM().getInnerHTML(fixture.nativeElement);
expect(html).toContain('"ng-reflect-ng-if": "true"');
});
it('should reflect property values of multiple directive bound to the same input name',
() => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir, MyDir2]});
TestBed.overrideComponent(
MyComp, {set: {template: `<div my-dir my-dir2 [elprop]="ctxProp"></div>`}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = 'hello';
fixture.detectChanges();
const html = getDOM().getInnerHTML(fixture.nativeElement);
expect(html).toContain('ng-reflect-dir-prop="hello"');
expect(html).toContain('ng-reflect-dir-prop2="hello"');
}); });
it('should indicate when toString() throws', () => { it('should indicate when toString() throws', () => {
@ -2019,6 +2060,12 @@ class MyDir {
constructor() { this.dirProp = ''; } constructor() { this.dirProp = ''; }
} }
@Directive({selector: '[my-dir2]', inputs: ['dirProp2: elprop'], exportAs: 'mydir2'})
class MyDir2 {
dirProp2: string;
constructor() { this.dirProp2 = ''; }
}
@Directive({selector: '[title]', inputs: ['title']}) @Directive({selector: '[title]', inputs: ['title']})
class DirectiveWithTitle { class DirectiveWithTitle {
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.