diff --git a/modules/@angular/core/test/linker/integration_spec.ts b/modules/@angular/core/test/linker/integration_spec.ts index 00730dd2d0..78a9cf1b9c 100644 --- a/modules/@angular/core/test/linker/integration_spec.ts +++ b/modules/@angular/core/test/linker/integration_spec.ts @@ -1507,6 +1507,14 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea .toContain('ng-reflect-dir-prop="hello"'); }); + it(`should work with prop names containing '$'`, () => { + TestBed.configureTestingModule({declarations: [ParentCmp, SomeCmpWithInput]}); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + + expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('ng-reflect-test$="hello"'); + }); + it('should reflect property values on template comments', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = ''; @@ -2300,3 +2308,16 @@ class DirectiveWithPropDecorators { class SomeCmp { value: any; } + +@Component({ + selector: 'parent-cmp', + template: ``, +}) +export class ParentCmp { + name: string = 'hello'; +} + +@Component({selector: 'cmp', template: ''}) +class SomeCmpWithInput { + @Input() test$: any; +} diff --git a/modules/@angular/platform-browser/src/dom/dom_renderer.ts b/modules/@angular/platform-browser/src/dom/dom_renderer.ts index 68de6d4d9d..26f5b90437 100644 --- a/modules/@angular/platform-browser/src/dom/dom_renderer.ts +++ b/modules/@angular/platform-browser/src/dom/dom_renderer.ts @@ -230,7 +230,14 @@ export class DomRenderer implements Renderer { renderElement.nodeValue = TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(parsedBindings, null, 2)); } else { - this.setElementAttribute(renderElement, propertyName, propertyValue); + // Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers + if (propertyName[propertyName.length - 1] === '$') { + const attrNode: Attr = createAttrNode(propertyName).cloneNode(true) as Attr; + attrNode.value = propertyValue; + renderElement.setAttributeNode(attrNode); + } else { + this.setElementAttribute(renderElement, propertyName, propertyValue); + } } } @@ -341,3 +348,20 @@ export function splitNamespace(name: string): string[] { const match = name.match(NS_PREFIX_RE); return [match[1], match[2]]; } + +let attrCache: Map; + +function createAttrNode(name: string): Attr { + if (!attrCache) { + attrCache = new Map(); + } + if (attrCache.has(name)) { + return attrCache.get(name); + } else { + const div = document.createElement('div'); + div.innerHTML = `
`; + const attr: Attr = div.firstChild.attributes[0]; + attrCache.set(name, attr); + return attr; + } +}