Skip to content

Commit daaa8ee

Browse files
committed
fix(compiler): support properties on SVG elements
Have DomElementSchemaRegistry support namespaced elements, so that it does not fail when directives are applied in SVG (or xlink). Without this fix, directives or property bindings cannot be used in SVG. Related to #5547 Closes #5653
1 parent 50490b5 commit daaa8ee

11 files changed

Lines changed: 89 additions & 15 deletions

File tree

modules/angular2/src/compiler/html_parser.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
stringify,
66
assertionsEnabled,
77
StringJoiner,
8-
RegExpWrapper,
98
serializeEnum,
109
CONST_EXPR
1110
} from 'angular2/src/facade/lang';
@@ -17,7 +16,7 @@ import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlElementAst} from './html_ast';
1716
import {Injectable} from 'angular2/src/core/di';
1817
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
1918
import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util';
20-
import {HtmlTagDefinition, getHtmlTagDefinition} from './html_tags';
19+
import {HtmlTagDefinition, getHtmlTagDefinition, getHtmlTagNamespacePrefix} from './html_tags';
2120

2221
export class HtmlTreeError extends ParseError {
2322
static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError {
@@ -134,7 +133,7 @@ class TreeBuilder {
134133
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
135134
this._advance();
136135
selfClosing = true;
137-
if (namespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
136+
if (getHtmlTagNamespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
138137
this.errors.push(HtmlTreeError.create(
139138
fullName, startTagToken.sourceSpan.start,
140139
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
@@ -237,16 +236,9 @@ function getElementFullName(prefix: string, localName: string,
237236
if (isBlank(prefix)) {
238237
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
239238
if (isBlank(prefix) && isPresent(parentElement)) {
240-
prefix = namespacePrefix(parentElement.name);
239+
prefix = getHtmlTagNamespacePrefix(parentElement.name);
241240
}
242241
}
243242

244243
return mergeNsAndName(prefix, localName);
245244
}
246-
247-
var NS_PREFIX_RE = /^@([^:]+)/g;
248-
249-
function namespacePrefix(elementName: string): string {
250-
var match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName);
251-
return isBlank(match) ? null : match[1];
252-
}

modules/angular2/src/compiler/html_tags.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import {isPresent, isBlank, normalizeBool, CONST_EXPR} from 'angular2/src/facade/lang';
1+
import {
2+
isPresent,
3+
isBlank,
4+
normalizeBool,
5+
RegExpWrapper,
6+
CONST_EXPR
7+
} from 'angular2/src/facade/lang';
28

39
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
410
// see https://html.spec.whatwg.org/multipage/entities.json
@@ -386,3 +392,17 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
386392
var result = TAG_DEFINITIONS[tagName.toLowerCase()];
387393
return isPresent(result) ? result : DEFAULT_TAG_DEFINITION;
388394
}
395+
396+
var NS_PREFIX_RE = /^@([^:]+):(.+)/g;
397+
398+
export function splitHtmlTagNamespace(elementName: string): string[] {
399+
if (elementName[0] != '@') {
400+
return [null, elementName];
401+
}
402+
let match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName);
403+
return [match[1], match[2]];
404+
}
405+
406+
export function getHtmlTagNamespacePrefix(elementName: string): string {
407+
return splitHtmlTagNamespace(elementName)[0];
408+
}

modules/angular2/src/compiler/schema/dom_element_schema_registry.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import {Injectable} from 'angular2/src/core/di';
2-
import {isPresent, isBlank} from 'angular2/src/facade/lang';
2+
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/facade/lang';
33
import {StringMapWrapper} from 'angular2/src/facade/collection';
44
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
5+
import {splitHtmlTagNamespace} from 'angular2/src/compiler/html_tags';
56

67
import {ElementSchemaRegistry} from './element_schema_registry';
78

9+
const NAMESPACE_URIS =
10+
CONST_EXPR({'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'});
11+
812
@Injectable()
913
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
1014
private _protoElements = new Map<string, Element>();
1115

1216
private _getProtoElement(tagName: string): Element {
1317
var element = this._protoElements.get(tagName);
1418
if (isBlank(element)) {
15-
element = DOM.createElement(tagName);
19+
var nsAndName = splitHtmlTagNamespace(tagName);
20+
element = isPresent(nsAndName[0]) ?
21+
DOM.createElementNS(NAMESPACE_URIS[nsAndName[0]], nsAndName[1]) :
22+
DOM.createElement(nsAndName[1]);
1623
this._protoElements.set(tagName, element);
1724
}
1825
return element;

modules/angular2/src/platform/server/parse5_adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export class Parse5DomAdapter extends DomAdapter {
274274
createElement(tagName): HTMLElement {
275275
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
276276
}
277-
createElementNS(ns, tagName): HTMLElement { throw 'not implemented'; }
277+
createElementNS(ns, tagName): HTMLElement { return treeAdapter.createElement(tagName, ns, []); }
278278
createTextNode(text: string): Text {
279279
var t = <any>this.createComment(text);
280280
t.type = 'text';

modules/angular2/test/compiler/schema/dom_element_schema_registry_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ export function main() {
4040
expect(registry.getMappedPropName('title')).toEqual('title');
4141
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
4242
});
43+
44+
it('should detect properties on namespaced elements',
45+
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
4346
});
4447
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
library playground.e2e_test.svg.svg_spec;
2+
3+
main() {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {verifyNoBrowserErrors} from 'angular2/src/testing/e2e_util';
2+
3+
describe('SVG', function() {
4+
5+
var URL = 'playground/src/svg/index.html';
6+
7+
afterEach(verifyNoBrowserErrors);
8+
beforeEach(() => { browser.get(URL); });
9+
10+
it('should display SVG component contents', function() {
11+
var svgText = element.all(by.css('g text')).get(0);
12+
expect(svgText.getText()).toEqual('Hello');
13+
});
14+
15+
});

modules/playground/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ transformers:
3131
- web/src/routing/index.dart
3232
- web/src/template_driven_forms/index.dart
3333
- web/src/zippy_component/index.dart
34+
- web/src/svg/index.dart
3435
- web/src/material/button/index.dart
3536
- web/src/material/checkbox/index.dart
3637
- web/src/material/dialog/index.dart
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!doctype html>
2+
<html>
3+
<title>SVG</title>
4+
<body>
5+
<svg-app>
6+
Loading...
7+
</svg-app>
8+
$SCRIPTS$
9+
</body>
10+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {bootstrap} from 'angular2/bootstrap';
2+
import {Component} from 'angular2/core';
3+
4+
@Component({selector: '[svg-group]', template: `<svg:text x="20" y="20">Hello</svg:text>`})
5+
class SvgGroup {
6+
}
7+
8+
9+
@Component({
10+
selector: 'svg-app',
11+
template: `<svg>
12+
<g svg-group></g>
13+
</svg>`,
14+
directives: [SvgGroup]
15+
})
16+
class SvgApp {
17+
}
18+
19+
20+
export function main() {
21+
bootstrap(SvgApp);
22+
}

0 commit comments

Comments
 (0)