fix(ivy): handle namespaces in attributes (#28242)

Adds handling for namespaced attributes when generating the template and in the `elementAttribute` instruction.

PR Close #28242
This commit is contained in:
Kristiyan Kostadinov 2019-01-22 23:21:53 +01:00 committed by Alex Rickabaugh
parent 03c8528fcb
commit 9f9024b7a1
5 changed files with 101 additions and 65 deletions

View File

@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata';
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import * as core from '../../core'; import * as core from '../../core';
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEvent, ParsedEventType, PropertyRead} from '../../expression_parser/ast'; import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead} from '../../expression_parser/ast';
import {Lexer} from '../../expression_parser/lexer'; import {Lexer} from '../../expression_parser/lexer';
import {Parser} from '../../expression_parser/parser'; import {Parser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast'; import * as i18n from '../../i18n/i18n_ast';
@ -567,7 +567,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
}); });
outputAttrs.forEach(attr => attributes.push(o.literal(attr.name), o.literal(attr.value))); outputAttrs.forEach(attr => {
attributes.push(...getAttributeNameLiterals(attr.name), o.literal(attr.value));
});
// this will build the instructions so that they fall into the following syntax // this will build the instructions so that they fall into the following syntax
// add attributes for directive matching purposes // add attributes for directive matching purposes
@ -719,13 +721,25 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const value = input.value.visit(this._valueConverter); const value = input.value.visit(this._valueConverter);
if (value !== undefined) { if (value !== undefined) {
const params: any[] = []; const params: any[] = [];
const [attrNamespace, attrName] = splitNsName(input.name);
const isAttributeBinding = input.type === BindingType.Attribute; const isAttributeBinding = input.type === BindingType.Attribute;
const sanitizationRef = resolveSanitizationFn(input.securityContext, isAttributeBinding); const sanitizationRef = resolveSanitizationFn(input.securityContext, isAttributeBinding);
if (sanitizationRef) params.push(sanitizationRef); if (sanitizationRef) params.push(sanitizationRef);
if (attrNamespace) {
const namespaceLiteral = o.literal(attrNamespace);
if (sanitizationRef) {
params.push(namespaceLiteral);
} else {
// If there wasn't a sanitization ref, we need to add
// an extra param so that we can pass in the namespace.
params.push(o.literal(null), namespaceLiteral);
}
}
this.allocateBindingSlots(value); this.allocateBindingSlots(value);
this.updateInstruction(input.sourceSpan, instruction, () => { this.updateInstruction(input.sourceSpan, instruction, () => {
return [ return [
o.literal(elementIndex), o.literal(input.name), o.literal(elementIndex), o.literal(attrName),
this.convertPropertyBinding(implicit, value), ...params this.convertPropertyBinding(implicit, value), ...params
]; ];
}); });
@ -1038,10 +1052,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
function addAttrExpr(key: string | number, value?: o.Expression): void { function addAttrExpr(key: string | number, value?: o.Expression): void {
if (typeof key === 'string') { if (typeof key === 'string') {
if (!alreadySeen.has(key)) { if (!alreadySeen.has(key)) {
attrExprs.push(o.literal(key)); attrExprs.push(...getAttributeNameLiterals(key));
if (value !== undefined) { value !== undefined && attrExprs.push(value);
attrExprs.push(value);
}
alreadySeen.add(key); alreadySeen.add(key);
} }
} else { } else {
@ -1264,6 +1276,26 @@ function getLiteralFactory(
return o.importExpr(identifier).callFn(args); return o.importExpr(identifier).callFn(args);
} }
/**
* Gets an array of literals that can be added to an expression
* to represent the name and namespace of an attribute. E.g.
* `:xlink:href` turns into `[AttributeMarker.NamespaceURI, 'xlink', 'href']`.
*
* @param name Name of the attribute, including the namespace.
*/
function getAttributeNameLiterals(name: string): o.LiteralExpr[] {
const [attributeNamespace, attributeName] = splitNsName(name);
const nameLiteral = o.literal(attributeName);
if (attributeNamespace) {
return [
o.literal(core.AttributeMarker.NamespaceURI), o.literal(attributeNamespace), nameLiteral
];
}
return [nameLiteral];
}
/** /**
* Function which is executed whenever a variable is referenced for the first time in a given * Function which is executed whenever a variable is referenced for the first time in a given
* scope. * scope.

View File

@ -1050,9 +1050,11 @@ export function elementEnd(): void {
* @param value value The attribute is removed when value is `null` or `undefined`. * @param value value The attribute is removed when value is `null` or `undefined`.
* Otherwise the attribute value is set to the stringified value. * Otherwise the attribute value is set to the stringified value.
* @param sanitizer An optional function used to sanitize the value. * @param sanitizer An optional function used to sanitize the value.
* @param namespace Optional namespace to use when setting the attribute.
*/ */
export function elementAttribute( export function elementAttribute(
index: number, name: string, value: any, sanitizer?: SanitizerFn | null): void { index: number, name: string, value: any, sanitizer?: SanitizerFn | null,
namespace?: string): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
ngDevMode && validateAttribute(name); ngDevMode && validateAttribute(name);
const lView = getLView(); const lView = getLView();
@ -1060,15 +1062,21 @@ export function elementAttribute(
const element = getNativeByIndex(index, lView); const element = getNativeByIndex(index, lView);
if (value == null) { if (value == null) {
ngDevMode && ngDevMode.rendererRemoveAttribute++; ngDevMode && ngDevMode.rendererRemoveAttribute++;
isProceduralRenderer(renderer) ? renderer.removeAttribute(element, name) : isProceduralRenderer(renderer) ? renderer.removeAttribute(element, name, namespace) :
element.removeAttribute(name); element.removeAttribute(name);
} else { } else {
ngDevMode && ngDevMode.rendererSetAttribute++; ngDevMode && ngDevMode.rendererSetAttribute++;
const tNode = getTNode(index, lView); const tNode = getTNode(index, lView);
const strValue = const strValue =
sanitizer == null ? renderStringify(value) : sanitizer(value, tNode.tagName || '', name); sanitizer == null ? renderStringify(value) : sanitizer(value, tNode.tagName || '', name);
isProceduralRenderer(renderer) ? renderer.setAttribute(element, name, strValue) :
element.setAttribute(name, strValue);
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, name, strValue, namespace);
} else {
namespace ? element.setAttributeNS(namespace, name, strValue) :
element.setAttribute(name, strValue);
}
} }
} }
} }

View File

@ -1832,25 +1832,24 @@ function declareTests(config?: {useJit: boolean}) {
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {
describe('svg', () => { describe('svg', () => {
fixmeIvy('FW-672: SVG attribute xlink:href is output as :xlink:href (extra ":")') it('should support svg elements', () => {
.it('should support svg elements', () => { TestBed.configureTestingModule({declarations: [MyComp]});
TestBed.configureTestingModule({declarations: [MyComp]}); const template = '<svg><use xlink:href="Port" /></svg>';
const template = '<svg><use xlink:href="Port" /></svg>'; TestBed.overrideComponent(MyComp, {set: {template}});
TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp);
const fixture = TestBed.createComponent(MyComp);
const el = fixture.nativeElement; const el = fixture.nativeElement;
const svg = getDOM().childNodes(el)[0]; const svg = getDOM().childNodes(el)[0];
const use = getDOM().childNodes(svg)[0]; const use = getDOM().childNodes(svg)[0];
expect(getDOM().getProperty(<Element>svg, 'namespaceURI')) expect(getDOM().getProperty(<Element>svg, 'namespaceURI'))
.toEqual('http://www.w3.org/2000/svg'); .toEqual('http://www.w3.org/2000/svg');
expect(getDOM().getProperty(<Element>use, 'namespaceURI')) expect(getDOM().getProperty(<Element>use, 'namespaceURI'))
.toEqual('http://www.w3.org/2000/svg'); .toEqual('http://www.w3.org/2000/svg');
const firstAttribute = getDOM().getProperty(<Element>use, 'attributes')[0]; const firstAttribute = getDOM().getProperty(<Element>use, 'attributes')[0];
expect(firstAttribute.name).toEqual('xlink:href'); expect(firstAttribute.name).toEqual('xlink:href');
expect(firstAttribute.namespaceURI).toEqual('http://www.w3.org/1999/xlink'); expect(firstAttribute.namespaceURI).toEqual('http://www.w3.org/1999/xlink');
}); });
it('should support foreignObjects with document fragments', () => { it('should support foreignObjects with document fragments', () => {
TestBed.configureTestingModule({declarations: [MyComp]}); TestBed.configureTestingModule({declarations: [MyComp]});
@ -1874,40 +1873,38 @@ function declareTests(config?: {useJit: boolean}) {
describe('attributes', () => { describe('attributes', () => {
fixmeIvy('FW-672: SVG attribute xlink:href is output as :xlink:href (extra ":")') it('should support attributes with namespace', () => {
.it('should support attributes with namespace', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]});
TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]}); const template = '<svg:use xlink:href="#id" />';
const template = '<svg:use xlink:href="#id" />'; TestBed.overrideComponent(SomeCmp, {set: {template}});
TestBed.overrideComponent(SomeCmp, {set: {template}}); const fixture = TestBed.createComponent(SomeCmp);
const fixture = TestBed.createComponent(SomeCmp);
const useEl = getDOM().firstChild(fixture.nativeElement); const useEl = getDOM().firstChild(fixture.nativeElement);
expect(getDOM().getAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href')) expect(getDOM().getAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href'))
.toEqual('#id'); .toEqual('#id');
}); });
fixmeIvy('FW-672: SVG attribute xlink:href is output as :xlink:href (extra ":")') it('should support binding to attributes with namespace', () => {
.it('should support binding to attributes with namespace', () => { TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]});
TestBed.configureTestingModule({declarations: [MyComp, SomeCmp]}); const template = '<svg:use [attr.xlink:href]="value" />';
const template = '<svg:use [attr.xlink:href]="value" />'; TestBed.overrideComponent(SomeCmp, {set: {template}});
TestBed.overrideComponent(SomeCmp, {set: {template}}); const fixture = TestBed.createComponent(SomeCmp);
const fixture = TestBed.createComponent(SomeCmp);
const cmp = fixture.componentInstance; const cmp = fixture.componentInstance;
const useEl = getDOM().firstChild(fixture.nativeElement); const useEl = getDOM().firstChild(fixture.nativeElement);
cmp.value = '#id'; cmp.value = '#id';
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().getAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href')) expect(getDOM().getAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href'))
.toEqual('#id'); .toEqual('#id');
cmp.value = null; cmp.value = null;
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().hasAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href')) expect(getDOM().hasAttributeNS(useEl, 'http://www.w3.org/1999/xlink', 'href'))
.toEqual(false); .toEqual(false);
}); });
}); });
} }
}); });

View File

@ -394,7 +394,7 @@ class MockRenderer implements ProceduralRenderer3 {
destroy(): void {} destroy(): void {}
createComment(value: string): RComment { return document.createComment(value); } createComment(value: string): RComment { return document.createComment(value); }
createElement(name: string, namespace?: string|null): RElement { createElement(name: string, namespace?: string|null): RElement {
return document.createElement(name); return namespace ? document.createElementNS(namespace, name) : document.createElement(name);
} }
createText(value: string): RText { return document.createTextNode(value); } createText(value: string): RText { return document.createTextNode(value); }
appendChild(parent: RElement, newChild: RNode): void { parent.appendChild(newChild); } appendChild(parent: RElement, newChild: RNode): void { parent.appendChild(newChild); }

View File

@ -579,15 +579,14 @@ class HiddenModule {
}); });
}))); })));
fixmeIvy('FW-672: SVG xlink:href is sanitized to :xlink:href (extra ":")') it('works with SVG elements', async(() => {
.it('works with SVG elements', async(() => { renderModule(SVGServerModule, {document: doc}).then(output => {
renderModule(SVGServerModule, {document: doc}).then(output => { expect(output).toBe(
expect(output).toBe( '<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' + '<svg><use xlink:href="#clear"></use></svg></app></body></html>');
'<svg><use xlink:href="#clear"></use></svg></app></body></html>'); called = true;
called = true; });
}); }));
}));
it('works with animation', async(() => { it('works with animation', async(() => {
renderModule(AnimationServerModule, {document: doc}).then(output => { renderModule(AnimationServerModule, {document: doc}).then(output => {