Angular.js: 1.3 : Requiring <base> tags means I can't use SVG with clip-path, mask, etc...

Created on 4 Sep 2014  Â·  51Comments  Â·  Source: angular/angular.js

With the recent change of requiring the <base> tag, now all my SVG icons that I use that have clip-paths, masks, etc... are broken. I can't set the urls for those properties to absolute paths because the url constantly changes when navigate in the app and I can't set relative urls because of the <base> now. The only thing I can do is not use HTML5 mode which I just don't like.

I don't see why the <base> tag should be required when historically from what I read it generally causes more issues than is solves (like this one). As far as linking goes, I can't think of a reason they should not all start with a /. There is no reason I can think of where when on /page1 and you want to link to /page2 you would need to have the link be page2. As far as the deep url issue, have that be something you can configure with $locationProvider or something.

I really hope this change can be un-done or someone can tell me how to use SVG properties that require url() while still being able to HTML5 mode.

PRs plz! $location moderate inconvenient

Most helpful comment

This may be useful for anyone running into this who still wants to have a <base> tag

// in controller:
$scope.absoluteRef = function (id) {
          return 'url(' + $location.absUrl() + '#' + id + ')';
        }

in template (example):

<svg xmlns="http://www.w3.org/2000/svg">

      <radialGradient id="my-radial-gradient">
        <stop offset="15%" stop-color="white"></stop>
        ...
      </radialGradient>

      <circle fill="{{absoluteRef('my-radial-gradient')}}">
      </circle>
</svg>


I don't know if there's anything about 'fill' that means this won't work with 'mask' or 'clip-path', but $location.absUrl() will provide the current URL for any dynamically changing url, and it works for me

All 51 comments

or someone can tell me how to use SVG properties that require url() while still being able to HTML5 mode.

You shold be able to use a full URL (including scheme + host + path) for these --- the browser won't rewrite those relative to the <base> tag.

As for undoing it, well we tried something else first, but unfortunately the alternative had more problems. I agree that it really sucks that the <base> tag is so coupled with the application base url in angular, but there's little we can do about it without making things even worse :(

You could go bug anne van kesteren about it though :p maybe he'll let us have multiple base tags in the future, for different things

/cc @tbosch

You should be able to use a full URL (including scheme + host + path) for these --- the browser won't rewrite those relative to the tag.

I wish it were that easy. When I try that, the SVG icons look fine on initial page load however the first time I navigate to a different page within the application, all the clipping goes away. The clipping only works on the initially loaded page because that is the full path that is used in the SVG. The SVG icons are not getting recompiled or anything like that (using ui-router in order to have certain view that don't change when moving around the application). I assume this is because once I navigate to a new page, the SVG no longer can find the element (since the full url does not match the new url after the initial page load).

Yes, that's more of a problem... I'll see if there's anything we can do about that without reverting the location fixes

Could you like, provide a reproduction to what you're talking about (wrt to clipping being broken)? I'm not really getting that --- even just showing the markup of your svg would be helpful

@caitp
Without <base>:

http://plnkr.co/edit/ykU211Qgx2HczNnM5LXY?p=preview

With <base>:

http://plnkr.co/edit/syDHyECUmo5aJyw7LzwI?p=preview

The SVG has a mask and is generated with Sketch but I believe most vector software will export similar to this.

Thanks --- so yes, changing the mask URI to an absolute url does work around it... could probably write a quick tool to do that for you. I don't think we can really do that in core though ._.

You can author the SVG differently though, using paths instead of --- it's a bit of a heavier transfer, but it should work for you.

I'm not sure we'd be wise to automate changing FuncIRI notation on route change though :(

Digging into more, it seems like it has to do with the library I am using for SVG icons, Iconic. When I remove the iconic.js library that is being used to load the icons and fallback to the SVGInjector library, route switching no longer effect the icons properties that use url() (though I still need to add the absolute url to the beginning which is not that big of an issue).

I still hate the fact that 1.3 requires the <base> tag with HTML5 mode. Are there any downsides (besides a slight ugly URL) that I should be aware of when not using HTML5 mode?

Hi,
not using html5 mode will not resolve your problem, as Angular then uses the hash fragment for storing the location information within your app, so all links will be relative to the initial url with which the page was loaded as well (same effect as using html5mode with the <base> tag).

The major problem with html5mode and not having a <base> tag is that you get different behavior on older browsers that don't support the history API (IE9) and newer browsers. Older browsers will fall back to using the hash fragment, which makes all links relative to the initial url.

The problem with svg attributes that use url is that they don't respect the value of the <base> tag and always uses the current path defined by the history api, in contrast to xlink:href.

See this plunker: http://plnkr.co/edit/yRChhan8SB5fQIy9BmjC?p=preview
Note how the <use xlink:href="#path-1"></use> is working correctly, but the mask="url(#mask-2)" is not.

Also note that changing the path via the history API AFTER the svg has been loaded does not seem a problem!
Thinking about how to solve this...

Ah, calling history.pushState right after the <svg> also causes the problem, but calling history.pushState in a setTimeout does not :-(

One solution could be: Create a directive for all of the SVG properties that are allows to use a url value (see this list: http://www.w3.org/TR/SVG/linking.html#processingIRI). Those directives would then make the specified urls into absolute urls using the <base> url, just like a link would do.

To make this more concrete:

  • directive for the attributes: ['clip-path', 'color-profile', 'src', 'cursor', 'fill', 'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end', 'mask', 'stroke’]
  • check attribute value for url(...) using regex
  • normalize those attributes using angular internal urlResolve function

Anyone would like to make a PR for this?

@tbosch I'll take a stab at a PR

Could you check whether our solution also works in FF using the Plunker and
maybe look into the spec to see if this behavior is by accident/a bug in
browsers?
Thanks!

On Thursday, September 11, 2014, Jeff Cross [email protected]
wrote:

@tbosch https://github.com/tbosch I'll take a stab at a PR

—
Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/issues/8934#issuecomment-55359468.

The spec for svg 1.1 linking seems to ("seems to" = "I'm not the best at parsing specs") indicate that the FuncIRI notation (attr="url(...)") should respect the document's base tag, in the order of priority supported by xml:base tag. However, issue 34 on the SVG2 draft indicates that the document's base tag's impact on SVG isn't clearly speced.

I'm experimenting with adding xml:base to different self/parent svg elements in the plnkr, but haven't yet been able to get it to do what I want.

Actually it seems that Chrome doesn't honor xml:base, but Firefox does.

https://crbug.com/341854 this looks like bad news for xml:base

This revised plnkr shows the new build fixing the issue: http://plnkr.co/edit/DwGcTgntneGj0AjLaIBG?p=preview

Change angular src in <head> to see difference. Safari seems to have already been behaving; works the same with both Angular builds.

I'm going to open an unsquashed PR for review and a Travis run, but I still want to update the tests to manipulate the real base tag, and would also like to write a benchmark to measure cost of registering no-op directives (per @tbosch suggestion).

Recapping an IRL chat with @tbosch . He has reservations about the auto-fixing directive approach (I don't feel 100% for it myself, but it seems least of all evils).

My follow-up tasks:

  • I'm going to research the SVG Linking spec some more to verify my assumption that browsers aren't properly scoping FuncIRI fragment links to the svg document fragment (they are resolving with the document's base).
  • Also clarify how relative fragment links should be resolved (url(/somePath#fragment)), and whether browsers are handling correctly.
  • I'm going to research how other SVG libraries are resolving problems with <base>

Only relevant issue I found on d3 was https://github.com/mbostock/d3/issues/1510, to which the solution was to manually fix problem outside of library. I don't think there's anything in the library that accounts for browser discrepancies resolving FuncIRI references.

Fragment identifiers (values that begin with #) are not considered relative or absolute, and are considered a local IRI reference Spec: http://www.w3.org/TR/SVG/linking.html#IRIforms

Local IRI are defined as referring to an element within the current document definition

So it seems as Webkit is honoring this, but blink and gecko are not.

After some more discussion with @tbosch we decided to move this feature into a separate module outside of core for now, and see if it's the correct approach (and if enough people find it worthwhile). I'm going to work on moving it this afternoon. It would be good to reference this from our docs somewhere. Probably in $location and $locationProvider. I'll also add a note to the API reference for $location about the base tag requirement since right now it can only be found in the guide or the error that gets thrown.

that sounds like the right approach, the only problem with that is that urlResolve isn't exposed (so, code duplication or exposing yet another utility ._.)

image

Interestingly, SVGTiny disallows loading a fragment from an external document using any attributes that use FuncIRI notation: http://www.w3.org/TR/SVGTiny12/linking.html#ReferenceRestrictions

But SVG 1.1 makes no mention of a similar restriction: http://www.w3.org/TR/SVG/linking.html#processingIRI

I'm having trouble referencing fragments of external SVG documents, with or without base tags.

I created an external module to fix this issue at https://github.com/jeffbcross/angular-svg-base

@caitp care to take a look? I implemented your feedback from the original PR.

I'm not keen on making things like urlResolve() injectable, since it's putting stuff into the global injector --- otherwise it looks okay to me.

Do we want to add it to the angular repo so it's easier to maintain?

@tbosch thought it'd be best as an external module so we could gauge its utility before making part of core (even as a separate module inside core).

FYI here is my attempt at referring to an external clipMask, which doesn't work in blink, webkit, or gecko. @caitp do you see any flaw with my approach?
http://plnkr.co/edit/9t5mCQO3YYIEod4GmApt?p=preview

SVG 1.1 does seem to indicate that clip-path must reference a clipPath element (by id, presumably).

I guess for this to work you'd need to import the external svg into the document and reference it as a local, but OTOH I'm not sure how you'd do that

(or in other words, if there is a way to do this, I can't tell what it is. Some research found a script on github (oh look at that, JonathanNeal's, huh) which will try to fetch an external SVG with XHR and process it to add it to the local document, but other than that I'm not sure there is a solution)

This issue on Chromium might be of relevance: https://code.google.com/p/chromium/issues/detail?id=109212

I'm asking pdr / other svg folks whether Firefox is even doing the right thing there, because I honestly can't tell from reading the svg 1.1 spec.

Anyways, @ryanzec can you try out the module Jeff wrote and see if that works for you? If it works well for people we can maybe make it a core module

[17:08] <pdr> caitp, I think it's valid from the spec's perspective. I am not sure if it will ever be implemented properly though

Take for whatever that's worth I guess

Hey you know what though? I'm not sure the reproduction was correct.

The base URL was not correct in the original reproduction, it looks like it works here: http://plnkr.co/edit/XC3flEpBitdKsTOM87yg?p=preview

I think, given that the reproduction was wrong and this bug can't be reproduced with the correct base URL, and with jeff's solution for manipulating the FuncIRI attributes, we're probably good to close this. Please re-open if you find more issues with SVG =)

(recapping external conversation): that reproduction works because the SVG is not re-rendering after path change. If any property of the element would change (like changing the fill of the ellipse) it would re-draw without the clip mask.

yes --- however we do have solution 2 which (seems) to fix that, as well

[18:00] <pdr> caitp, yeah, I think this is a bug. I'll file. I would recommend trying a different approach though. Might a and a:active work?

(re: the re-rendering breaking things)

@caitp I don't understand the suggestion; do you? Could you elaborate?

To clarify, if you would start off opening a document at a subpath in html5Mode, the first render would not find the clip path, and would draw incorrectly. The reproduction happens to work the first time because the document is at exactly the base href.

I didn't really understand the suggestion either --- I asked for clarification but didn't really get it, I think what they're saying is to modify history differently or something. (maybe show different content depending on an anchor tag having a specific pseudoclass? I was not sure what to make of it)

However he did say he's filing a bug regarding the re-rendering issue, so if I get a link I'll paste it here

I've introduced the ability to remove the base tag requirement in https://github.com/angular/angular.js/commit/dc3de7fb7a14c38b5c3dc7decfafb0b51d422dd1 by passing an object to html5Mode() with a property of requireBase set to false:

$locationProvider.html5Mode({enabled: true, requireBase: false});

Setting this removes the requirement of having a base tag in the document, with the caveat that IE9 will have issues with resolving relative paths.

I always use absolute paths anyways (and I might only have to support IE10+) so this should work for me.

@jeffbcross, thanks! $locationProvider.html5Mode({enabled: true, requireBase: false} works for me

@jeffbcross Thanks a lot for this!

screen shot 2015-12-09 at 12 31 47

Thanks.

This may be useful for anyone running into this who still wants to have a <base> tag

// in controller:
$scope.absoluteRef = function (id) {
          return 'url(' + $location.absUrl() + '#' + id + ')';
        }

in template (example):

<svg xmlns="http://www.w3.org/2000/svg">

      <radialGradient id="my-radial-gradient">
        <stop offset="15%" stop-color="white"></stop>
        ...
      </radialGradient>

      <circle fill="{{absoluteRef('my-radial-gradient')}}">
      </circle>
</svg>


I don't know if there's anything about 'fill' that means this won't work with 'mask' or 'clip-path', but $location.absUrl() will provide the current URL for any dynamically changing url, and it works for me

Was this page helpful?
0 / 5 - 0 ratings