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

How to handle dynamic icons or icon names? #267

Closed
9uenther opened this issue Jun 8, 2023 · 16 comments
Closed

How to handle dynamic icons or icon names? #267

9uenther opened this issue Jun 8, 2023 · 16 comments
Labels
question Further information is requested

Comments

@9uenther
Copy link
Contributor

9uenther commented Jun 8, 2023

I want to pass the names dynamically. But if they have not been loaded before, the icon does not appear.

<UIcon :name="myDynamicIcon" />
const myDynamicIcon = ref('i-mdi-file-tree');

Is it possible to use an IconifyIcon object like this?

import { getIcon } from '@iconify/vue';
const myDynamicIcon = ref(getIcon('i-mdi-file-tree'));

Docs: https://iconify.design/docs/icon-components/vue/get-icon.html

@9uenther 9uenther added the question Further information is requested label Jun 8, 2023
Copy link
Member

You cannot load icons dynamically with the UIcon component, you need to install iconify collections. You can learn more about it in the documentation: https://ui.nuxtlabs.com/getting-started/theming#icons

However, you can use nuxt-icon module to achieve this.

@9uenther
Copy link
Contributor Author

9uenther commented Jun 9, 2023

I played around and made a few adjustments so that i can pass the IconifyIcon object for the icon.vue aka UIcon component in the name. I write the svg stuff directly into the style attribute.

<UIcon :name="getIcon(myDynamicIconName)" />
<template>
  <span :class="getName" :style="getStyle" />
</template>

<script>
import { computed, defineComponent } from "vue";
import { classNames } from "../../utils";
export default defineComponent({
  props: {
    name: {
      type: [String, Object],
      required: true
    },
    class: {
      type: String,
      default: null
    }
  },
  setup(props) {
    const getName = computed(() => {
      
      if(typeof props.name === 'string') {
        return classNames(
          props.name,
          props.class
        );
      }

      return props.class;
    });
    const getStyle = computed(() => {
      
      if(typeof props.name === 'object') {
        const data = {...props.name};
        const svgUrl = `url("data:image/svg+xml,${encodeURIComponent(
          `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${data.left} ${data.top} ${data.width} ${data.height}' width='${data.width}' height='${data.height}'>${data.body}</svg>`
        )}")`;

        return {
          '--svg': svgUrl,
          'background-color': 'currentColor',
          '-webkit-mask-image': `var(--svg), ${svgUrl}`,
          'mask-image': 'var(--svg)',
          '-webkit-mask-repeat': 'no-repeat',
          'mask-repeat': 'no-repeat',
          '-webkit-mask-size': '100% 100%',
          'mask-size': '100% 100%',
        };
      }

      return null;
    });
    return {
      getName,
      getStyle
    };
  }
});
</script>

My own getIcon component using import { iconExists, getIcon, loadIcon } from '@iconify/vue';.

Copy link
Member

That's pretty much what the IconCSS component of Nuxt Icon does: https://github.com/nuxt-modules/icon#css-icons

@9uenther
Copy link
Contributor Author

9uenther commented Jun 9, 2023

Ah ok, i didn't know that yet. I got it from the i-mdi... CSS class and replaced a few values manually. My component loads the icon as needed something like this:

<UIcon :name="getIcon(myDynamicIconName)" />
// composables/getIcon.js
import { iconExists, getIcon, loadIcon } from '@iconify/vue';
import { computed, ref } from 'vue';

const icons = ref({});

const loader = async (icon) => {

    if (!iconExists(icon)) {
        await loadIcon(icon)
        .then((data) => {
            icons.value[icon] = data;
        });
    } else if (!icons.value[icon]) {
        icons.value[icon] = getIcon(icon);
    }

    return icons.value[icon];
}

export default (icon) => {
    const iconLoader = loader(icon);
    return icons.value[icon];
}

This is more practical in my app than defining all the icons first. Whereby some of them stem from user input and i would have to load all the icons.

Copy link
Member

@9uenther Can this issue be closed?

@goodpixels
Copy link

I don't think this should be closed - it's a very common use case, for example, we asynchronously fetch menu items that are stored in the CMS, and we have no idea what icons are going to be used by the editors.

Ideally, the icon component should accept a string, and resolve the icon at runtime.

Copy link
Member

As mentioned previously, for this case you should use https://github.com/nuxt-modules/icon.

There is also no issue using both, for example on Volta we use UIcon for all the icons defined within the app which are bundled and IconCSS from nuxt-icon for the milestones where the user can pick their own icon.

@goodpixels
Copy link

Thanks for a swift reply Ben! What is the reason for not using Nuxt Icon by default if you don't mind me asking?

Copy link
Member

Historically nuxt-icon was the Icon component we made for this library before making it open-source and Atinux created a module out of it.

However, since this ui library was originally made for Volta and when this package by egoist was released https://github.com/egoist/tailwindcss-icons I made the choice to switch to a system with bundled icons like UnoCSS does: https://unocss.dev/presets/icons.

It's a tiny bit more work as you have to install the iconify packages for the collections you want to use but this makes icons instantly load as they are bundled in your css instead of fetching them from the Iconify API when rendering the page the first time.

@9uenther
Copy link
Contributor Author

In general, the component is fine. However, i would extend it by dynamic use, as i have already indicated above.

I think Iconify also pretty perfect, because i can add my own icons with relatively little effort or make them available via api.

@Jak3b0
Copy link

Jak3b0 commented Oct 29, 2023

As another work around, I created a dummy component that renders the icons that are "dynamic". This dummy component is not used anywhere but allows the icons to be included. :)

<script lang="ts" setup>

// NOTE: This is just a way to whitelist icons for the app. It's not used anywhere.

const icons = [
	"i-heroicons-computer-desktop",
	"i-heroicons-moon",
	"i-heroicons-sun",
];
</script>

<template>
    <UIcon v-for="icon in icons"
                 :key="icon"
                 :name="icon"
                 class="w-8 h-8" />
</template>

Copy link
Member

@Jak3b0 You can also put those in comments anywhere in your app, they will be picked up by Tailwind.

@Jak3b0
Copy link

Jak3b0 commented Nov 4, 2023

Hi @benjamincanac , thanks for your response!

Not sure I understand the relation with Tailwind in this case though. I had the icon names defined in an array of objects inside a computed property and the icons were not included.

@Neo-Zhixing
Copy link

Neo-Zhixing commented Dec 7, 2023

@benjamincanac No offense intended here but this is a very bad design decision.

It's a tiny bit more work as you have to install the iconify packages for the collections you want to use but this makes icons instantly load as they are bundled in your css instead of fetching them from the Iconify API when rendering the page the first time.

So the problem we're trying to solve here is that "nuxt-icon causes icons to not instantly load", right?

And I would agree that it's a pretty bad problem. The iconify API is not the most reliable, and occasionally icons don't render on the Nuxt UI official website.

This is caused by a known issue in nuxt-icon, nuxt/icon#34 or nuxt/icon#99

Basically, nuxt-icon doesn't load the icon on the server side, and instead opt to call the iconify API from the client at all times.

So the reasonable solution to the problem of "nuxt-icon icons doesn't instantly load" would be "fix nuxt-icon so that icons can be injected and bundled at build time".

Instead of doing that, here we introduced another dependency so that icons can be injected by tailwind as a CSS icon.

What's the problem with that?

  • Dynamic icons, which was partially addressed in feat(Icon): switch to nuxt-icon with dynamic prop or app config #862. feat(Icon): switch to nuxt-icon with dynamic prop or app config #862 basically falls back to the nuxt-icon component when dynamic = true. But then with that, you once again lose the ability to allow icons to load instantly, which was why egoist/tailwindcss-icons was introduced in the first place.
  • Behavior consistency. nuxt-icon allows you to define custom Icon API sources using addIconProvider. egoist/tailwindcss-icons expect you to provide them in nuxt.config.ts. I can use bundle:name as the name property in "static" icons but not dynamic icons. That means for any custom icons I have to package them twice, once for static icons as an npm package and once for dynamic icons on my custom iconify server.
  • Implementation discrepancy. nuxt-icon embeds the SVG into the DOM. egoist/tailwindcss-icons uses CSS icons. monotone icons are image-mask. Image icons are background-image. That means if I want to style a subset of the paths in the SVG, I can do that with nuxt-icon, but not egoist/tailwindcss-icons.

What I'm trying to say here is that by introducing egoist/tailwindcss-icons you're significantly complicating an otherwise simple problem. Just go into nuxt-icon, help out @atinux, and fix nuxt-icon so that it can preload icons. Call useAsyncData to fetch the icons on the server side so they're available as soon as the page loads. That way you have one unified method of loading icons, instead of introducing a whole lot of mental overload for a problem as trivial as showing icons.

@Sazzels
Copy link

Sazzels commented Dec 22, 2023

@benjamincanac how could i use dynamic or nuxt-icon with DropdownItem.

currently DropdownItem icon allows only to be string.

could that be extended to also allow string | UIcon so dynamic could be passed?

Copy link
Member

@Sazzels This is unfortunately not possible at the moment, the dynamic prop only works on the Icon component or globally through the app.config.ts. You might be able to override the slots to put an <UIcon dynamic />.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

6 participants