fix(ivy): incorrectly remapping certain properties that refer inputs (#28765)
During build time we remap particular property bindings, because their names don't match their attribute equivalents (e.g. the property for the `for` attribute is called `htmlFor`). This breaks down if the particular element has an input that has the same name, because the property gets mapped to something invalid. The following changes address the issue by mapping the name during runtime, because that's when directives are resolved and we know all of the inputs that are associated with a particular element. PR Close #28765
This commit is contained in:
parent
9defc00b14
commit
93a7836f7a
|
@ -242,11 +242,11 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
literal.push(new t.TextAttribute(
|
literal.push(new t.TextAttribute(
|
||||||
prop.name, prop.expression.source || '', prop.sourceSpan, undefined, i18n));
|
prop.name, prop.expression.source || '', prop.sourceSpan, undefined, i18n));
|
||||||
} else {
|
} else {
|
||||||
// we skip validation here, since we do this check at runtime due to the fact that we need
|
// Note that validation is skipped and property mapping is disabled
|
||||||
// to make sure a given prop is not an input of some Directive (thus should not be a subject
|
// due to the fact that we need to make sure a given prop is not an
|
||||||
// of this check) and Directive matching happens at runtime
|
// input of a directive and directive matching happens at runtime.
|
||||||
const bep = this.bindingParser.createBoundElementProperty(
|
const bep = this.bindingParser.createBoundElementProperty(
|
||||||
elementName, prop, /* skipValidation */ true);
|
elementName, prop, /* skipValidation */ true, /* mapPropertyName */ false);
|
||||||
bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n));
|
bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -240,8 +240,8 @@ export class BindingParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
createBoundElementProperty(
|
createBoundElementProperty(
|
||||||
elementSelector: string, boundProp: ParsedProperty,
|
elementSelector: string, boundProp: ParsedProperty, skipValidation: boolean = false,
|
||||||
skipValidation: boolean = false): BoundElementProperty {
|
mapPropertyName: boolean = true): BoundElementProperty {
|
||||||
if (boundProp.isAnimation) {
|
if (boundProp.isAnimation) {
|
||||||
return new BoundElementProperty(
|
return new BoundElementProperty(
|
||||||
boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null,
|
boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null,
|
||||||
|
@ -286,12 +286,13 @@ export class BindingParser {
|
||||||
|
|
||||||
// If not a special case, use the full property name
|
// If not a special case, use the full property name
|
||||||
if (boundPropertyName === null) {
|
if (boundPropertyName === null) {
|
||||||
boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name);
|
const mappedPropName = this._schemaRegistry.getMappedPropName(boundProp.name);
|
||||||
|
boundPropertyName = mapPropertyName ? mappedPropName : boundProp.name;
|
||||||
securityContexts = calcPossibleSecurityContexts(
|
securityContexts = calcPossibleSecurityContexts(
|
||||||
this._schemaRegistry, elementSelector, boundPropertyName, false);
|
this._schemaRegistry, elementSelector, mappedPropName, false);
|
||||||
bindingType = BindingType.Property;
|
bindingType = BindingType.Property;
|
||||||
if (!skipValidation) {
|
if (!skipValidation) {
|
||||||
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
|
this._validatePropertyOrAttributeName(mappedPropName, boundProp.sourceSpan, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -176,10 +176,10 @@ describe('R3 template transform', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should normalize property names via the element schema', () => {
|
it('should not normalize property names via the element schema', () => {
|
||||||
expectFromHtml('<div [mappedAttr]="v"></div>').toEqual([
|
expectFromHtml('<div [mappedAttr]="v"></div>').toEqual([
|
||||||
['Element', 'div'],
|
['Element', 'div'],
|
||||||
['BoundAttribute', BindingType.Property, 'mappedProp', 'v'],
|
['BoundAttribute', BindingType.Property, 'mappedAttr', 'v'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1213,6 +1213,18 @@ export function componentHostSyntheticProperty<T>(
|
||||||
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer);
|
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping between attributes names that don't correspond to their element property names.
|
||||||
|
*/
|
||||||
|
const ATTR_TO_PROP: {[name: string]: string} = {
|
||||||
|
'class': 'className',
|
||||||
|
'for': 'htmlFor',
|
||||||
|
'formaction': 'formAction',
|
||||||
|
'innerHtml': 'innerHTML',
|
||||||
|
'readonly': 'readOnly',
|
||||||
|
'tabindex': 'tabIndex',
|
||||||
|
};
|
||||||
|
|
||||||
function elementPropertyInternal<T>(
|
function elementPropertyInternal<T>(
|
||||||
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
||||||
nativeOnly?: boolean,
|
nativeOnly?: boolean,
|
||||||
|
@ -1233,6 +1245,8 @@ function elementPropertyInternal<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (tNode.type === TNodeType.Element) {
|
} else if (tNode.type === TNodeType.Element) {
|
||||||
|
propName = ATTR_TO_PROP[propName] || propName;
|
||||||
|
|
||||||
if (ngDevMode) {
|
if (ngDevMode) {
|
||||||
validateAgainstEventProperties(propName);
|
validateAgainstEventProperties(propName);
|
||||||
validateAgainstUnknownProperties(lView, element, propName, tNode);
|
validateAgainstUnknownProperties(lView, element, propName, tNode);
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
{
|
{
|
||||||
"name": "ANIMATION_PROP_PREFIX"
|
"name": "ANIMATION_PROP_PREFIX"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ATTR_TO_PROP"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "BINDING_INDEX"
|
"name": "BINDING_INDEX"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {EventEmitter} from '@angular/core';
|
||||||
import {defineComponent, defineDirective} from '../../src/render3/index';
|
import {defineComponent, defineDirective} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, listener, load, reference, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, listener, load, reference, text, textBinding} from '../../src/render3/instructions';
|
||||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
import {NO_CHANGE} from '../../src/render3/tokens';
|
|
||||||
|
|
||||||
import {ComponentFixture, createComponent, renderToHtml} from './render_util';
|
import {ComponentFixture, createComponent, renderToHtml} from './render_util';
|
||||||
|
|
||||||
|
@ -77,6 +76,26 @@ describe('elementProperty', () => {
|
||||||
expect(fixture.html).toEqual('<span id="_otherId_"></span>');
|
expect(fixture.html).toEqual('<span id="_otherId_"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should map properties whose names do not correspond to their attribute names', () => {
|
||||||
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
element(0, 'label');
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(0, 'for', bind(ctx.forValue));
|
||||||
|
}
|
||||||
|
}, 1, 1);
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(App);
|
||||||
|
fixture.component.forValue = 'some-input';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<label for="some-input"></label>');
|
||||||
|
|
||||||
|
fixture.component.forValue = 'some-textarea';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<label for="some-textarea"></label>');
|
||||||
|
});
|
||||||
|
|
||||||
describe('input properties', () => {
|
describe('input properties', () => {
|
||||||
let button: MyButton;
|
let button: MyButton;
|
||||||
let otherDir: OtherDir;
|
let otherDir: OtherDir;
|
||||||
|
@ -365,6 +384,50 @@ describe('elementProperty', () => {
|
||||||
expect(otherDir !.id).toEqual(3);
|
expect(otherDir !.id).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not map properties whose names do not correspond to their attribute names, ' +
|
||||||
|
'if they correspond to inputs',
|
||||||
|
() => {
|
||||||
|
let comp: Comp;
|
||||||
|
|
||||||
|
class Comp {
|
||||||
|
// TODO(issue/24571): remove '!'.
|
||||||
|
// clang-format off
|
||||||
|
for !: string;
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: Comp,
|
||||||
|
selectors: [['comp']],
|
||||||
|
consts: 0,
|
||||||
|
vars: 0,
|
||||||
|
template: function(rf: RenderFlags, ctx: any) {},
|
||||||
|
factory: () => comp = new Comp(),
|
||||||
|
inputs: {for: 'for'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** <comp [for]="forValue"></comp> */
|
||||||
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
element(0, 'comp');
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(0, 'for', bind(ctx.forValue));
|
||||||
|
}
|
||||||
|
}, 1, 1, [Comp]);
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(App);
|
||||||
|
fixture.component.forValue = 'hello';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual(`<comp></comp>`);
|
||||||
|
expect(comp !.for).toEqual('hello');
|
||||||
|
|
||||||
|
fixture.component.forValue = 'hej';
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual(`<comp></comp>`);
|
||||||
|
expect(comp !.for).toEqual('hej');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('attributes and input properties', () => {
|
describe('attributes and input properties', () => {
|
||||||
|
|
Loading…
Reference in New Issue