w0rm / gulp-svgstore

Combine svg files into one with symbol elements
https://www.npmjs.com/package/gulp-svgstore
645 stars 33 forks source link

Option to switch between <symbol> and <g> #27

Closed darsain closed 8 years ago

darsain commented 9 years ago

An option switch so we could choose between transforming files into <symbol> or <g>.

<symbol> is awesome for icons, but doesn't work for CSS backgrounds. <g> is awesome for CSS backgrounds.

In other words, this doesn't work with <symbol>, but does with <g>:

.foo {
  background: url('images.svg#bar');
}
w0rm commented 9 years ago

@darsain can you please provide more details on how combined svg should look like in this case or maybe you have a link to an article? It would be also interesting to know browser support.

If you only need <g> instead of <symbol>, then you can rename them with gulp-cheerio until we find a better solution for gulp-svgstore.

var gulp = require('gulp');
var svgstore = require('gulp-svgstore');
var cheerio = require('gulp-cheerio');

gulp.task('default', function () {
    return gulp
        .src('src/*.svg')
        .pipe(svgstore())
        .pipe(cheerio(function ($) {
            $('symbol').each(function () {
                this.tagName = 'g';
            });
        }))
        .pipe(gulp.dest('dest'));
});
darsain commented 9 years ago

To make this work in CSS:

background: url('images.svg#chart');

You need to structure images.svg in a way that enables SVG fragment identifiers. There are 3 ways I know of:

1. SVG stack with global viewBox

You'd use this when all images are the same size.

<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style>
        .img { display: none }
        .img:target { display: inline }
        </style>
    </defs>

    <g id="chart" class="img">
        <rect x="6" width="4" height="16"/>
        <rect x="12" y="4" width="4" height="12"/>
        <rect x="0" y="8" width="4" height="8"/>
    </g>

    <!-- more "g" tags -->
</svg>
2. SVG stack with per image viewBox

You'd use this when every image has different size.

<svg xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style>
        .img { display: none }
        .img:target { display: inline }
        </style>
    </defs>

    <svg viewBox="0 0 16 16">
        <g id="chart" class="img">
            <rect x="6" width="4" height="16"/>
            <rect x="12" y="4" width="4" height="12"/>
            <rect x="0" y="8" width="4" height="8"/>
        </g>
    </svg>

    <!-- more "svg > g" tags -->
</svg>
3. SVG sprite with ID'd views

This one is sub-optimal. When mismatching target element to image size ratio, you might see other sprites peeking from sides. You'd have to make big gaps between images, but that's ugly and not bulletproof.

It is also more annoying to write build scripts for. But here it is anyway:

<svg xmlns="http://www.w3.org/2000/svg">
    <view id="chart" viewBox="0 0 16 16"/>
    <view id="plus" viewBox="16 0 16 16"/>

    <g transform="translate(0 0)">
        <rect x="6" width="4" height="16"/>
        <rect x="12" y="4" width="4" height="12"/>
        <rect x="0" y="8" width="4" height="8"/>
    </g>

    <g transform="translate(16 0)">
        <mask id="m" x="0" y="0" width="1" height="1">
            <circle cx="8" cy="8" r="8" fill="white"/>
            <line x1="8" y1="3" x2="8" y2="13" stroke="black" stroke-width="2"/>
            <line x1="3" y1="8" x2="13" y2="8" stroke="black" stroke-width="2"/>
        </mask>
        <rect width="16" height="16" mask="url(#m)"/>
    </g>
</svg>

Currently, SVG fragment identifiers in CSS backgrounds work only in FF and IE, and will work in Chrome as soon as crbug.com/128055 is fixed.

Isolated working example implementation with all 3 solutions above here: svg-fragment-identifiers.zip

w0rm commented 9 years ago

@darsain why do you need a tool for this if it is not supported in Chrome?

Something like this make it work with 2nd example. I don't say it is a good solution though, will have to reconsider it when chrome adds support for this method.

var gulp = require('gulp');
var svgstore = require('gulp-svgstore');
var cheerio = require('gulp-cheerio');

gulp.task('default', function () {
  return gulp
      .src('src/svg/*.svg')
      .pipe(svgstore())
      .pipe(cheerio(function ($) {
          $('svg').prepend(
              '<defs>'+
              '<style>' +
              '.img { display: none }' +
              '.img:target { display: inline }' +
              '</style>' + 
              '</defs>'
          )
          $('symbol').each(function () {
              var $node = $(this);
              this.tagName = 'svg';
              $node
                  .html(
                      '<g id="' + $node.attr('id') + '" class="img">' + 
                          $node.html() + 
                      '</g>'
                  )
                  .removeAttr('id')
          });
      }))
      .pipe(gulp.dest('dest'));
});
darsain commented 9 years ago

@w0rm to be honest, before I started researching this, I thought that Chrome did support that. Was quite surprised that it doesn't.

But still, would be nice to have this available when support hits stable.

w0rm commented 9 years ago

@darsain do you have any objections regarding closing this issue as wonfix?

darsain commented 9 years ago

@w0rm yes, as this is gonna be extremely useful in near future. I have nothing against putting this on low priority until Chrome adds support, but straight wontfix? :(

w0rm commented 9 years ago

@darsain I'd prefer to keep it closed, and then re-open the ticket if needed.

w0rm commented 9 years ago

@darsain thanks! I'd like to treat tickets in the "action is needed" manner.

LeZuse commented 9 years ago

Just ran into this, too. Pretty surprising that Chrome is behind on this. Maybe the SVG implementations are not ready for today after all :)

zbjornson commented 8 years ago

angular/material's md-icon/$mdIconProvider requires g instead of symbol because it doesn't use the use tag to display icons. This is a current use case (unlike the above discussion if it's not out of date) for a very popular framework. An option like element: "symbol|g" would be great. Would you accept a PR with this change?

w0rm commented 8 years ago

@zbjornson it may not play well with the current logic that extracts elements from <defs> of each file and combines in <defs> section of the result file. Also it would require test cases including visual tests. If you're willing to do this, then sure.

zbjornson commented 8 years ago

Was this closed because someone else made the change? If not I'm planning to do it next weekend.

w0rm commented 8 years ago

@zbjornson closed because I don't need this feature. Feel free to open PR if you want this!

w0rm commented 8 years ago

@zbjornson out of curiosity, why don't you use this library https://github.com/jkphl/gulp-svg-sprite? I think it supports every possible case to bundle svgs

zbjornson commented 8 years ago

Hmm thanks for pointing out that repo, will take a look. Have been happily using yours for almost a year and only recently needed to switch element types. If it doesn't work I'll make a PR here, thanks.

w0rm commented 8 years ago

@zbjornson my library is very opinionated about bundling svgs, instead of implementing all possible methods, I did some research and focused on the method that I liked best.

https://github.com/w0rm/compare-sprite-methods — playground with different bundling methods https://tech.zalando.com/blog/creating-bulletproof-svg-icons/ — blogpost about usage with fallback http://unsoundscapes.com/slides/2015-03-19-creating-bulletproof-svg-icons/ — slides from a meetup