Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 623b5ca

Browse files
committedAug 22, 2014
fix($compile): use the correct namespace for transcluded svg elements
Via transclusion, svg elements can occur outside an `<svg>` container in an Angular template but are put into an `<svg>` container through compilation and linking. E.g. Given that `svg-container` is a transcluding directive with the following template: ``` <svg ng-transclude></svg> ``` The following markup creates a `<circle>` inside of an `<svg>` element during runtime: ``` <svg-container> <circle></circle> </svg-container> ``` However, this produces non working `<circle>` elements, as svg elements need to be created inside of an `<svg>` element. This change detects for most cases the correct namespace of transcluded content and recreates that content in the correct `<svg>` container when needed during compilation. For special cases it adds an addition argument to `$transclude` that allows to specify the future parent node of elements that will be cloned and attached using the `cloneAttachFn`. Related to #8494
1 parent 35af6a6 commit 623b5ca

File tree

5 files changed

+257
-18
lines changed

5 files changed

+257
-18
lines changed
 

‎src/ng/compile.js

+51-15
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,18 @@
185185
* * `$scope` - Current scope associated with the element
186186
* * `$element` - Current element
187187
* * `$attrs` - Current attributes object for the element
188-
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope.
189-
* The scope can be overridden by an optional first argument.
190-
* `function([scope], cloneLinkingFn)`.
188+
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
189+
* `function([scope], cloneLinkingFn, futureParentElement)`.
190+
* * `scope`: optional argument to override the scope.
191+
* * `cloneLinkingFn`: optional argument to create clones of the original translcuded content.
192+
* * `futureParentElement`:
193+
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
194+
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
195+
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
196+
* and when the `cloneLinkinFn` is passed,
197+
* as those elements need to created and cloned in a special way when they are defined outside their
198+
* usual containers (e.g. like `<svg>`).
199+
* * See also the `directive.templateNamespace` property.
191200
*
192201
*
193202
* #### `require`
@@ -265,6 +274,10 @@
265274
* one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive
266275
* Directives Guide} for an example.
267276
*
277+
* There very few scenarios were element replacement is required for the application function,
278+
* the main one being reusable custom components that are used within SVG contexts
279+
* (because SVG doesn't work with custom elements in the DOM tree).
280+
*
268281
* #### `transclude`
269282
* compile the content of the element and make it available to the directive.
270283
* Typically used with {@link ng.directive:ngTransclude
@@ -359,10 +372,9 @@
359372
* the directives to use the controllers as a communication channel.
360373
*
361374
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
362-
* The scope can be overridden by an optional first argument. This is the same as the `$transclude`
363-
* parameter of directive controllers.
364-
* `function([scope], cloneLinkingFn)`.
365-
*
375+
* This is the same as the `$transclude`
376+
* parameter of directive controllers, see there for details.
377+
* `function([scope], cloneLinkingFn, futureParentElement)`.
366378
*
367379
* #### Pre-linking function
368380
*
@@ -879,8 +891,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
879891
compileNodes($compileNodes, transcludeFn, $compileNodes,
880892
maxPriority, ignoreDirective, previousCompileContext);
881893
safeAddClass($compileNodes, 'ng-scope');
882-
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){
894+
var namespace = null;
895+
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){
883896
assertArg(scope, 'scope');
897+
if (!namespace) {
898+
namespace = detectNamespaceForChildElements(futureParentElement);
899+
if (namespace !== 'html') {
900+
$compileNodes = jqLite(
901+
wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html())
902+
);
903+
}
904+
}
905+
884906
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
885907
// and sometimes changes the structure of the DOM.
886908
var $linkNode = cloneConnectFn
@@ -901,6 +923,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
901923
};
902924
}
903925

926+
function detectNamespaceForChildElements(parentElement) {
927+
// TODO: Make this detect MathML as well...
928+
var node = parentElement && parentElement[0];
929+
if (!node) {
930+
return 'html';
931+
} else {
932+
return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg': 'html';
933+
}
934+
}
935+
904936
function safeAddClass($element, className) {
905937
try {
906938
$element.addClass(className);
@@ -1024,7 +1056,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
10241056

10251057
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) {
10261058

1027-
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) {
1059+
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) {
10281060
var scopeCreated = false;
10291061

10301062
if (!transcludedScope) {
@@ -1033,7 +1065,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
10331065
scopeCreated = true;
10341066
}
10351067

1036-
var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn);
1068+
var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
10371069
if (scopeCreated && !elementTransclusion) {
10381070
clone.on('$destroy', function() { transcludedScope.$destroy(); });
10391071
}
@@ -1645,20 +1677,24 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16451677
}
16461678

16471679
// This is the function that is injected as `$transclude`.
1648-
function controllersBoundTransclude(scope, cloneAttachFn) {
1680+
// Note: all arguments are optional!
1681+
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
16491682
var transcludeControllers;
16501683

1651-
// no scope passed
1652-
if (!cloneAttachFn) {
1684+
// No scope passed in:
1685+
if (!isScope(scope)) {
1686+
futureParentElement = cloneAttachFn;
16531687
cloneAttachFn = scope;
16541688
scope = undefined;
16551689
}
16561690

16571691
if (hasElementTranscludeDirective) {
16581692
transcludeControllers = elementControllers;
16591693
}
1660-
1661-
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers);
1694+
if (!futureParentElement) {
1695+
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
1696+
}
1697+
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement);
16621698
}
16631699
}
16641700
}

‎test/ng/compileSpec.js

+146-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ describe('$compile', function() {
2121
}
2222

2323
function supportsMathML() {
24-
var d = document.createElement('div');
25-
d.innerHTML = '<math></math>';
26-
return !isUnknownElement(d.firstChild);
24+
var d = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math');
25+
return !isUnknownElement(d);
26+
}
27+
28+
// IE9-11 do not support foreignObject in svg...
29+
function supportsForeignObject() {
30+
var d = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
31+
return !!d.toString().match(/SVGForeignObject/);
2732
}
2833

2934
var element, directive, $compile, $rootScope;
@@ -80,6 +85,45 @@ describe('$compile', function() {
8085
terminal: true
8186
}));
8287

88+
directive('svgContainer', function() {
89+
return {
90+
template: '<svg width="400" height="400" ng-transclude></svg>',
91+
replace: true,
92+
transclude: true
93+
};
94+
});
95+
96+
directive('svgCustomTranscludeContainer', function() {
97+
return {
98+
template: '<svg width="400" height="400"></svg>',
99+
transclude: true,
100+
link: function(scope, element, attr, ctrls, $transclude) {
101+
var futureParent = element.children().eq(0);
102+
$transclude(function(clone) {
103+
futureParent.append(clone);
104+
}, futureParent);
105+
}
106+
};
107+
});
108+
109+
directive('svgCircle', function(){
110+
return {
111+
template: '<circle cx="2" cy="2" r="1"></circle>',
112+
templateNamespace: 'svg',
113+
replace: true
114+
};
115+
});
116+
117+
directive('myForeignObject', function(){
118+
return {
119+
template: '<foreignObject width="100" height="100" ng-transclude></foreignObject>',
120+
templateNamespace: 'svg',
121+
replace: true,
122+
transclude: true
123+
};
124+
});
125+
126+
83127
return function(_$compile_, _$rootScope_) {
84128
$rootScope = _$rootScope_;
85129
$compile = _$compile_;
@@ -154,6 +198,105 @@ describe('$compile', function() {
154198
});
155199

156200

201+
describe('svg namespace transcludes', function() {
202+
// this method assumes some sort of sized SVG element is being inspected.
203+
function assertIsValidSvgCircle(elem) {
204+
expect(isUnknownElement(elem)).toBe(false);
205+
expect(isSVGElement(elem)).toBe(true);
206+
var box = elem.getBoundingClientRect();
207+
expect(box.width === 0 && box.height === 0).toBe(false);
208+
}
209+
210+
it('should handle transcluded svg elements', inject(function($compile){
211+
element = jqLite('<div><svg-container>' +
212+
'<circle cx="4" cy="4" r="2"></circle>' +
213+
'</svg-container></div>');
214+
$compile(element.contents())($rootScope);
215+
document.body.appendChild(element[0]);
216+
217+
var circle = element.find('circle');
218+
219+
assertIsValidSvgCircle(circle[0]);
220+
}));
221+
222+
it('should handle custom svg elements inside svg tag', inject(function(){
223+
element = jqLite('<div><svg width="300" height="300">' +
224+
'<svg-circle></svg-circle>' +
225+
'</svg></div>');
226+
$compile(element.contents())($rootScope);
227+
document.body.appendChild(element[0]);
228+
229+
var circle = element.find('circle');
230+
assertIsValidSvgCircle(circle[0]);
231+
}));
232+
233+
it('should handle transcluded custom svg elements', inject(function(){
234+
element = jqLite('<div><svg-container>' +
235+
'<svg-circle></svg-circle>' +
236+
'</svg-container></div>');
237+
$compile(element.contents())($rootScope);
238+
document.body.appendChild(element[0]);
239+
240+
var circle = element.find('circle');
241+
assertIsValidSvgCircle(circle[0]);
242+
}));
243+
244+
if (supportsForeignObject()) {
245+
it('should handle foreignObject', inject(function(){
246+
element = jqLite('<div><svg-container>' +
247+
'<foreignObject width="100" height="100"><div class="test" style="position:absolute;width:20px;height:20px">test</div></foreignObject>' +
248+
'</svg-container></div>');
249+
$compile(element.contents())($rootScope);
250+
document.body.appendChild(element[0]);
251+
252+
var testElem = element.find('div');
253+
expect(isHTMLElement(testElem[0])).toBe(true);
254+
var bounds = testElem[0].getBoundingClientRect();
255+
expect(bounds.width === 20 && bounds.height === 20).toBe(true);
256+
}));
257+
258+
it('should handle custom svg containers that transclude to foreignObject that transclude html', inject(function(){
259+
element = jqLite('<div><svg-container>' +
260+
'<my-foreign-object><div class="test" style="width:20px;height:20px">test</div></my-foreign-object>' +
261+
'</svg-container></div>');
262+
$compile(element.contents())($rootScope);
263+
document.body.appendChild(element[0]);
264+
265+
var testElem = element.find('div');
266+
expect(isHTMLElement(testElem[0])).toBe(true);
267+
var bounds = testElem[0].getBoundingClientRect();
268+
expect(bounds.width === 20 && bounds.height === 20).toBe(true);
269+
}));
270+
271+
// NOTE: This test may be redundant.
272+
it('should handle custom svg containers that transclude to foreignObject'+
273+
' that transclude to custom svg containers that transclude to custom elements', inject(function(){
274+
element = jqLite('<div><svg-container>' +
275+
'<my-foreign-object><svg-container><svg-circle></svg-circle></svg-container></my-foreign-object>' +
276+
'</svg-container></div>');
277+
$compile(element.contents())($rootScope);
278+
document.body.appendChild(element[0]);
279+
280+
var circle = element.find('circle');
281+
assertIsValidSvgCircle(circle[0]);
282+
}));
283+
}
284+
285+
it('should handle directives with templates that manually add the transclude further down', inject(function() {
286+
element = jqLite('<div><svg-custom-transclude-container>' +
287+
'<circle cx="2" cy="2" r="1"></circle></svg-custom-transclude-container>' +
288+
'</div>');
289+
$compile(element.contents())($rootScope);
290+
document.body.appendChild(element[0]);
291+
292+
var circle = element.find('circle');
293+
assertIsValidSvgCircle(circle[0]);
294+
295+
}));
296+
297+
});
298+
299+
157300
describe('compile phase', function() {
158301

159302
it('should attach scope to the document node when it is compiled explicitly', inject(function($document){

‎test/ng/directive/ngIfSpec.js

+19
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,23 @@ describe('ngIf animations', function () {
351351
});
352352
});
353353

354+
it('should work with svg elements when the svg container is transcluded', function() {
355+
module(function($compileProvider) {
356+
$compileProvider.directive('svgContainer', function() {
357+
return {
358+
template: '<svg ng-transclude></svg>',
359+
replace: true,
360+
transclude: true
361+
};
362+
});
363+
});
364+
inject(function($compile, $rootScope) {
365+
element = $compile('<svg-container><circle ng-if="flag"></circle></svg-container>')($rootScope);
366+
$rootScope.flag = true;
367+
$rootScope.$apply();
368+
369+
var circle = element.find('circle');
370+
expect(circle[0].toString()).toMatch(/SVG/);
371+
});
372+
});
354373
});

‎test/ng/directive/ngRepeatSpec.js

+20
Original file line numberDiff line numberDiff line change
@@ -1387,4 +1387,24 @@ describe('ngRepeat animations', function() {
13871387
})
13881388
);
13891389

1390+
it('should work with svg elements when the svg container is transcluded', function() {
1391+
module(function($compileProvider) {
1392+
$compileProvider.directive('svgContainer', function() {
1393+
return {
1394+
template: '<svg ng-transclude></svg>',
1395+
replace: true,
1396+
transclude: true
1397+
};
1398+
});
1399+
});
1400+
inject(function($compile, $rootScope) {
1401+
element = $compile('<svg-container><circle ng-repeat="r in rows"></circle></svg-container>')($rootScope);
1402+
$rootScope.rows = [1];
1403+
$rootScope.$apply();
1404+
1405+
var circle = element.find('circle');
1406+
expect(circle[0].toString()).toMatch(/SVG/);
1407+
});
1408+
});
1409+
13901410
});

‎test/ng/directive/ngSwitchSpec.js

+21
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,25 @@ describe('ngSwitch animations', function() {
433433
expect(destroyed).toBe(true);
434434
});
435435
});
436+
437+
it('should work with svg elements when the svg container is transcluded', function() {
438+
module(function($compileProvider) {
439+
$compileProvider.directive('svgContainer', function() {
440+
return {
441+
template: '<svg ng-transclude></svg>',
442+
replace: true,
443+
transclude: true
444+
};
445+
});
446+
});
447+
inject(function($compile, $rootScope) {
448+
element = $compile('<svg-container ng-switch="inc"><circle ng-switch-when="one"></circle>' +
449+
'</svg-container>')($rootScope);
450+
$rootScope.inc = 'one';
451+
$rootScope.$apply();
452+
453+
var circle = element.find('circle');
454+
expect(circle[0].toString()).toMatch(/SVG/);
455+
});
456+
});
436457
});

0 commit comments

Comments
 (0)
This repository has been archived.