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

Guidelines for validating email input using jQuery

Although I am not utilizing the form tag, you can still achieve form functionality using jQuery Ajax. <input type="email" placeholder="Email" name="email" /> <input type="password" placeholder="Password ...

The constructor error in ng-serve signalizes an issue in

Currently, I am developing an AngularJS application. However, when attempting to start the dev server, I encountered an issue with my ng serve command: https://i.stack.imgur.com/QujSe.png ...

Steady Navigation Bar in Javascript without Bouncing

While experimenting with a fixed navigation bar, I've encountered an issue where the content below jumps up on the page when the navigation bar "fixes" itself to the top of the screen. You can check out the JSFiddle I've been working on for refer ...

Is it possible for my website to send an email without the need for a script to be executed?

Looking to include a contact page on my website, but struggling with limitations imposed due to hosting on a secure school server that restricts script execution. Wondering if there's a workaround that would allow for email sending on the client side ...

Error: Unable to access the 'Result' property because it is undefined

I am encountering an issue while attempting to showcase database results, and the error message I'm receiving is: TypeError: Cannot read property 'Result' of undefined I am in the process of developing a Single Page Application with Angula ...

Organize chat messages based on date using JavaScript

I came across a similar query, but it didn't resolve my issue. Check this out: How can I group items in an array based on date? My goal is to organize these chat messages by date after fetching them from the database using AJAX. The console displays ...

Updating the index page with AJAX in Rails 4: Step-by-step guide

It's surprising that I haven't found answers to my specific questions despite searching extensively. I have an Expenses page where I want to display all expenses for a given month in a table. Currently, I achieve this by adding month and year par ...

Transforming the *.vue file into a *.js file for deployment on a content delivery network

Is there a way to compile Component.vue files into JavaScript files for use with the Vue CDN in HTML? For example, consider the following component Component.vue: <template> <div class="demo">{{ msg }}</div> </template& ...

Verifying the activation status of a button within a Chrome extension

I have been working on a chrome plugin that continuously checks the status of a button to see if it is enabled. If it is, the plugin clicks on the button. I initially used an infinite for loop for this task, but realized that it was causing the browser to ...

Clicking on links will open them in a separate tab, ensuring that only the new tab

Is there a way to open a new tab or popup window, and have it update the existing tab or window whenever the original source link is clicked again? The current behavior of continuously opening new tabs isn't what I was hoping for. ...

Issues with Vue JS: the closing tag linting function is not functioning properly

Has anyone experienced this particular issue before? https://i.stack.imgur.com/p8dM3.png I've checked all my outputs and there are no error messages. I'm currently going through my plugins one by one to troubleshoot. If anyone has encountered t ...

Concentrate on the HTML element once it becomes active

I am facing a challenge where I need to focus on a specific input element. However, there is a spinner that appears on page load and remains hidden until certain http requests are completed. All inputs are disabled until the requests are finished. The setu ...

Encountering a parser error during an Ajax request

Attempting to develop a login system with PHP, jQuery, Ajax, and JSON. It successfully validates empty fields, but upon form submission, the Ajax call fails. The response text displays a JSON array in the console, indicating that the PHP part is not the is ...

Guide on transferring information from .ejs file to .js file?

When sending data to a .ejs file using the res.render() method, how can we pass the same data to a .js file if that .ejs file includes a .js file in a script tag? // Server file snippet app.get('/student/data_structures/mock_test_1', (req, res) = ...

Issue with the navbar toggler not displaying the list items

When the screen is minimized, the toggle button appears. However, clicking it does not reveal the contents of the navbar on a small screen. I have tried loading jQuery before the bootstrap JS file as suggested by many, but it still doesn't work. Can s ...

Unable to access data from the Array by passing the index as an argument to the method

Having trouble retrieving an item from an Array using method() with an index argument that returns undefined export class DataService { public list = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, ...

What is the best way to retrieve the dimensions of a custom pop-up window from the user?

Currently, I am in the process of developing a website that allows users to input width and height parameters within an HTML file. The idea is to use JavaScript to take these user inputs and generate a pop-up window of a custom size based on the values pro ...

What are the steps to create a one-way binding between multiple input fields and a master input in Vue3?

In my scenario, there exists a primary HTML <input> element: <template> <input id="tax_master" max="100" min="0" type="number" v-model="taxDefault" /> </template ...

Set a string data type as the output stream in C++

I have a method called prettyPrintRaw that seems to format the output in a visually appealing manner. However, I am having trouble understanding what this specific code does and what kind of data it returns. How can I assign the return value of this code t ...

What is the best way to utilize an HTML form for updating a database entry using the "patch" method?

I have been attempting to update documents in my mongoDB database using JavaScript. I understand that forms typically only support post/get methods, which has limitations. Therefore, I am looking for an alternative method to successfully update the documen ...