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:
-
A single component (eg.
<v-icon>
), let users pass aname
/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 ofv-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)
-
A single component (eg.
<v-icon>
), let users pass adata
/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.
-
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.
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).
<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>
<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>
Obviously with the latest
<script setup>
: