Assign a class to each offspring of a slot

I am currently in the process of setting up a component with a slot that, upon rendering, adds a specific class to each child within that slot. To simplify:

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

How can I achieve this? My current approach involves adding the class to elements using 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, when rerendered (using npm run serve), the elements lose their styling. Additionally, Jest tests yield 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 consider moving the slot to its own component and utilizing a render function there? Even then, I am unsure how to manipulate children to add classes or generate multiple root-level elements through the render function.

Answer №1

After some creative problem-solving, I found a rather unconventional way to tackle this issue. By creating a component with a render function that adds a specific class to all children elements, I successfully resolved the re-rendering problem in my Vue application.


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

To apply this solution, simply wrap the slot in this custom component:

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

I'm open to suggestions for enhancing this approach, particularly regarding:

  • Improving type checking to eliminate the heavy usage of 'any' types
  • Enhancing reliability to prevent potential crashes in different scenarios
  • Fine-tuning based on Vue and TypeScript best practices
  • Exploring alternative methods for comparing symbols

UPDATE: I have refined the solution by creating a separate file ClassedSlot.js housing a more structured render function:

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);
  },
};

This updated version appears to be more robust and aligned with Vue's conventions. Please note that I opted for JavaScript due to complexities in typing these functions accurately.

Answer №2

@Sophia's response is helpful.
I have made some modifications to allow for the specification of the component name.

import { FunctionalComponent, StyleValue, cloneVNode, Ref, ComputedRef, unref } from "vue";

type MaybeRef<T> = T | Ref<T>;

/**
 * @param additionalProperties - ref or regular object
 * @returns
 * 
 * @example
 * 
 * const StyledComponent = applyCssStyles({class: 'text-red-500'});
 * 
 * const ComponentA = () => {
 *  return <StyledComponent><span>text is red-500</span></StyledComponent>
 * }
 */
export const applyCssStyles = (additionalProperties: MaybeRef<{
  class?: string,
  style?: StyleValue
}>) => {
  const FnComponent: FunctionalComponent = (_, { slots }) => {
    const defaultSlot = slots.default?.()[0];
    // it could be a Fragment or something else? I'm just disregarding those cases here
    if (!defaultSlot) return;
    const node = cloneVNode(defaultSlot, unref(additionalProperties));
    return [node];
  };

  return FnComponent
}

Answer №3

My TypeScript Solution

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

<script setup lang='ts'>
import { cloneVNode, useAttrs, useSlots, type VNode } from 'vue'

const att = useAttrs()

function recursivelyAddProps(element: VNode): VNode | Array<VNode> {
  if (Array.isArray(element?.children)) {
    return element.children.map((el) => recursivelyAddProps(el as VNode)) as Array<VNode>
  } else {
    return cloneVNode(element, att)
  }
}

const render = () => {
  const slot = useSlots()?.default!()
  return recursivelyAddProps(slot[0])
}
</script>

Usage Example

<ExtendedSlot class='dropdown-item'>
    <slot></slot>
</ExtendedSlot>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieving ID of an element to be animated with jQuery

I have a sprite image that changes background position when hovered over, and while it's currently working, I'm looking for a more efficient way to achieve this. I need to apply this effect to several images and am exploring ways to avoid duplica ...

Looking to display all items once the page has finished loading

I am experiencing a minor issue. Every time I access my store page where all products are listed, I have to click on the size filter to load the products. This is not ideal as I want all products to be displayed automatically when the page loads. What modi ...

Error encountered with Vuetify stepper simple component wrapper and v-on="$listeners" attribute

I'm working on developing a custom wrapper for the Vuetify Stepper component with the intention of applying some personalized CSS styles to it. My aim is to transmit all the $attrs, $listeners, and $slots. I do not wish to alter any functionality or ...

Leveraging JSON in conjunction with AJAX and Python database operations

I am a beginner in Python and I am attempting to access the database through Python in order to retrieve results in a JSON array using AJAX. When I test it by returning a JSON list and triggering an alert in JavaScript, everything works fine. However, as ...

determining the quantity of dates

Is there a way to calculate the number of a specific day of the week occurring in a month using AngularJS? For example, how can I determine the count of Saturdays in a given month? Thank you. Here is the code snippet I have been working on: <!doctype ...

Is there a way to retrieve the HTML code of a DOM element created through JavaScript?

I am currently using java script to generate an svg object within my html document. The code looks something like this: mySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); myPath = document.createElementNS("http://www.w3.org/2000/svg", ...

Retrieve a specified quantity of JSON data from an external API

Dealing with a recently received API from an external source: (please note: the data returned is extensive) I'm aware that I can retrieve this large object by using the following method: $.getJSON('https://www.saferproducts.gov/RestWebServices ...

Is it possible to utilize a designated alias for an imported module when utilizing dot notation for exported names?

In a React application, I encountered an issue with imports and exports. I have a file where I import modules like this: import * as cArrayList from './ClassArrayList' import * as mCalc1 from './moduleCalc1' And then export them like t ...

How can radios be made reusable in Vue.js?

Recently started delving into vue.js and encountered an issue where radio values were being repeated in the v-for div. Any ideas on how to resolve this problem? Check out this JSFiddle for reference. <script src="https://unpkg.com/<a href="/cdn-cgi ...

Getting started with integrating Vue.js with Lumen: a step-by-step guide

After successfully creating a RESTful API using Lumen and testing it with Postman, I am now looking to develop the front end for it with Vue. What is the best approach for integrating Vue into my existing project? Should I create a separate directory for ...

`Passing JavaScript variables through HTML anchor tags`

I am currently attempting to pass the id to the controller using this method: {{ route("admin.service.edit", '+val[0]+' )}} However, the '+val[0]+' is being interpreted as a string in the URL http://localhost:8000/admin/servi ...

Incorporating Material-UI with React Searchkit for a seamless user experience, featuring

I encountered an issue when trying to use both searchkit and material-ui in my React application. The problem arises from the fact that these two libraries require different versions of reactjs. Initially, everything was working fine with just searchkit in ...

Retrieve data from cookies that have been stored by the node server on the front end

I am currently utilizing the Node package 'cookie' to establish cookies from the backend as shown below: res.setHeader('Set-Cookie', cookie.serialize('token', token, { maxAge: 60 * 60 * 24 * 7 // 1 week ...

What could be the reason for my array parameter not being included in the POST request

When working on my laravel 5.7 / jquery 3 app, I encountered an issue while trying to save an array of data. After submitting the form, I noticed that only the _token parameter is being sent in the POST request as seen in the browser console: let todos_co ...

How can I effectively set up and utilize distinct npm modules on my local environment?

A React Native component I have created lives in a separate folder with its own package.json file, and now I want to use it in another project. The component named MyComponent is located in Workspace/MyComponent and has specific dependencies listed in its ...

When hovering over a select option, a description and clickable link will be displayed

I need to display a description with a clickable link when hovering over any option in the select tag. <div class="col-lg-4"> <div class="form-group"> <label class="form-label">Goal</label> <select name="semiTaskType ...

JQuery addClass function not functioning properly when used in conjunction with an AJAX request

I have a website where I've implemented an AJAX pagination system. Additionally, I've included a JQUERY call to add a class to certain list items within my document ready function. $(document).ready(function(){ $(".products ul li:nth-child(3 ...

Can a Node.js dependent library be integrated into Cordova?

After setting up an android project using Cordova, I came across a javascript library that relies on node.js to function. Since Cordova operates with node.js, can the library be invoked within the Cordova environment? ...

What is the best way to recycle a component instance in Nuxt.js?

Recently, I developed a cutting-edge single-page application using Nuxt.js 2 and Vue2. The highlight of this app is a complex WebGL visualizer showcasing a 3D scene across two distinct sections: SectionDesign and SectionConfirm <template> <Sec ...

Turn off the ability to view the content of .css and .js files within the browser

Earlier, I inquired about how to disable file and folder listing and discovered that it can be achieved using a file named .htaccess. To disable folder listing, I entered Options -Indexes in the .htaccess file located in the parent folder. Additionally, to ...