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:
parent
d9ce8a4ab5
commit
4e13700ad2
|
@ -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';
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '!'.
|
||||
|
|
Loading…
Reference in New Issue