From ebddac66247b82c8769c09208167229783b9fc81 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 --- 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 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',