From 93a7836f7a580ed7e45defb07c4fecf0b2955561 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 15 Feb 2019 21:55:07 +0100 Subject: [PATCH] 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 --- .../src/render3/r3_template_transform.ts | 8 +-- .../src/template_parser/binding_parser.ts | 13 ++-- .../render3/r3_template_transform_spec.ts | 6 +- packages/core/src/render3/instructions.ts | 14 ++++ .../bundling/todo/bundle.golden_symbols.json | 3 + packages/core/test/render3/properties_spec.ts | 65 ++++++++++++++++++- 6 files changed, 95 insertions(+), 14 deletions(-) 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', () => {