Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support inline SVG icons #3155

Closed
dword-design opened this issue Jan 19, 2021 · 11 comments
Closed

Support inline SVG icons #3155

dword-design opened this issue Jan 19, 2021 · 11 comments

Comments

@dword-design
Copy link
Contributor

dword-design commented Jan 19, 2021

Description

Currently the icon system depends on icon fonts. Meaning you set a class and that class sets a special font family that contains the respective icons.

This approach has its pros and cons. On the pro side is the ease of use because you can just set a class and you have an icon. On the con side is the bundle size and the page loading time. Explicitly setting the icon set leads to dependencies between the components and the app config.

A different approach is to directly render inline SVGs instead. There are webpack loaders that allow to load SVG files and use them as components. Then you can do something like this:

<template>
  <mdi-rss aria-hidden="true" class="icon" />
</template>

<script>
import MdiRss from '@mdi/svg/svg/rss.svg'

export default {
  components: {
    MdiRss,
  },
}
</script>

This is currently not working with Buefy because the b-icon component requires the icon property to be a string, and then constructs css classes and passes them to the underlying defaultIconComponent. You have to create the icons yourself without using b-icon (see above). Also, you cannot use b-button and the dialog system, which implicitly use b-icon. A better approach would be to leave the class creation to the defaultIconComponent and to widen up the allowed icon types to objects and strings, so we can pass the SVG icon through. Then you could do things like this:

<template>
  <div>
    <b-icon :icon="MdiRss" />
    <b-button :icon-left="MdiRss" />
  </div>
</template>

<script>
import MdiRss from '@mdi/svg/svg/rss.svg'

export default {
  computed: {
    MdiRss: () => MdiRss,
  },
}
</script>

I've successfully used the approach in this project by mainly directly using the bulma classes and would love use it within Buefy.

Why Buefy need this feature

It allows to use the common technique of inline SVG icons with Buefy to decrease the bundle size and remove the dependency between the component and the app config.

Looking forward to your thoughts. I'm also open to contributing a PR if people like the idea.

@zonque
Copy link

zonque commented Apr 19, 2021

This limitation is in fact a real issue for deployments on systems with limited storage resources. The MDI web font is some orders of magnitude larger than my application, and there is currently no alternative to the (discouraged) way of embedding all icons through the font.

@dword-design maybe a PR is worth a shot to demonstrate how things would look like?

@johnpuddephatt
Copy link

johnpuddephatt commented May 13, 2021

I've also been concerned about this due to the size of the MDI font, to the extent that I started exploring creating my own replacement icon component... and at this point I realised it's not difficult to use inline SVG icons through the existing Buefy icon component!

  1. Configure Buefy to use an icon component that we'll create in step 2.
import SvgIcon from './SvgIcon'
Vue.use(Buefy, {
  defaultIconPack: null,
  defaultIconComponent: SvgIcon
});
  1. Create the SvgIcon component referenced above. (e.g. ./SvgIcon.vue). The example below uses Vue Feather Icons but it should be possible with others. This component receives your icon name, converts it from kebab-case to camelCase and loads the right icon component. The CSS at the end is necessary to override some existing CSS. You'll need to import all icons you're using. I've just added two in this example – ArrowRightIcon and CheckIcon.
<template>
  <component :is="iconName"></component>
</template>
<script>
import { ArrowRightIcon, CheckIcon } from 'vue-feather-icons'
export default {
  components: {
    ArrowRightIcon,
    CheckIcon
  },
  props: ['icon','size'],
  computed: {
    iconName: function() {
      return `${ this.icon[1].replace(/-./g, x=>x.toUpperCase()[1]).replace(/\b\w/g, c => c. toUpperCase()) }Icon`;
    }
  },
}
</script>

<style lang="scss">
  .icon svg {
    stroke-width: 2 !important;
    fill: none !important;
  }
</style>
  1. Use as you normally would in Buefy, e.g. <b-icon icon="arrow-right"> or <b-button icon-left="check">

@dword-design
Copy link
Contributor Author

@johnpuddephatt Oh wow that's actually a great solution. At least way better than the hackery I'm using right now.
Addition: You can use change-case to convert between casing, which should be cleaner than the regex solution.

I think we still need the issue so we can directly pass the icons through and have compile-time checks like unused icon imports etc.

Haven't tried the alternative by @johnpuddephatt yet, I'll write in case there's anything.

@r-rayns
Copy link

r-rayns commented May 15, 2021

@johnpuddephatt Thank you for this, it was a great help to me. Just to expand on your solution with some extra details:

To the icon prop Buefy passes an array, the first element is the icon pack name (not used in this scenario) and the second element is the icon name.

To the size prop Buefy passes the value set against the it's customSize prop.

There is also a third prop class to which Buefy passes an array containing customClass

You can see in Buefy's Icon component where Buefy instantiates a custom icon component:

   <component
            v-else
            :is="useIconComponent" // this will dynamically load in our SvgIcon component
            :icon="[newPack, newIcon]" 
            :size="newCustomSize"
            :class="[customClass]"/>

Vue feather icons take a size and class prop. We can pass these through using v-bind:

<template>
  <component :is="iconName" v-bind="iconProps"></component>
</template>
<script>
  // ...
  props: ['icon','size','class],
  computed: {
    iconName: function() {
      // ...
    },
    iconProps: function() {
      return { size: this.size || '1x', class: ...this.class || '' }
    },
  },
}
</script>

Then something like the below should pass through the customSize and customClass props to our SvgIcon component and into the vue feather icon.

<b-icon icon="arrow-right" customSize="4x" customClass="my-class">

@arthur-clq
Copy link

@johnpuddephatt @End-S Thanks for the solutions.

I have been using Bootstrap-Vue for a few of my past projects and I feel that their approach of using slots to allow basically any type of icon works quite well. Perhaps this could be considered?

@dword-design
Copy link
Contributor Author

dword-design commented Jul 5, 2021

@johnpuddephatt @End-S Thanks for the solutions.

I have been using Bootstrap-Vue for a few of my past projects and I feel that their approach of using slots to allow basically any type of icon works quite well. Perhaps this could be considered?

@arthur-clq Isn't this exactly like it's done in Buefy with a b-icon component?

<template>
  <div class="h2 mb-0">
    <b-icon-arrow-up></b-icon-arrow-up>
    <b-icon-exclamation-triangle-fill></b-icon-exclamation-triangle-fill>
  </div>
</template>
<template>
  <div class="h2 mb-0">
    <b-icon icon="arrow-up"></b-icon>
    <b-icon icon="exclamation-triangle"></b-icon>
  </div>
</template>

@arthur-clq
Copy link

@arthur-clq Isn't this exactly like it's done in Buefy with a b-icon component?

@dword-design Hi, sorry I meant for other components. For example Bootstrap-Vue button allows a default slot, so if I do not want to use the b-icon component, I can put in my own svg in there.

I think this approach could be considered because it gives more flexibility given that b-icon limits which icon pack can be used.

@dword-design
Copy link
Contributor Author

@arthur-clq Alright I see. Makes sense to me or make the icon data type more flexible in Buefy.

@dword-design
Copy link
Contributor Author

I got into this topic again and one downside of @johnpuddephatt s solution is that you cannot use the vue/no-unused-components rule when passing the icon components to the components property in a page. It only works when declaring them globally. I'm still counting for relaxing the icon prop type to any type so the defaultIconComponent can decide how to handle it.

@stale
Copy link

stale bot commented Nov 12, 2022

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.

@stale stale bot added the stale label Nov 12, 2022
@stale stale bot closed this as completed Jun 18, 2023
@kikuomax
Copy link
Collaborator

kikuomax commented Feb 1, 2024

I got into this topic again and one downside of @johnpuddephatt s solution is that you cannot use the vue/no-unused-components rule when passing the icon components to the components property in a page. It only works when declaring them globally. I'm still counting for relaxing the icon prop type to any type so the defaultIconComponent can decide how to handle it.

@dword-design I opened a new discussion for improving the flexibility of Icon component:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants