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

[Feature Request] Iconify Support #7821

Closed
tankerkiller125 opened this issue Jul 13, 2019 · 12 comments
Closed

[Feature Request] Iconify Support #7821

tankerkiller125 opened this issue Jul 13, 2019 · 12 comments
Labels
C: VIcon VIcon T: feature A new feature
Milestone

Comments

@tankerkiller125
Copy link

Problem to solve

Currently, the icons supported require loading the full CSS for icons, although this certainly works it adds quite a bit of bloat to the loading times and on very slow mobile networks can cause serious performance impacts. Generally speaking this the way everyone has always loaded icons and I say that continue to do so. However, we also need an alternative that allows loading the SVG icons.

Proposed solution

I am proposing support for https://iconify.design/ this would allow for potentially much faster load times (only load the icons on the page) and it would allow for the use of many different font packs without loading in any extra CSS or any bloat. The JS that runs it is also very small and in my own testing results in much better icon load times.

I propose that we continue to use the icon="" prop. Maybe in the same way we currently handle Material Design icons and font awesome we could use something like icon="iconify-mdi:google" where iconify- informs the vuetify icon render to use the iconify format. and the mdi:google gets passed to the data-icon="" part of the iconify format.

Another option would be to completely replace the current icon system and completely replace it with iconify as iconify already includes the Google Material Design Icons (whats currently default) as well as font-awesome, MDI and many more. Plus it could potentially actually decrease the amount of code logic required to create the icon HTML.

@ghost ghost added the S: triage label Jul 13, 2019
@KaelWD
Copy link
Member

KaelWD commented Jul 15, 2019

This looks like it would cause the same problems as FontAwesome: #3505 (comment)

We have support for svg icons so you can already import only what's being used, albeit manually.

@tankerkiller125
Copy link
Author

Iconify is open source, so maybe a custom implementation that actually would work for Vuetify with the use of their API? And then also adding a Vuetify option to change the API URL (if someone wanted to host their own iconify)

@cyberalien
Copy link

Author of Iconify here. I'd love to make Iconify work with Vue and solve any issues Vue users might have.

Looks like main issue is that Iconify messes with DOM. However it doesn't really need to. It messes with DOM only if it sees icon placeholder, such as <span class="iconify" data-icon="mdi:home" /> to change it to <svg .../>.

But there are other ways to handle icons. Iconify script has many functions that can be used to render SVG without Iconify messing with DOM. Iconify exposes Iconify variable to global namespace that has many functions. I wrote short documentation for those functions: https://iconify.design/docs/functions/

Using those functions you can retrieve full SVG code and render it instead of using placeholder. For example

let icon1 = Iconify.getSVG('mdi:home');
let icon2 = Iconify.getSVG('fa-regular:home', {'data-rotate': '90deg'});
// ... render those icons

That's the basic version.

However in reality its a bit more complex than that because icons need to be loaded first. Usually Iconify looks for icon placeholders, then requests icon data from API (or preloads them from localStorage if they were requested on previous page load), only then replaces placeholders with SVG. So rendering is asynchronous.

Iconify exposes functions to help with that:

  • Iconify.preloadImages(['mdi:home', 'fa-regular:home']); - loads icons
  • Iconify.iconExists('mdi:home'); - checks if icon exists
    Also there is DOM event you can subscribe to. It is fired when new icons have been loaded: IconifyAddedIcons.

With combination of those functions and event, you can build your own code to load icons, be notified when icons have loaded and render them.

Another solution is to bundle icons with package instead of loading icons from API. I've built React component that works like that: https://github.com/iconify/iconify-react so something like that might be built for Vue too.

I'm open to any suggestions and ideas. My goal is to build universal framework for icons and make it available on as many platforms as possible, so designers and users would have huge choice of icons on whatever platform they choose, including Vue.

@tankerkiller125
Copy link
Author

Thanks @cyberalien I think I might try to make a super basic Vue component (as a separate project) to see how viable it truly is using the functions you provided, if I get that working hopefully whatever I create can be ported for Vuetify (or just be used with Vuetify)

@cyberalien
Copy link

So I've got it working as basic component. Since I've spent only 1 day with Vue, this component is far from optimal and possibly has some bad coding, but it does work.

Requires Iconify 1.0.3 that has been released today. It fixes bug that prevented import from working properly and adds new function getSVGObject() that instead of rendering SVG returns attributes and content, so it could be used to generate component.

Component:

<script>

import Iconify from '@iconify/iconify';

// This is for debug - disables caching, so on each reload new API request would be sent
Iconify.setConfig('localStorage', false);
Iconify.setConfig('sessionStorage', false);
Iconify.setConfig('defaultAPI', 'https://api.iconify.design/{prefix}.js?t=' + Date.now() + '&icons={icons}');

/**
 * List of component properties to map to data- attributes
 *
 * @type {Array}
 */
const dataAttributes = ['inline', 'width', 'height', 'rotate', 'flip', 'align'];

/**
 * Array of components to update when icon has been loaded
 *
 * @type {Array}
 */
let listeners = [];

/**
 * Listen to IconifyAddedIcons that is fired whenever new icons are loaded from API
 */
document.addEventListener('IconifyAddedIcons', function() {
    listeners = listeners.filter(item => {
        if (Iconify.iconExists(item.icon)) {
            item.instance.iconLoaded();
            return false;
        }
        return true;
    });
});

/**
 * Export component
 */
export default {
    name: 'Icon',
    render: function(createElement) {
        // Check if icon exists, render span if not
        if (!Iconify.iconExists(this.name)) {
            return createElement('span', {
                attrs: {
                    style: 'display: inline-block; width: 1em;'
                }
            });
        }

        // Convert component properties to Iconify properties
        let props = {};
        if (this.color !== void 0) {
            props.style = 'color: ' + this.color + ';';
        }

        // All optional properties
        dataAttributes.forEach(key => {
            if (this[key] !== void 0) {
                props['data-' + key] = this[key];
            }
        });

        // Get SVG attributes and body
        let icon = Iconify.getSVGObject(this.name, props);
        return createElement('svg', {
            attrs: icon.attributes,
            domProps: {
                innerHTML: icon.body
            }
        });
    },
    props: {
        name: {
            type: String,
            required: true
        },
        // If one dimension is missing, it will be generated using width/height ratio of icon.
        // By default height is '1em', width is calculated from icon's width/height ratio.
        width: String,
        height: String,

        // If true, icon will have vertical alignment, so it renders similar to glyph font.
        // If false, icon will not have vertical alignment, so it renders as image above text baseline.
        // Use false for decorations, true when converting from legacy font code.
        // Default value is true
        inline: Boolean,

        // Color string, optional. If missing, currentColor is used
        color: String,

        // Rotation. Values are '90deg', '180deg', '270deg'. Rotation is done by rotating SVG content, not CSS rotation.
        rotate: String,

        // Flip. Values are 'horizontal', 'vertical' or 'horizontal,vertical' (last one is identical to 180deg rotation)
        flip: String,

        // Alignment. Used only if setting custom width and height that do not match icon's width/height ratio.
        // Value is comma separated list of alignments:
        // Horizontal: left, center, right
        // Vertical: top, middle, bottom
        // Crop: slice, meet
        align: String
    },
    beforeMount: function() {
        // Status of icon loading. false = not loading, string = icon name
        this._loadingIcon = false;
        this.loadIcon();
    },
    beforeUpdate: function() {
        // Try to load different icon if name property was changed
        this.loadIcon();
    },
    methods: {
        /**
         * Load icon from API
         */
        loadIcon: function() {
            if (this._loadingIcon !== this.name && !Iconify.iconExists(this.name)) {
                if (this._loadingIcon !== false) {
                    // Already loading with different icon name - remove component with old icon name from listeners list
                    this.removeListener();
                }

                // Add to queue
                this._loadingIcon = this.name;
                listeners.push({
                    instance: this,
                    icon: this._loadingIcon
                });

                // Add to Iconify loading queue
                // Iconify will execute queue on next tick, so its safe to add icons one by one
                Iconify.preloadImages([this.name]);
            }
        },

        /**
         * Remove component from Iconify event listener
         */
        removeListener: function() {
            listeners = listeners.filter(item => item.instance !== this);
        },

        /**
         * Icon has loaded. Force component update
         */
        iconLoaded: function() {
            this._loadingIcon = false;
            this.$forceUpdate();
        }
    },
    beforeDestroy: function() {
        if (this._loadingIcon !== false) {
            this.removeListener();
        }
    }
}

</script>

Test app, based on default App.vue:

<template>
  <v-app>
    <v-content>
      <div style="font-size: 24px; box-shadow: 0 0 2px #ccc">Few icons from same collection to test Iconify loading queue: <Icon name="uil-bug" /> <Icon name="uil-cloud-slash" /> <Icon name="uil-exclude" /></div>
      <div style="font-size: 24px; box-shadow: 0 0 2px #ccc">Icon: <Icon v-bind="icon" /></div>
      icon: <a v-on:click="icon.name = 'ant-design:home-outline'">ant-design:home-outline</a>, <a v-on:click="icon.name = 'mdi-home'">mdi-home</a>, <a v-on:click="icon.name = 'fa-solid:home'">fa-solid:home</a>, <a v-on:click="icon.name = 'icomoon-free:home'">icomoon-free:home</a>, <a v-on:click="icon.name = 'dashicons:admin-home'">dashicons:admin-home</a>, <a v-on:click="icon.name = 'flat-color-icons:home'">flat-color-icons:home</a>, <a v-on:click="icon.name = 'octicon-home'">octicon-home</a>, <a v-on:click="icon.name = 'uil-home'">uil-home</a><br />
      inline: <a v-on:click="icon.inline = true">inline</a> (has vertical alignment, like glyph font), <a v-on:click="icon.inline = false">block</a> (no vertical alignment, like image)<br />
      color: <a v-on:click="icon.color = 'red'">red</a>, <a v-on:click="icon.color = '#4a4'">green</a><br />
      rotate: <a v-on:click="icon.rotate = '0'">0deg</a>, <a v-on:click="icon.rotate = '90deg'">90deg</a>, <a v-on:click="icon.rotate = '180deg'">180deg</a>, <a v-on:click="icon.rotate = '270deg'">270deg</a><br />
      flip: <a v-on:click="icon.flip = ''">none</a>, <a v-on:click="icon.flip = 'horizontal'">horizontal</a>, <a v-on:click="icon.flip = 'vertical'">vertical</a>, <a v-on:click="icon.flip = 'horizontal,vertical'">both</a><br />
    </v-content>
  </v-app>
</template>

<script>
import Icon from './components/Icon';

export default {
  name: 'App',
  components: {
    Icon
  },
  data: function() {
    return {
        icon: {
            name: 'mdi-home',
            color: '',
            inline: true,
            rotate: '',
            flip: ''
        }
    };
  }
}
</script>

@KaelWD
Copy link
Member

KaelWD commented Jul 20, 2019

This is something we could consider adding, but it's probably more suitable as a standalone vue component that we can offer integration with like we do FontAwesomeIcon

@tankerkiller125
Copy link
Author

@cyberalien I've taken your code and converted it into a full proper Vue plugin, I still need to write the proper documentation and some test for it but I can confirm it works properly. You can find it here I think this may be the best way to get it implemented for Vue in general.

@cyberalien
Copy link

Forgot about this issue, got reminded after receiving notification about issue being closed.

A while ago I've created Vue components for Iconify: @iconify/vue. It is available for Vue 2 and Vue 3.

Vue 2 documentation: https://docs.iconify.design/implementations/vue2/
Vue 3 documentation: https://docs.iconify.design/implementations/vue/

@ThaDaVos
Copy link

ThaDaVos commented Sep 30, 2022

I've created this little handy helper for Vuetify 3 custom icon set:

import { h } from "vue";
import type { IconSet, IconProps, IconAliases } from "vuetify";
import { Icon } from "@iconify/vue";

export const iconify: (set: string) => IconSet = (set) => ({
    component: (props: IconProps) =>
        h(Icon, {
            icon: `${set}:${props.icon}`,
            disabled: props.disabled,
        })
});

Which can be used as:

createVuetify({
  icons: {
    sets: {
      flags: iconify('flags')
    }
  }
})

And in template as:

<template>
  <v-icon>flag:nl-4x3</v-icon>
</template>

Also using the amazing https://marketplace.visualstudio.com/items?itemName=antfu.iconify&ssr=false#qna vscode extension which also gives intellisense with this syntax:
image

@danielduckworth
Copy link

I've created this little handy helper for Vuetify 3 custom icon set:

Which can be used as:

createVuetify({
  icons: {
    sets: {
      flags: iconify('flags')
    }
  }
})

@ThaDaVos, Excellent! I'm working with a Vuetify theme that has ton <v-icon> components all over the place. Turns out a regex to find mdi-(.*) and replace mdi:$1 was only half the solution. Your handy helper is exactly what I needed to maintain the Vuetify components but render the Iconify icon data. Thanks, you're a star!

@victortrusov
Copy link

victortrusov commented Jan 4, 2024

I'm testing this solution for adding iconify mdi set as a vuetify default one, so I don't need to import material CSS:

import { h } from "vue";
import type { IconSet, IconProps } from 'vuetify';
import { aliases } from 'vuetify/iconsets/mdi';
import { Icon } from "@iconify/vue";
// in case of using nuxt-icon:
// import { Icon } from '#components';

const iconifyMdi: IconSet = {
  component: (props: IconProps) => h(
    'i', 
    {},
    [
      h(Icon, {
        name: props.icon.replace('mdi-', 'mdi:'),
        disabled: props.disabled,
      }),
    ]),
};

createVuetify({
    icons: {
      defaultSet: 'iconifyMdi',
      aliases,
      sets: {
        iconifyMdi,
      },
    },
  })

Seems to work just fine

@cyberalien
Copy link

I recommend using iconify-icon package instead of @iconify/vue. It is a web component, renders icons in Shadow DOM, so it eliminates several SSR issues because Vue no longer need to match content rendered on server and in browser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: VIcon VIcon T: feature A new feature
Projects
None yet
Development

No branches or pull requests

6 participants