gregberge / svgr

Transform SVGs into React components 🦁
https://react-svgr.com
MIT License
10.59k stars 422 forks source link

How to remove <svg> element? #769

Open emlai opened 2 years ago

emlai commented 2 years ago

We need to transform our icons to the following format, i.e. without the parent <svg> element:

const HomeIcon = createSvgIcon(
  <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />,
  'Home',
);

By default svgr includes the <svg> element in the output. How can this be done in svgr?

headfire94 commented 2 years ago

@emlai SVGR transforms SVG icons to React components, how you expect you react component to look like in the output?

headfire94 commented 2 years ago

I don't think it's possible with this plugin. you can write your babel-plugin to transform svg to empty tag https://react-svgr.com/docs/custom-transformations/

emlai commented 2 years ago

I solved this by defining a custom babel plugin to remove the parent <svg> element (added under jsx.babelConfig.plugins):

const removeSvgElement = (): PluginObj => ({
  visitor: {
    JSXElement(path) {
      if ((path.node.openingElement.name as JSXIdentifier).name === "svg") {
        if (path.node.children.length === 1) {
          path.replaceWith(path.node.children[0]);
        } else {
          path.replaceWith(
            t.jsxFragment(
              t.jsxOpeningFragment(),
              t.jsxClosingFragment(),
              path.node.children
            )
          );
        }
      }
    },
  },
});
stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

gajus commented 1 year ago

@emlai Thank you so much! Saved me a ton of time

gajus commented 1 year ago

@headfire94 To provide more context, the goal here is to replace top <svg> element with a more flexible React component, e.g.

const iconTemplate: Template = ({ componentName, jsx }, { tpl }) => {
  return tpl`/* Generated file. Do not modify. */
import { IconSvg, type IconSvgProps } from '../../IconSvg';

export const ${componentName} = (props: IconSvgProps) => <IconSvg size={props.size}>${jsx}</IconSvg>;

`;
};
gajus commented 1 year ago

Here is a full plugin file to save a few minutes for the next person:

import { type JSXIdentifier, type PluginObj, types as t } from '@babel/core';

/**
 * Babel plugin for svgr that removes svg element.
 *
 * @see https://react-svgr.com/docs/custom-transformations/
 * @author https://github.com/gregberge/svgr/issues/769#issuecomment-1236126769
 */
export const removeSvgElement = (): PluginObj => ({
  visitor: {
    JSXElement(path) {
      if ((path.node.openingElement.name as JSXIdentifier).name === 'svg') {
        if (path.node.children.length === 1) {
          path.replaceWith(path.node.children[0]);
        } else {
          path.replaceWith(
            t.jsxFragment(
              t.jsxOpeningFragment(),
              t.jsxClosingFragment(),
              path.node.children,
            ),
          );
        }
      }
    },
  },
});
gajus commented 1 year ago

Just realized there is a better way of doing this: just reference jsx.children

const iconTemplate: Template = ({ componentName, jsx }, { tpl }) => {
  return tpl`/* Generated file. Do not modify. */
import { createElement } from 'react';
import { IconSvg, type IconSvgProps } from '../../IconSvg';

export const ${componentName} = (props: IconSvgProps) => createElement(SvgIcon, props, ${jsx.children});

`;
};
emlai commented 1 year ago

gajus' solution using ${jsx.children} doesn't work in my case, I get an error from @babel/template:

@babel/template placeholder "$1": Cannot replace single expression with an array.
stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

sfc-gh-yalu commented 1 month ago

gajus' solution using ${jsx.children} doesn't work in my case, I get an error from @babel/template:

@babel/template placeholder "$1": Cannot replace single expression with an array.

Same here. I have to use the follow code (source) instead of just ${jsx.children} to fix that error.

${
    jsx.children.length === 1
      ? jsx.children[0]
      : {
          type: "JSXFragment",
          openingFragment: {
            type: "JSXOpeningFragment",
          },
          closingFragment: {
            type: "JSXClosingFragment",
          },
          children: jsx.children,
        }
  }