SVG’s FuncIRI, angular2, and the base tag

cog-wrong
Broken mask link: a visualization

I tried to make this title as descriptive and, let’s face it, clickbait-y, because this was hard enough for me to discover. I somehow had never had to deal with this issue until a few days ago – svgs do not play well with single page apps when routing using HTML location is mixed with a set <base> tag.

Specifically, what doesn’t work is anything that uses FuncIRI, or css-style urls. That means <use>, clip-path and filter tags, among others. When trying to fix this I came up with a roundabout solution, before discovering that I didn’t need to, very similar to this solution for angularJS, most likely made before this was fixed (in around version 1.3).

In my case, I didn’t need the <base> tag at all – it was basically set as <base href=”/”>, most likely from the habit and all the examples and starter apps one uses to get their hands dirty with angular. All I needed to know about was APP_BASE_HREF. If you remove the <base> tag angular rightfully complains that it needs a base for its LocationStrategy to work, but APP_BASE_HREF enables us to set it from the bootstrap step:

import {
    APP_BASE_HREF
} from '@angular/common';

bootstrap(App, [
    ...{
        provide: APP_BASE_HREF,
        useValue: '/'
    }
]);

This works even for cases where the base isn’t ‘/’, so should be pretty much universal. Of course, if there are other reasons why you might need the base tag to stay in the page, the only solution is then to update the relevant properties so that their urls match the current one. I feel this should be avoided if at all possible, seeing as it isn’t the most clean or efficient method – not to mention that in our case, it would mean messing directly with the DOM on top of what an SVG animation library is already doing.

Nevertheless, here is an example of how that might look:

import {
    Directive,
    ElementRef,
    OnDestroy
} from '@angular/core';
import {
    Location
} from '@angular/common';

import $ = require('jquery');

@Directive({
    selector: '[update-clip-path]'
})
export class UpdateClipPath implements OnDestroy {
    private sub: any;

    constructor(private location: Location, private elementRef: ElementRef) {
        this.sub = this.location.subscribe(
            next => this.updateClipPath()
        );

        this.updateClipPath();
    }

    private updateClipPath() {
        if (this.elementRef.nativeElement) {
            $(this.elementRef.nativeElement)
                .find('[clip-path]')
                .each((index, el) => {
                    let clipPath = el.getAttribute('clip-path');
                    el.setAttribute(
                        'clip-path',
                        'url(' + this.location.path() + clipPath.substr(clipPath.indexOf('#')));
                });
        }
    }

    ngOnDestroy() {
        if (this.sub && this.sub.unsubscribe) {
            this.sub.unsubscribe();
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *