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
This commit is contained in:
Tero Parviainen
2015-12-06 14:21:34 +02:00
parent 50490b55eb
commit daaa8ee1cd
11 changed files with 89 additions and 15 deletions
+3 -11
View File
@@ -5,7 +5,6 @@ import {
stringify,
assertionsEnabled,
StringJoiner,
RegExpWrapper,
serializeEnum,
CONST_EXPR
} from 'angular2/src/facade/lang';
@@ -17,7 +16,7 @@ import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlElementAst} from './html_ast';
import {Injectable} from 'angular2/src/core/di';
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util';
import {HtmlTagDefinition, getHtmlTagDefinition} from './html_tags';
import {HtmlTagDefinition, getHtmlTagDefinition, getHtmlTagNamespacePrefix} from './html_tags';
export class HtmlTreeError extends ParseError {
static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError {
@@ -134,7 +133,7 @@ class TreeBuilder {
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
if (namespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
if (getHtmlTagNamespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, startTagToken.sourceSpan.start,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
@@ -237,16 +236,9 @@ function getElementFullName(prefix: string, localName: string,
if (isBlank(prefix)) {
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) {
prefix = namespacePrefix(parentElement.name);
prefix = getHtmlTagNamespacePrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
var NS_PREFIX_RE = /^@([^:]+)/g;
function namespacePrefix(elementName: string): string {
var match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName);
return isBlank(match) ? null : match[1];
}
+21 -1
View File
@@ -1,4 +1,10 @@
import {isPresent, isBlank, normalizeBool, CONST_EXPR} from 'angular2/src/facade/lang';
import {
isPresent,
isBlank,
normalizeBool,
RegExpWrapper,
CONST_EXPR
} from 'angular2/src/facade/lang';
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json
@@ -386,3 +392,17 @@ export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
var result = TAG_DEFINITIONS[tagName.toLowerCase()];
return isPresent(result) ? result : DEFAULT_TAG_DEFINITION;
}
var NS_PREFIX_RE = /^@([^:]+):(.+)/g;
export function splitHtmlTagNamespace(elementName: string): string[] {
if (elementName[0] != '@') {
return [null, elementName];
}
let match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName);
return [match[1], match[2]];
}
export function getHtmlTagNamespacePrefix(elementName: string): string {
return splitHtmlTagNamespace(elementName)[0];
}
@@ -1,10 +1,14 @@
import {Injectable} from 'angular2/src/core/di';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {splitHtmlTagNamespace} from 'angular2/src/compiler/html_tags';
import {ElementSchemaRegistry} from './element_schema_registry';
const NAMESPACE_URIS =
CONST_EXPR({'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'});
@Injectable()
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _protoElements = new Map<string, Element>();
@@ -12,7 +16,10 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _getProtoElement(tagName: string): Element {
var element = this._protoElements.get(tagName);
if (isBlank(element)) {
element = DOM.createElement(tagName);
var nsAndName = splitHtmlTagNamespace(tagName);
element = isPresent(nsAndName[0]) ?
DOM.createElementNS(NAMESPACE_URIS[nsAndName[0]], nsAndName[1]) :
DOM.createElement(nsAndName[1]);
this._protoElements.set(tagName, element);
}
return element;
@@ -274,7 +274,7 @@ export class Parse5DomAdapter extends DomAdapter {
createElement(tagName): HTMLElement {
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
}
createElementNS(ns, tagName): HTMLElement { throw 'not implemented'; }
createElementNS(ns, tagName): HTMLElement { return treeAdapter.createElement(tagName, ns, []); }
createTextNode(text: string): Text {
var t = <any>this.createComment(text);
t.type = 'text';
@@ -40,5 +40,8 @@ export function main() {
expect(registry.getMappedPropName('title')).toEqual('title');
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
});
it('should detect properties on namespaced elements',
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
});
}
@@ -0,0 +1,3 @@
library playground.e2e_test.svg.svg_spec;
main() {}
@@ -0,0 +1,15 @@
import {verifyNoBrowserErrors} from 'angular2/src/testing/e2e_util';
describe('SVG', function() {
var URL = 'playground/src/svg/index.html';
afterEach(verifyNoBrowserErrors);
beforeEach(() => { browser.get(URL); });
it('should display SVG component contents', function() {
var svgText = element.all(by.css('g text')).get(0);
expect(svgText.getText()).toEqual('Hello');
});
});
+1
View File
@@ -31,6 +31,7 @@ transformers:
- web/src/routing/index.dart
- web/src/template_driven_forms/index.dart
- web/src/zippy_component/index.dart
- web/src/svg/index.dart
- web/src/material/button/index.dart
- web/src/material/checkbox/index.dart
- web/src/material/dialog/index.dart
+10
View File
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<title>SVG</title>
<body>
<svg-app>
Loading...
</svg-app>
$SCRIPTS$
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
import {bootstrap} from 'angular2/bootstrap';
import {Component} from 'angular2/core';
@Component({selector: '[svg-group]', template: `<svg:text x="20" y="20">Hello</svg:text>`})
class SvgGroup {
}
@Component({
selector: 'svg-app',
template: `<svg>
<g svg-group></g>
</svg>`,
directives: [SvgGroup]
})
class SvgApp {
}
export function main() {
bootstrap(SvgApp);
}
+1
View File
@@ -51,6 +51,7 @@ const kServedPaths = [
'playground/src/key_events',
'playground/src/routing',
'playground/src/sourcemap',
'playground/src/svg',
'playground/src/todo',
'playground/src/upgrade',
'playground/src/zippy_component',