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,18 +1062,24 @@ 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) :
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, name, strValue, namespace);
} else {
namespace ? element.setAttributeNS(namespace, name, strValue) :
element.setAttribute(name, strValue); element.setAttribute(name, strValue);
} }
} }
} }
}
/** /**
* Update a property on an element. * Update a property on an element.

View File

@ -1832,8 +1832,7 @@ 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}});
@ -1874,8 +1873,7 @@ 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}});
@ -1886,8 +1884,7 @@ function declareTests(config?: {useJit: boolean}) {
.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}});

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,8 +579,7 @@ 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">' +