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:
parent
03c8528fcb
commit
9f9024b7a1
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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); }
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Reference in New Issue