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:
parent
50490b55eb
commit
daaa8ee1cd
|
@ -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];
|
|
||||||
}
|
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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(); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<title>SVG</title>
|
||||||
|
<body>
|
||||||
|
<svg-app>
|
||||||
|
Loading...
|
||||||
|
</svg-app>
|
||||||
|
$SCRIPTS$
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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);
|
||||||
|
}
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue