Thursday, July 2, 2015

Reusable SVG with AngularJS

(Available through bower install. Link on github: ngReusableSvg)


Ohhh SVGs. How stressful thy be.

Scalable Vector Graphics.

All I wanted was to have one external SVG file so I could stretch its cheeks and color it to my liking.

But was it stretchy and colory? Noooooooo.

I thought the entire idea was to have one file, then CSS my way through its sizes and colors, and if it's not too much to ask, then please let it work in all the major browsers, m'kay?

Turns out, it's not that easy.

I hope this article will save you the days I spent researching this.

Important note: this method will only be of service to you if your SVG is an external file taken from the same domain your site is on. (unless the SVG is hosted on a site allowing cross origin requests)

1) The image way:

<img src="ship_icon.svg"/>

That renders the SVG somewhat like an image and you can forget about coloring it. Next!

2) Regular element with a CSS of background image:

<div class="ship_icon.svg"></div>

And straight up CSS it, but doesn't work for the same reason #1 doesn't work.

3) After much seeking, I had found the use option: 

<svg>
      <use xlink:href="ship_icon.svg#icon"></use>
</svg>

However, if my SVG that I got from the designer does not have any symbols, I have to create them and/or edit their IDs. I don't really want to add any steps like editing the SVG, but it's not that bad an option if all the requirements are met. Tough luck though, I found it not so browser compatible, and encountered issues in Firefox. Next :(

4) There's inline SVG - but I thought the entire idea of using SVG is that I can prevent duplicate images for different sizes and colors. So duplicating the code for every HTML page I need the SVG in is a big no no for me. I'd probably should state that I'm using AngularJS in my application, and yes, I could place the inline SVG code in my main page, but personally I think that's just horrible. Lots and lots of incomprehensible code cluttering the page, and you also lose the caching ability for the image.

5) Fifth time's a charm - I ended up using the object tag:

<object id="ship-origin"
        data="ship_icon.svg"
        type="image/svg+xml"
        class="ship-object-svg"
        height="20"
        width="20">
</object>

You can scale it using CSS if you place it inside a container of the size you need.
But it has one downside - you can't change the color using the "fill" attribute!


Why?


It embeds the SVG in a similar way an iframe embeds its content, and prevents you from changing the inner style: (notice that nasty #document)


Enters a trick I got from a fellow programmer (Matan Zohar) - he took the object's contents after it rendered the SVG, and then created a "div" object. He placed those contents inside the "div", and used that "div" to replace the original "object" element. The old switcheroo!

I took that trick and created a directive out of it which I'll share below.
The idea is to achieve exactly what I described above in the old switcheroo trick, and added some options like clicking the SVG:
(please note that this is the basic version. Check out the git repo for the most updated directive)

'use strict';
 
var SwitchSvgDirective = [function () {
 
    return {
        restrict: 'A',
        scope: {
            svgClick: '='
        },
        compile: function compile() {
            return {
                pre: function preLink(scope, iElement, iAttrs) {
                    iElement.bind('load', function() {

                        // Create the DIV element, and use height/width to prevent UI rescaling
                        var div = angular.element('<div style="float:left; height:' + 
                                                                         iAttrs.height + 
                                                                         'px; width:' + 
                                                                         iAttrs.width + 'px;"></div>');

                        // Keep the original classes/id
                        div.addClass(iAttrs.class);
                        div.attr('id', iAttrs.id);
                    
                        var svg = iElement.contents().find('svg');

                        // In case we want the icon to be clickable
                        if (scope.svgClick) {
                            svg.bind('click', function(){
                                scope.svgClick();
                            });
                        }
 
                        // Give the actual contents to our DIV
                        div.html(svg);

                        // Remove previous fills, if any
                        angular.forEach(div.contents().find('g,path,polygon,circle,rect'), function(el){
                            $(el).removeAttr('fill');
                        });

                        // Actual switch
                        iElement.replaceWith(div);
                    });
                }
            };
        }
    };
 
}];

Usage:


<object switch-svg
        svg-click="ctrl.someFunction"
        id="ship-origin"
        data="ship_icon.svg"
        type="image/svg+xml"
        class="ship-object-svg"
        height="20"
        width="20">
</object>


What do we get out of this?


We now have a regular DOM hierarchy without any disturbances, so we can abuse the CSS as much as we want:





Now we can use an external SVG file which is both scalable and colorable. Life is good again!