Search code examples
vue.jsvue-componentvue-slot

Executing js on slot


I'm a beginner in web development and I'm trying to help out friends restarting an old game. I'm in charge of the tooltip component but I hit a wall...

There are many Vue components and in a lot of them I want to call a child component named Tooltip, I'm using vue-tippy for easy configuration. This is the component:

<template>
    <tippy class="tippy-tooltip">
      <slot name='tooltip-trigger'></slot>

      <template #content>
          <slot name='tooltip-content'>
          </slot>
      </template>
    </tippy>
</template>

<script>
import { formatText } from "@/utils/formatText";

    export default {
    name: "Tooltip",
    methods:{
        formatContent(value) {
            if (! value) return '';
            return formatText(value.toString());
            }
        },
    }
</script>

In one of the other components I try to use the tooltip:

<template>
    <a class="action-button" href="#">
        <Tooltip>
            <template #tooltip-trigger>
                <span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="@/assets/images/pm.png" alt="mp"></span>
                <span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="@/assets/images/pa.png" alt="ap"></span>
                <span v-if="action.canExecute">{{ action.name }}</span>
                <span v-else><s>{{ action.name }}</s></span>
                <span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
            </template>
            <template #tooltip-content>
                <h1>{{action.name}}</h1>
                <p>{{action.description}}</p>
            </template>
        </Tooltip>
    </a>
</template>

<script>
import Tooltip from "@/components/Utils/ToolTip";

export default {
    props: {
        action: Object
    },
    components: {Tooltip}
};
</script>

From here everything is fine, the tooltip is correctly displayed with the proper content.

The thing is, the text in the {{ named.description }} needs to be formatted with the formatContent content. I know I can use the props, the components would look like that:

Tooltip.vue:

<template>
    <tippy class="tippy-tooltip">
      <slot name='tooltip-trigger'></slot>

      <template #content>
          <h1 v-html="formatContent(title)" />
          <p v-html="formatContent(content)"/>
      </template>
    </tippy>
</template>

<script>
import { formatText } from "@/utils/formatText";

    export default {
    name: "Tooltip",
    methods:{
        formatContent(value) {
            if (! value) return '';
            return formatText(value.toString());
            }
        },
    props: {
        title: { 
            type: String,
            required: true
            },
        content: { 
            type: Array,
            required: true
            }
        }
    }
</script>

Parent.vue:


<template>
    <a class="action-button" href="#">
        <Tooltip :title="action.name" :content="action.description">
            <template v-slot:tooltip-trigger>
                <span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="@/assets/images/pm.png" alt="mp"></span>
                <span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="@/assets/images/pa.png" alt="ap"></span>
                <span v-if="action.canExecute">{{ action.name }}</span>
                <span v-else><s>{{ action.name }}</s></span>
                <span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
            </template>
        </Tooltip>
    </a>
</template>

<script>
import Tooltip from "@/components/Utils/ToolTip";

export default {
    props: {
        action: Object
    },
    components: {Tooltip}
};
</script>

But I need to use a slot in the tooltip component because we'll have some "extensive" lists with v-for.

Is there a way to pass the data from a slot into a JS function?


Solution

  • If I understand you correctly, you're looking for scoped slots here.

    These will allow you to pass information (including methods) from child components (the components with <slot> elements) back to the parents (the component(s) filling those slots), allowing parents to use chosen information directly in the slotted-in content.

    In this case, we can give parents access to formatContent(), which will allow them to pass in content that uses it directly. This allows us to keep the flexibility of slots, with the data passing of props.

    To add this to your example, we add some "scope" to your content slot in Tooltip.vue. This just means we one or more attributes to your <slot> element, in this case, formatContent:

    <!-- Tooltip.vue -->
    <template>
        <tippy class="tippy-tooltip">
          <slot name='tooltip-trigger'></slot>
    
          <template #content>
              <!-- Attributes we add or bind to this slot (eg. formatContent) -->
              <!-- become available to components using the slot -->
              <slot name='tooltip-content' :formatContent="formatContent"></slot>
          </template>
        </tippy>
    </template>
    
    <script>
    import { formatText } from "@/utils/formatText";
    
    export default {
        name: "Tooltip",
        methods: {
            formatContent(value) {
                // Rewrote as a ternary, but keep what you're comfortable with
                return !value ? '' : formatText(value.toString());
            }
        },
    }
    </script>
    

    Now that we've added some scope to the slot, parents filling the slot with content can use it by invoking a slot's "scope":

    <!-- Parent.vue -->
    <template>
        <a class="action-button" href="#">
            <Tooltip>
                . . .
                <template #tooltip-content="{ formatContent }">
                    <!-- Elements in this slot now have access to 'formatContent' -->
                    <h1>{{ formatContent(action.name) }}</h1>
                    <p>{{ formatContent(action.description) }}</p>
                </template>
            </Tooltip>
        </a>
    </template>
    
    . . . 
    

    Sidenote: I prefer to use the destructured syntax for slot scope, because I feel it's clearer, and you only have to expose what you're actually using:

    <template #tooltip-content="{ formatContent }">
    

    But you can also use a variable name here if your prefer, which will become an object which has all your slot content as properties. Eg.:

    <template #tooltip-content="slotProps">
        <!-- 'formatContent' is now a property of 'slotProps' -->
        <h1>{{ slotProps.formatContent(action.name) }}</h1>
        <p>{{ slotProps.formatContent(action.description) }}</p>
    </template>
    

    If you still need the v-html rendering, you can still do that in the slot:

    <template #tooltip-content="{ formatContent }">
        <h1 v-html="formatContent(title)" />
        <p v-html="formatContent(content)"/>
    </template>