diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index fa4bc709fe..c8fd92d270 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -242,11 +242,11 @@ class HtmlAstToIvyAst implements html.Visitor { literal.push(new t.TextAttribute( prop.name, prop.expression.source || '', prop.sourceSpan, undefined, i18n)); } else { - // we skip validation here, since we do this check at runtime due to the fact that we need - // to make sure a given prop is not an input of some Directive (thus should not be a subject - // of this check) and Directive matching happens at runtime + // Note that validation is skipped and property mapping is disabled + // due to the fact that we need to make sure a given prop is not an + // input of a directive and directive matching happens at runtime. const bep = this.bindingParser.createBoundElementProperty( - elementName, prop, /* skipValidation */ true); + elementName, prop, /* skipValidation */ true, /* mapPropertyName */ false); bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n)); } }); diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 71d786bb96..1940825967 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -240,8 +240,8 @@ export class BindingParser { } createBoundElementProperty( - elementSelector: string, boundProp: ParsedProperty, - skipValidation: boolean = false): BoundElementProperty { + elementSelector: string, boundProp: ParsedProperty, skipValidation: boolean = false, + mapPropertyName: boolean = true): BoundElementProperty { if (boundProp.isAnimation) { return new BoundElementProperty( 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 (boundPropertyName === null) { - boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name); + const mappedPropName = this._schemaRegistry.getMappedPropName(boundProp.name); + boundPropertyName = mapPropertyName ? mappedPropName : boundProp.name; securityContexts = calcPossibleSecurityContexts( - this._schemaRegistry, elementSelector, boundPropertyName, false); + this._schemaRegistry, elementSelector, mappedPropName, false); bindingType = BindingType.Property; if (!skipValidation) { - this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); + this._validatePropertyOrAttributeName(mappedPropName, boundProp.sourceSpan, false); } } @@ -455,4 +456,4 @@ export function calcPossibleSecurityContexts( elementName => registry.securityContext(elementName, propName, isAttribute))); }); return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort(); -} \ No newline at end of file +} diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts index 5e8a541b2a..d41d598517 100644 --- a/packages/compiler/test/render3/r3_template_transform_spec.ts +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -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('
').toEqual([ ['Element', 'div'], - ['BoundAttribute', BindingType.Property, 'mappedProp', 'v'], + ['BoundAttribute', BindingType.Property, 'mappedAttr', 'v'], ]); }); @@ -499,4 +499,4 @@ describe('R3 template transform', () => { ]); }); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 2c8b009b06..40fd1912bd 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -1213,6 +1213,18 @@ export function componentHostSyntheticProperty( 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( index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean, @@ -1233,6 +1245,8 @@ function elementPropertyInternal( } } } else if (tNode.type === TNodeType.Element) { + propName = ATTR_TO_PROP[propName] || propName; + if (ngDevMode) { validateAgainstEventProperties(propName); validateAgainstUnknownProperties(lView, element, propName, tNode); diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index bec6fedfe0..08225ab963 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -5,6 +5,9 @@ { "name": "ANIMATION_PROP_PREFIX" }, + { + "name": "ATTR_TO_PROP" + }, { "name": "BINDING_INDEX" }, diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts index 808901d036..1a9239a423 100644 --- a/packages/core/test/render3/properties_spec.ts +++ b/packages/core/test/render3/properties_spec.ts @@ -11,7 +11,6 @@ import {EventEmitter} from '@angular/core'; 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 {RenderFlags} from '../../src/render3/interfaces/definition'; -import {NO_CHANGE} from '../../src/render3/tokens'; import {ComponentFixture, createComponent, renderToHtml} from './render_util'; @@ -77,6 +76,26 @@ describe('elementProperty', () => { expect(fixture.html).toEqual(''); }); + 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(''); + + fixture.component.forValue = 'some-textarea'; + fixture.update(); + expect(fixture.html).toEqual(''); + }); + describe('input properties', () => { let button: MyButton; let otherDir: OtherDir; @@ -365,6 +384,50 @@ describe('elementProperty', () => { 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'} + }); + } + + /** */ + 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(``); + expect(comp !.for).toEqual('hello'); + + fixture.component.forValue = 'hej'; + fixture.update(); + expect(fixture.html).toEqual(``); + expect(comp !.for).toEqual('hej'); + }); + }); describe('attributes and input properties', () => {