将一个类添加到slot的每个子元素

4

我正在尝试设置一个有插槽的组件,当呈现时,该插槽中的每个子元素都会添加一个类。非常简化地说:

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

我该如何处理这个问题?我的现有解决方案是在onBeforeUpdate钩子中向元素添加类名:

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

然而,每当重新呈现时(使用npm run serve),这些元素失去了样式,同时jest测试会给我一个警告:
    [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.

我应该将插槽移动到自己的组件中并在那里使用渲染函数吗?但即使这样,我也不确定如何编辑子组件以添加类,或者如何从渲染函数中生成多个根元素。

2个回答

2

所以,我用一种非常hacky的方式解决了这个问题,但至少我的重新渲染问题不再存在,而jest也不再抱怨。我编写了一个带有渲染函数的组件,将该类附加到所有子元素上。


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

然后我只需将插槽包装在此组件中,以向子元素添加类:

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

我很乐意接受任何能改进这个解决方案的答案,特别是:

  • 输入时的任何提示。所有这些any都感觉奇怪,但我发现使用Vue类型来处理插槽非常不实用,因为它们通常是3或4种类型的联合,唯一的解决方案是将其包装在类型检查中。
  • 任何可以提高可靠性的内容,因为似乎在与我预期略有不同的设置中会崩溃。
  • 基于Vue(或TS)最佳实践的任何建议,因为这看起来非常业余。
  • 对于符号相等性的任何其他测试方法,因为我不知道有哪些。

编辑 这是我的最新尝试,一个名为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);
  },
};

这个组件的使用方法与之前的完全相同。我对这个解决方案感到满意,它似乎更加健壮和符合惯用法。请注意,它是JavaScript编写的,因为我发现正确输入这些函数非常困难。


0

@Haf的回答很好。
我做了一些调整,这样你就可以指定组件名称。

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

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

/**
 * @param extraProps - ref or normal object
 * @returns
 * 
 * @example
 * 
 * const StyleComponent = useCssInJs({class: 'text-red-500'});
 * 
 * const ComponentA = () => {
 *  return <StyleComponent><span>text is red-500</span></StyleComponent>
 * }
 */
export const useCssInJs = (extraProps: MaybeRef<{
  class?: string,
  style?: StyleValue
}>) => {
  const FnComponent: FunctionalComponent = (_, { slots }) => {
    const defaultSlot = slots.default?.()[0];
    // could be Fragment or others? I'm just ignoring these case here
    if (!defaultSlot) return;
    const node = cloneVNode(defaultSlot, unref(extraProps));
    return [node];
  };

  return FnComponent
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接