Skip to content

Instantly share code, notes, and snippets.

@Justineo
Last active December 13, 2021 13:09
Show Gist options
  • Save Justineo/fb2ebe773009df80e80d625132350e30 to your computer and use it in GitHub Desktop.
Save Justineo/fb2ebe773009df80e80d625132350e30 to your computer and use it in GitHub Desktop.

Using Icon Components in Vue.js

Icon fonts and SVG sprite are not included here. I'm talking about an icon system that can let users import icons by demand.

There are three major ways of exposing API of an icon component in Vue.js and each one of them has its own pros & cons:

  1. A single component (eg. <v-icon>), let users pass a name/type prop to specify the actual icon.

    Icon data are registered into a global “pool” like:

    // v-icon/flag.js
    import Icon from 'v-icon'
    import { mdiFlag } from '@mdi/js'
    Icon.add('flag', mdiFlag)

    And use like:

    <template>
      <v-icon name="flag" />
    </template>
    
    <script>
    import VIcon from 'v-icon'
    import 'v-icon/flag'
    
    export default {
      components: {
        VIcon
      }
    }
    </script>

    This is the approach adopted in VueAwesome (an icon component with built-in FontAwesome support which I'm maitaining) and IMO is the most ergonomic one ATM. But the link between the name prop and the imported side-effect-only module is implicit and icon data injection is global. It causes problems when you have more than one version of v-icon installed in your dependencies.

    FontAwesome's official Vue.js component took a slightly different approach that icons are explicitly added to the global “pool” by users themselves (maybe I shouldn't have categorized it into this approach):

    import { library } from '@fortawesome/fontawesome-svg-core'
    import { faUserSecret } from '@fortawesome/free-solid-svg-icons'
    
    library.add(faUserSecret)
  2. A single component (eg. <v-icon>), let users pass a data/content prop to create the actual icon.

    Icon data are passed into the component by users themselves:

    <template>
      <v-icon :content="mdiFlag" />
    </template>
    
    <script>
    import VIcon from 'v-icon'
    import { mdiFlag } from '@mdi/js'
    
    export default {
      components: {
        VIcon
      },
      created() {
        Object.assign(this, {
          mdiFlag
        })
      }
    }
    </script>

    This approach is supported by Vuetify (which supports various usages of icons), which is less ergonomic and straightforward but doesn't have the same shortcomes of approach 1.

  3. One component for each icon (eg. <icon-flag/>, <icon-star/>, etc.).

    Each icon may be generated with an icon factory:

    // icon-flag.js
    import { mdiFlag } from '@mdi/js'
    import { createIcon } from 'v-icon'
    
    export default createIcon('flag', mdiFlag)

    And use like:

    <template>
      <icon-flag />
    </template>
    
    <script>
    import { IconFlag } from 'v-icon'
    
    export default {
      components: {
        VIcon,
        IconFlag
      }
    }
    </script>

    This is the most adopted approach in the React community. I'll discuss this approach in the rest of the post.

One component for each icon

I'm going to dig a little further of this approach when we apply it in Vue.js.

In Vue.js we have separeted template and script so components have to be registered via the components option. As we all know this is sometimes cumbersome especially when we need to use a lot of icons in one component (which applies to other components as well).

Vue 2

<template>
  <div>
    <!-- inline -->
    <icon-flag />

    <!-- conditional -->
    <icon-flag v-if="flag" />
    <icon-star v-else />

    <!-- dynamic -->
    <component :is="flag ? IconFlag : IconStar" />
  </div>
</template>

<script>
import { IconFlag, IconStar } from 'foo-icons'

export default {
  components: {
    IconFlag,
    IconStar
  },
  data() {
    return {
      flag: true
    }
  },
  created() {
    Object.assign(this, {
      IconFlag,
      IconStar
    })
  }
}
</script>

As you can see if we want to use icons in :is bindings, we have to manually expose our components to the rendering context. Or we can use string instead of component definition instead, but this will be less friendly to linters and type systems.

<template>
  <div>
    <!-- inline -->
    <icon-flag />

    <!-- conditional -->
    <icon-flag v-if="flag" />
    <icon-star v-else />

    <!-- dynamic -->
    <component :is="flag ? 'icon-flag' : 'icon-star'" />
  </div>
</template>

<script>
import { IconFlag, IconStar } from 'foo-icons'

export default {
  components: {
    IconFlag,
    IconStar
  },
  data() {
    return {
      flag: true
    }
  }
}
</script>

Vue 3

<template>
  <!-- inline -->
  <icon-flag />

  <!-- conditional -->
  <icon-flag v-if="flag" />
  <icon-star v-else />

  <!-- dynamic -->
  <component :is="flag ? IconFlag : IconStar" />
</template>

<script>
import { ref } from 'vue'
import { IconFlag, IconStar } from 'foo-icons'

export default {
  components: {
    IconFlag,
    IconStar
  },
  setup() {
    const flag = ref(true)

    return {
      flag,
      IconFlag,
      IconStar
    }
  }
}
</script>

When using string :is bindings, the <script> part becomes:

import { ref } from 'vue'
import { IconFlag, IconStar } from 'foo-icons'

export default {
  components: {
    IconFlag,
    IconStar
  },
  setup() {
    const flag = ref(true)

    return {
      flag
    }
  }
}

Plus if we adopt something like <script components>:

<template>
  <!-- inline -->
  <icon-flag />

  <!-- conditional -->
  <icon-flag v-if="flag" />
  <icon-star v-else />

  <!-- dynamic -->
  <component :is="flag ? 'icon-flag' : 'icon-star'" />
</template>

<script components>
export { IconFlag, IconStar } from 'foo-icons'
</script>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const flag = ref(true)

    return {
      flag
    }
  }
}
</script>

Or with the proposed <script setup>:

<script setup>
import { ref } from 'vue'

export const flag = ref(true)
</script>
@Justineo
Copy link
Author

Obviously with the latest <script setup>:

<template>
  <!-- inline -->
  <icon-flag />

  <!-- conditional -->
  <icon-flag v-if="flag" />
  <icon-star v-else />

  <!-- dynamic -->
  <component :is="flag ? IconFlag : IconStar" />
</template>

<script setup>
import { IconFlag, IconStar } from 'foo-icons'
import { ref } from 'vue'

const flag = ref(true)
</script>

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