nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
3.45k stars 383 forks source link

How to handle dynamic icons or icon names? #267

Closed 9uenther closed 11 months ago

9uenther commented 11 months ago

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

benjamincanac commented 11 months ago

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 commented 11 months ago

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';.

benjamincanac commented 11 months ago

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

9uenther commented 11 months ago

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.

benjamincanac commented 11 months ago

@9uenther Can this issue be closed?

goodpixels commented 11 months ago

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.

benjamincanac commented 11 months ago

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 commented 11 months ago

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

benjamincanac commented 11 months ago

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 commented 11 months ago

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 commented 7 months ago

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>
benjamincanac commented 7 months ago

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

Jak3b0 commented 7 months ago

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 commented 5 months ago

@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, https://github.com/nuxt-modules/icon/issues/34 or https://github.com/nuxt-modules/icon/issues/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?

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 commented 5 months ago

@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?

benjamincanac commented 5 months ago

@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 />.