Add a class to every child of a slot

Viewed 701

I'm trying to set up a component with a slot, that when rendered adds a class to every children of that slot. In a very simplified manner:

<template>
<div>
  <slot name="footerItems"></slot>
</div>
</template>

How would I go about this? My current solution is to add the class to the elements in an onBeforeUpdate hook:

<script setup lang="ts">
import { useSlots, onMounted, onBeforeUpdate } from 'vue';

onBeforeUpdate(() => addClassToFooterItems());
onMounted(() => addClassToFooterItems());

function addClassToFooterItems() {
  const slots = useSlots();

  if (slots && slots.footerItems) {
    for (const item of slots.footerItems()) {
      item.el?.classList.add("card-footer-item");
    }
  }
}
</script>

However, the elements lose the styling whenever it's rerendered (using npm run serve) and also jest tests give me a warning:

    [Vue warn]: Slot "footerItems" invoked outside of the render function: this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.

Should I move the slot to its own component and use a render function there? But even then I'm not sure how to edit the children to add the classes or how to produce several root level elements from the render function.

2 Answers

You need the class for apply some style?

You can pass the style or class with props

And apply the class or style with :style="" or :class="", it's explained here

But just for apply a class, just :class="" solves your problem

So, I managed to solve this in an incredibly hacky way, but at least my issue with re-rendering doesn't happen anymore and jest doesn't complain. I wrote a component with a render function that appends that class to all children


<template>
<render>
  <slot></slot>
</render>
</template>

<script setup lang="ts">
import { useSlots } from 'vue';

const props = defineProps<{
  childrenClass: string;
}>();

function recurseIntoFragments(element: any): any {
  if (element.type.toString() === 'Symbol(Fragment)'
    && element.children[0].type.toString() === 'Symbol(Fragment)'
  ) {
    return recurseIntoFragments(element.children[0]);
  } else {
    return element;
  }
}

const render = () => {

  const slot = useSlots().default!();
  recurseIntoFragments(slot[0]).children.forEach((element: any) => {
    if (element.props?.class && !element.props?.class.includes(props.childrenClass)) {
      element.props.class += ` ${props.childrenClass}`;
    } else {
      element.props.class = props.childrenClass;
    }
  });

  return slot;
}
</script>

Then I would just wrap the slot in this component to add the class to the children elements:

<template>
<div>
  <classed-slot childrenClass="card-footer-item">
    <slot name="footerItems"></slot>
  </classed-slot>
</div>
</template>

I would gladly accept any answer that improves upon this solution, especially:

  • Any tips to type it. All those anys feel wonky but I find it very impractical working with Vue types for slots since they are usually unions of 3 or 4 types and the only solution is to wrap these in type checks
  • Anything that improves its reliability since it seems that it'd crash in any slightly different setup than the one I intended
  • Any recommendation based on Vue's (or TS) best practices, since this looks very amateurish.
  • Really any other way to test for symbol equality because I know none

EDIT This is my latest attempt, a render function in a file ClassedSlot.js:

import { cloneVNode } from 'vue';

function recursivelyAddClass(element, classToAdd) {
  if (Array.isArray(element)) {
    return element.map(el => recursivelyAddClass(el, classToAdd));
  } else if (element.type.toString() === 'Symbol(Fragment)') {
    const clone = cloneVNode(element);
    clone.children = recursivelyAddClass(element.children, classToAdd)
    return clone;
  } else {
    return cloneVNode(element, { class: classToAdd });
  }
}

export default {
  props: {
    childrenClass: {
      type: String,
      required: true
    },
  },

  render() {
    const slot = this.$slots.default();

    return recursivelyAddClass(slot, this.$props.childrenClass);
  },
};

The usage of this component is exactly the same as in the previous one. I'm kinda happy with this solution, seems more robust and idiomatic. Note that it's javascript because I found it really hard to type these functions correctly.

Related