From f0e12ea7fea853192e4eead00b40d6041c5f914a Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Sat, 26 Apr 2014 19:35:01 -0400 Subject: [PATCH] feat($compile): allow SVG and MathML templates via special `type` property Previously, templates would always be assumed to be valid HTML nodes. In some cases, it is desirable to use SVG or MathML or some other language. For the time being, this change is only truly meaningful for SVG elements, as MathML has very limited browser support. But in the future, who knows? Closes #7265 --- src/ng/compile.js | 34 +++++++++++-- test/ng/compileSpec.js | 108 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 4256adcaabc7..769ac0e0ff34 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -213,6 +213,19 @@ * * `M` - Comment: `` * * + * #### `type` + * String representing the document type used by the markup. This is useful for templates where the root + * node is non-HTML content (such as SVG or MathML). The default value is "html". + * + * * `html` - All root template nodes are HTML, and don't need to be wrapped. Root nodes may also be + * top-level elements such as `` or ``. + * * `svg` - The template contains only SVG content, and must be wrapped in an `` node prior to + * processing. + * * `math` - The template contains only MathML content, and must be wrapped in an `` node prior to + * processing. + * + * If no `type` is specified, then the type is considered to be html. + * * #### `template` * replace the current element with the contents of the HTML. The replacement process * migrates all of the attributes / classes from the old element to the new one. See the @@ -1261,7 +1274,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(trim(directiveValue)); + $template = jqLite(wrapTemplate(directive.type, trim(directiveValue))); } compileNode = $template[0]; @@ -1676,7 +1689,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + : origAsyncDirective.templateUrl, + type = origAsyncDirective.type; $compileNode.empty(); @@ -1690,7 +1704,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(trim(content)); + $template = jqLite(wrapTemplate(type, trim(content))); } compileNode = $template[0]; @@ -1813,6 +1827,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch(type) { + case 'svg': + case 'math': + var wrapper = document.createElement('div'); + wrapper.innerHTML = '<'+type+'>'+template+''; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } + + function getTrustedContext(node, attrNormalizedName) { if (attrNormalizedName == "srcdoc") { return $sce.HTML; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 41bf4854b46d..6dcc536276a4 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -768,6 +768,59 @@ describe('$compile', function() { }).not.toThrow(); expect(nodeName_(element)).toMatch(/optgroup/i); })); + + if (window.SVGAElement) { + it('should support SVG templates using directive.type=svg', function() { + module(function() { + directive('svgAnchor', valueFn({ + replace: true, + template: '{{text}}', + type: 'SVG', + scope: { + linkurl: '@svgAnchor', + text: '@?' + } + })); + }); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + var child = element.children().eq(0); + $rootScope.$digest(); + expect(nodeName_(child)).toMatch(/a/i); + expect(child[0].constructor).toBe(window.SVGAElement); + expect(child[0].href.baseVal).toBe("/foo/bar"); + }); + }); + } + + // MathML is only natively supported in Firefox at the time of this test's writing, + // and even there, the browser does not export MathML element constructors globally. + // So the test is slightly limited in what it does. But as browsers begin to + // implement MathML natively, this can be tightened up to be more meaningful. + it('should support MathML templates using directive.type=math', function() { + module(function() { + directive('pow', valueFn({ + replace: true, + transclude: true, + template: '{{pow}}', + type: 'MATH', + scope: { + pow: '@pow', + }, + link: function(scope, elm, attr, ctrl, transclude) { + transclude(function(node) { + elm.prepend(node[0]); + }); + } + })); + }); + inject(function($compile, $rootScope) { + element = $compile('8')($rootScope); + $rootScope.$digest(); + var child = element.children().eq(0); + expect(nodeName_(child)).toMatch(/msup/i); + }); + }); }); @@ -1612,6 +1665,61 @@ describe('$compile', function() { $rootScope.$digest(); expect(nodeName_(element)).toMatch(/optgroup/i); })); + + if (window.SVGAElement) { + it('should support SVG templates using directive.type=svg', function() { + module(function() { + directive('svgAnchor', valueFn({ + replace: true, + templateUrl: 'template.html', + type: 'SVG', + scope: { + linkurl: '@svgAnchor', + text: '@?' + } + })); + }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('template.html', '{{text}}'); + element = $compile('')($rootScope); + $rootScope.$digest(); + var child = element.children().eq(0); + expect(nodeName_(child)).toMatch(/a/i); + expect(child[0].constructor).toBe(window.SVGAElement); + expect(child[0].href.baseVal).toBe("/foo/bar"); + }); + }); + } + + // MathML is only natively supported in Firefox at the time of this test's writing, + // and even there, the browser does not export MathML element constructors globally. + // So the test is slightly limited in what it does. But as browsers begin to + // implement MathML natively, this can be tightened up to be more meaningful. + it('should support MathML templates using directive.type=math', function() { + module(function() { + directive('pow', valueFn({ + replace: true, + transclude: true, + templateUrl: 'template.html', + type: 'MATH', + scope: { + pow: '@pow', + }, + link: function(scope, elm, attr, ctrl, transclude) { + transclude(function(node) { + elm.prepend(node[0]); + }); + } + })); + }); + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('template.html', '{{pow}}'); + element = $compile('8')($rootScope); + $rootScope.$digest(); + var child = element.children().eq(0); + expect(nodeName_(child)).toMatch(/msup/i); + }); + }); });