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 {appendChild} from '../node_manipulation';
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 {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {renderStringify} from '../util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared';
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.
*
* @param tNodeFlags node flags
* @param tNode
* @param direction whether to consider inputs or outputs
* @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 (ngDevMode) {
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) {
@ -884,24 +895,21 @@ function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
}
}
function setNgReflectProperties(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
value: any) {
for (let i = 0; i < inputs.length; i += 3) {
const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 2] as string);
const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ?
renderer.setAttribute((element as RElement), attrName, debugValue) :
(element as RElement).setAttribute(attrName, debugValue);
} else if (value !== undefined) {
const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`;
if (isProceduralRenderer(renderer)) {
renderer.setValue((element as RComment), value);
} else {
(element as RComment).textContent = value;
}
export function setNgReflectProperty(
lView: LView, element: RElement | RComment, type: TNodeType, attrName: string, value: any) {
const renderer = lView[RENDERER];
attrName = normalizeDebugBindingName(attrName);
const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ?
renderer.setAttribute((element as RElement), attrName, debugValue) :
(element as RElement).setAttribute(attrName, debugValue);
} else if (value !== undefined) {
const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`;
if (isProceduralRenderer(renderer)) {
renderer.setValue((element as RComment), value);
} else {
(element as RComment).textContent = value;
}
}
}
@ -1306,7 +1314,7 @@ function addComponentLogic<T>(
*
* @param directiveIndex Index of the directive in directives array
* @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
*/
function setInputsFromAttrs<T>(
@ -1328,6 +1336,11 @@ function setInputsFromAttrs<T>(
} else {
(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.
*
* @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.
* @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) :
native.setAttributeNS(namespaceURI, attrName, attrVal);
} else {
/// attrName is string;
// attrName is string;
const attrName = value as string;
const attrVal = attrs[++i];
// Standard attributes

View File

@ -1687,17 +1687,27 @@ function declareTests(config?: {useJit: boolean}) {
describe('logging property updates', () => {
it('should reflect property values as attributes', () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
const template = '<div>' +
'<div my-dir [elprop]="ctxProp"></div>' +
'</div>';
TestBed.overrideComponent(MyComp, {set: {template}});
TestBed.overrideComponent(
MyComp, {set: {template: `<div my-dir [elprop]="ctxProp"></div>`}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxProp = 'hello';
fixture.detectChanges();
expect(getDOM().getInnerHTML(fixture.nativeElement))
.toContain('ng-reflect-dir-prop="hello"');
const html = getDOM().getInnerHTML(fixture.nativeElement);
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 '$'`, () => {
@ -1705,23 +1715,54 @@ function declareTests(config?: {useJit: boolean}) {
const fixture = TestBed.createComponent(ParentCmp);
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', () => {
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent(
MyComp, {set: {template: '<ng-template [ngIf]="ctxBoolProp"></ng-template>'}})
MyComp, {set: {template: `<ng-template [ngIf]="ctxBoolProp"></ng-template>`}})
.createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges();
expect(getDOM().getInnerHTML(fixture.nativeElement))
.toContain('"ng\-reflect\-ng\-if"\: "true"');
const html = getDOM().getInnerHTML(fixture.nativeElement);
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', () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
const template = '<div my-dir [elprop]="toStringThrow"></div>';
@ -2019,6 +2060,12 @@ class MyDir {
constructor() { this.dirProp = ''; }
}
@Directive({selector: '[my-dir2]', inputs: ['dirProp2: elprop'], exportAs: 'mydir2'})
class MyDir2 {
dirProp2: string;
constructor() { this.dirProp2 = ''; }
}
@Directive({selector: '[title]', inputs: ['title']})
class DirectiveWithTitle {
// TODO(issue/24571): remove '!'.