From daaa8ee1cd35d45bbd52f6c79a9d6fe05be54e3f Mon Sep 17 00:00:00 2001 From: Tero Parviainen Date: Sun, 6 Dec 2015 14:21:34 +0200 Subject: [PATCH] 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 --- modules/angular2/src/compiler/html_parser.ts | 14 +++--------- modules/angular2/src/compiler/html_tags.ts | 22 ++++++++++++++++++- .../schema/dom_element_schema_registry.ts | 11 ++++++++-- .../src/platform/server/parse5_adapter.ts | 2 +- .../dom_element_schema_registry_spec.ts | 3 +++ modules/playground/e2e_test/svg/svg_spec.dart | 3 +++ modules/playground/e2e_test/svg/svg_spec.ts | 15 +++++++++++++ modules/playground/pubspec.yaml | 1 + modules/playground/src/svg/index.html | 10 +++++++++ modules/playground/src/svg/index.ts | 22 +++++++++++++++++++ tools/broccoli/trees/browser_tree.ts | 1 + 11 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 modules/playground/e2e_test/svg/svg_spec.dart create mode 100644 modules/playground/e2e_test/svg/svg_spec.ts create mode 100644 modules/playground/src/svg/index.html create mode 100644 modules/playground/src/svg/index.ts diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 9d595b684e..fc9c9a234d 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 e393a0da5c..a2b77fef3d 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 @@ -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]; +} 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 86d825022d..751e420a43 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 9621e04f82..78d45a47fd 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 2b26540742..57c32b8a76 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 0000000000..2d253782ac --- /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 0000000000..40bb4958e0 --- /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 97d9bdd140..35538a25aa 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 0000000000..9b2f3abd40 --- /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 0000000000..e87641bc8a --- /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 51d2a85bbb..443808e459 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',