diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 9d595b684ec4..fc9c9a234daa 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -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]; -} diff --git a/modules/angular2/src/compiler/html_tags.ts b/modules/angular2/src/compiler/html_tags.ts index 1253b94ae204..952eca4e52d4 100644 --- a/modules/angular2/src/compiler/html_tags.ts +++ b/modules/angular2/src/compiler/html_tags.ts @@ -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 @@ -381,3 +387,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]; +} diff --git a/modules/angular2/src/compiler/schema/dom_element_schema_registry.ts b/modules/angular2/src/compiler/schema/dom_element_schema_registry.ts index 86d825022d2a..751e420a4300 100644 --- a/modules/angular2/src/compiler/schema/dom_element_schema_registry.ts +++ b/modules/angular2/src/compiler/schema/dom_element_schema_registry.ts @@ -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(); @@ -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; diff --git a/modules/angular2/src/platform/server/parse5_adapter.ts b/modules/angular2/src/platform/server/parse5_adapter.ts index 9621e04f8214..78d45a47fdeb 100644 --- a/modules/angular2/src/platform/server/parse5_adapter.ts +++ b/modules/angular2/src/platform/server/parse5_adapter.ts @@ -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 = this.createComment(text); t.type = 'text'; diff --git a/modules/angular2/test/compiler/schema/dom_element_schema_registry_spec.ts b/modules/angular2/test/compiler/schema/dom_element_schema_registry_spec.ts index 2b2654074231..57c32b8a7660 100644 --- a/modules/angular2/test/compiler/schema/dom_element_schema_registry_spec.ts +++ b/modules/angular2/test/compiler/schema/dom_element_schema_registry_spec.ts @@ -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(); }); }); } diff --git a/modules/playground/e2e_test/svg/svg_spec.dart b/modules/playground/e2e_test/svg/svg_spec.dart new file mode 100644 index 000000000000..2d253782acf7 --- /dev/null +++ b/modules/playground/e2e_test/svg/svg_spec.dart @@ -0,0 +1,3 @@ +library playground.e2e_test.svg.svg_spec; + +main() {} diff --git a/modules/playground/e2e_test/svg/svg_spec.ts b/modules/playground/e2e_test/svg/svg_spec.ts new file mode 100644 index 000000000000..40bb4958e061 --- /dev/null +++ b/modules/playground/e2e_test/svg/svg_spec.ts @@ -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'); + }); + +}); diff --git a/modules/playground/pubspec.yaml b/modules/playground/pubspec.yaml index 97d9bdd140be..35538a25aa09 100644 --- a/modules/playground/pubspec.yaml +++ b/modules/playground/pubspec.yaml @@ -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 diff --git a/modules/playground/src/svg/index.html b/modules/playground/src/svg/index.html new file mode 100644 index 000000000000..9b2f3abd40e9 --- /dev/null +++ b/modules/playground/src/svg/index.html @@ -0,0 +1,10 @@ + + + SVG + + + Loading... + + $SCRIPTS$ + + diff --git a/modules/playground/src/svg/index.ts b/modules/playground/src/svg/index.ts new file mode 100644 index 000000000000..e87641bc8abf --- /dev/null +++ b/modules/playground/src/svg/index.ts @@ -0,0 +1,22 @@ +import {bootstrap} from 'angular2/bootstrap'; +import {Component} from 'angular2/core'; + +@Component({selector: '[svg-group]', template: `Hello`}) +class SvgGroup { +} + + +@Component({ + selector: 'svg-app', + template: ` + + `, + directives: [SvgGroup] +}) +class SvgApp { +} + + +export function main() { + bootstrap(SvgApp); +} diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 51d2a85bbba3..443808e459ea 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -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',