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

View File

@ -5,7 +5,6 @@ import {
stringify, stringify,
assertionsEnabled, assertionsEnabled,
StringJoiner, StringJoiner,
RegExpWrapper,
serializeEnum, serializeEnum,
CONST_EXPR CONST_EXPR
} from 'angular2/src/facade/lang'; } 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 {Injectable} from 'angular2/src/core/di';
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer'; import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util'; 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 { export class HtmlTreeError extends ParseError {
static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError { static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError {
@ -134,7 +133,7 @@ class TreeBuilder {
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) { if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
this._advance(); this._advance();
selfClosing = true; selfClosing = true;
if (namespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) { if (getHtmlTagNamespacePrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create( this.errors.push(HtmlTreeError.create(
fullName, startTagToken.sourceSpan.start, fullName, startTagToken.sourceSpan.start,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`)); `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)) { if (isBlank(prefix)) {
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix; prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) { if (isBlank(prefix) && isPresent(parentElement)) {
prefix = namespacePrefix(parentElement.name); prefix = getHtmlTagNamespacePrefix(parentElement.name);
} }
} }
return mergeNsAndName(prefix, localName); 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];
}

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 http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json // 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()]; var result = TAG_DEFINITIONS[tagName.toLowerCase()];
return isPresent(result) ? result : DEFAULT_TAG_DEFINITION; 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];
}

View File

@ -1,10 +1,14 @@
import {Injectable} from 'angular2/src/core/di'; 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 {StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/platform/dom/dom_adapter'; import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {splitHtmlTagNamespace} from 'angular2/src/compiler/html_tags';
import {ElementSchemaRegistry} from './element_schema_registry'; 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() @Injectable()
export class DomElementSchemaRegistry extends ElementSchemaRegistry { export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _protoElements = new Map<string, Element>(); private _protoElements = new Map<string, Element>();
@ -12,7 +16,10 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _getProtoElement(tagName: string): Element { private _getProtoElement(tagName: string): Element {
var element = this._protoElements.get(tagName); var element = this._protoElements.get(tagName);
if (isBlank(element)) { 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); this._protoElements.set(tagName, element);
} }
return element; return element;

View File

@ -274,7 +274,7 @@ export class Parse5DomAdapter extends DomAdapter {
createElement(tagName): HTMLElement { createElement(tagName): HTMLElement {
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []); 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 { createTextNode(text: string): Text {
var t = <any>this.createComment(text); var t = <any>this.createComment(text);
t.type = 'text'; t.type = 'text';

View File

@ -40,5 +40,8 @@ export function main() {
expect(registry.getMappedPropName('title')).toEqual('title'); expect(registry.getMappedPropName('title')).toEqual('title');
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown'); expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
}); });
it('should detect properties on namespaced elements',
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
}); });
} }

View File

@ -0,0 +1,3 @@
library playground.e2e_test.svg.svg_spec;
main() {}

View File

@ -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');
});
});

View File

@ -31,6 +31,7 @@ transformers:
- web/src/routing/index.dart - web/src/routing/index.dart
- web/src/template_driven_forms/index.dart - web/src/template_driven_forms/index.dart
- web/src/zippy_component/index.dart - web/src/zippy_component/index.dart
- web/src/svg/index.dart
- web/src/material/button/index.dart - web/src/material/button/index.dart
- web/src/material/checkbox/index.dart - web/src/material/checkbox/index.dart
- web/src/material/dialog/index.dart - web/src/material/dialog/index.dart

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<title>SVG</title>
<body>
<svg-app>
Loading...
</svg-app>
$SCRIPTS$
</body>
</html>

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);
}

View File

@ -51,6 +51,7 @@ const kServedPaths = [
'playground/src/key_events', 'playground/src/key_events',
'playground/src/routing', 'playground/src/routing',
'playground/src/sourcemap', 'playground/src/sourcemap',
'playground/src/svg',
'playground/src/todo', 'playground/src/todo',
'playground/src/upgrade', 'playground/src/upgrade',
'playground/src/zippy_component', 'playground/src/zippy_component',