From f8f0610dab52d6a960ddcdbc3462fa26d2c91845 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 7 May 2014 12:14:38 -0400 Subject: [PATCH 1/2] fix($compile): support transcluding SVG nodes when there is no root node Due to https://github.com/angular/angular.js/commit/f0e12ea7fea853192e4eead00b40d6041c5f914a, it is now possible to have a directive whose template content is SVG. However, the following issue arises: A template like My circle! Would not work as expected, due to the group, circle and text nodes being parsed as HTMLUnknownElements, for not being parsed in an SVG context. This change enables transcluded SVG content to work when parsed as non-HTML content, with the cost of linking the transcluded elements again. --- src/.jshintrc | 1 + src/Angular.js | 34 ++++++++++++++++++++++++++++++++++ src/ng/compile.js | 35 +++++++++++++++++++++++++++++++++++ test/ng/compileSpec.js | 31 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/src/.jshintrc b/src/.jshintrc index 1202b64447a3..2f735cce6e10 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -70,6 +70,7 @@ "isElement": false, "makeMap": false, "map": false, + "reduce": false, "size": false, "includes": false, "indexOf": false, diff --git a/src/Angular.js b/src/Angular.js index 57f478b5b944..50ca116de1a7 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -50,6 +50,7 @@ -isElement, -makeMap, -map, + -reduce, -size, -includes, -indexOf, @@ -641,6 +642,39 @@ function map(obj, iterator, context) { } +/** + * @description + * Reduces a collection into a single accumulated value. Based on ES5 Array#reduce + * + * This is useful for performing operations on a collection, which should result in + * a single value, such as concatenating the outerHTML of a collection of DOM nodes + * into a single string. + * + * @param {Array} array The array to operate on. + * @param {Function(*, *, i, Array)} callback Count function to operate on each value in the + * collection. The parameters are as follows: + * - previousValue The previous accumulated value + * - currentValue The value from the current index in the collection + * - index The index of the current value + * - array A reference to the array being operated on + * + * The callback should return the accumulated value. + * @param {*} initialValue The initial value for the accumulator + * + * @returns {*} The accumulated value, resulting from callback being called on each element + * in the collection. See http://goo.gl/YwhaCh for more details. + */ +function reduce(array, callback, initialValue) { + var i; + var ii; + var previousValue = initialValue; + for (i = 0, ii = array.length; i < ii; ++i) { + previousValue = callback(previousValue, array[i], i, array); + } + return previousValue; +} + + /** * @description * Determines the number of elements in an array, the number of properties an object has, or diff --git a/src/ng/compile.js b/src/ng/compile.js index a5b49cb42d0a..0ac4b33aeadf 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -854,6 +854,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { safeAddClass($compileNodes, 'ng-scope'); return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ assertArg(scope, 'scope'); + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. var $linkNode = cloneConnectFn @@ -874,6 +875,40 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (cloneConnectFn) cloneConnectFn($linkNode, scope); + + if (window.HTMLUnknownElement) { + // If any of these elements are HTMLUnknownElement, it may be necessary + // to fix them. + var _i, _ii; + for (_i = 0, _ii = $linkNode.length; _i < _ii; ++_i) { + if ($linkNode[_i].constructor === window.HTMLUnknownElement) { + var origParent = $linkNode[_i].parentNode; + var parent = origParent; + while (parent) { + if (window.SVGSVGElement && parent.constructor === window.SVGSVGElement) { + var wrapper = document.createElement('div'), svg; + wrapper.innerHTML = '' + reduce($linkNode, function(previous, current) { + return previous + current.outerHTML; + }, '') + ''; + svg = wrapper.childNodes[0]; + $linkNode.remove(); + $linkNode.length = 0; + forEach(svg.childNodes, function(node) { + svg.removeChild(node); + $linkNode.push(node); + }); + + // Sadly, this needs to be re-linked ._. + if (cloneConnectFn) cloneConnectFn($linkNode, scope); + break; + } + parent = parent.parentNode; + } + break; + } + } + } + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); return $linkNode; }; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 6dcc536276a4..b10e35ae65cc 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4040,6 +4040,37 @@ describe('$compile', function() { }); }); + + + it('should transclude SVG content into SVG templates', function() { + if (!window.SVGCircleElement) return; + module(function() { + directive('svgRoot', valueFn({ + restrict: 'A', + template: '', + replace: true, + transclude: true, + link: function(scope, elem, attr, ctrl, transclude) { + transclude(scope, function(nodes) { + jqLite(nodes).addClass('test'); + elem.children(0).append(nodes); + }); + } + })); + }); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + var circle = element.find('circle'); + expect(circle.length).toBe(1); + expect(circle[0].constructor).toBe(window.SVGCircleElement); + + // Ensure that the directives are still compiled + browserTrigger(circle, 'click'); + expect($rootScope.foo).toBe(88); + expect(circle).toHaveClass('test'); + }); + }); }); From aec1a139f4007abf68f33bd1a31a75600c10c0c3 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Wed, 7 May 2014 13:18:54 -0400 Subject: [PATCH 2/2] test(matchers): make `toHaveClass()` matcher support SVG even when jQuery used `toHaveClass()` is a useful matcher, but unfortunately leverages jQuery/jqLite's hasClass() method. jQuery's class API does not support SVG nodes, while angular's does. Therefore, some extra work needs to be done to make this work for both libraries. --- test/helpers/matchers.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js index b18604bf9ba4..6ca789c27286 100644 --- a/test/helpers/matchers.js +++ b/test/helpers/matchers.js @@ -167,6 +167,14 @@ beforeEach(function() { this.message = function() { return "Expected '" + angular.mock.dump(this.actual) + "' to have class '" + clazz + "'."; }; + var node = this.actual; + if (node.hasClass) node = node[0]; + if (window.SVGElement && node instanceof window.SVGElement + && typeof node.className === "object") { + var tmp = document.createElement('div'); + tmp.className = node.className.animVal; + return jqLite(tmp).hasClass(clazz); + } return this.actual.hasClass ? this.actual.hasClass(clazz) : angular.element(this.actual).hasClass(clazz);