diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index bc2ce97089..4a095580d2 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -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'; diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 4f08f9e80d..1ef5613bd1 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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( 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( * * @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( @@ -1328,6 +1336,11 @@ function setInputsFromAttrs( } 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. */ diff --git a/packages/core/src/render3/util/attrs_utils.ts b/packages/core/src/render3/util/attrs_utils.ts index ff9bdbf383..445404948a 100644 --- a/packages/core/src/render3/util/attrs_utils.ts +++ b/packages/core/src/render3/util/attrs_utils.ts @@ -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 diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 2bc52e3b2c..f689a89212 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -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 = '
' + - '
' + - '
'; - TestBed.overrideComponent(MyComp, {set: {template}}); + TestBed.overrideComponent( + MyComp, {set: {template: `
`}}); 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: `
`}}); + 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: ''}}) + MyComp, {set: {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: `content`}}) + .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: `
`}}); + 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 = '
'; @@ -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 '!'.