// Tutorial //

Renderless Behavioral Slots in Vue.js

Published on September 18, 2019
Default avatar
By David Desmaisons
Developer and author at DigitalOcean.
Renderless Behavioral Slots in Vue.js

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Let’s go over the renderless slot pattern in Vue and see the problems that it can help solve.

Introduced with Vue.js 2.3.0, scoped slots have considerably improved component reusability. For example, the renderless component pattern emerged and solved the problem of providing reusable behavior and pluggable presentation.

Here we’ll see how to solve the opposite problem: providing reusable presentation and pluggable behavior.

Renderless Components

This pattern applies for components that implement complex behavior and have customizable presentation.

To do so:

  1. The component implements all the behavior
  2. Scoped slots are responsible for the rendering
  3. Fallback content ensures that the component can be used out-of-the-box.

Let’s take an example: a component performing an Ajax request and having a slot to display the result. The component handles the Ajax request and the loading state while the default slot provides the presentation.

Here a simplified implementation:

<template>
  <div>
    <slot v-if="loading" name="loading">
        <div>Loading ...</div>
    </slot>
    <slot v-else v-bind={data}>
    </slot>
  </div>
</template>

<script>
export default {
  props: ["url"],
  data: () => ({
    loading: true,
    data: null
  }),
  async created() {
    this.data = await fetch(this.url);
    this.loading = false;
  }
};
</script>

Usage:

<lazy-loading url="https://server/api/data">
  <template #default="{ data }">
    <div>{{ data }}</div>
  </template>
</lazy-loading>

For the original post about this pattern, check here.

A different problem

What if the problem is the contrary: imagine the main feature of a component is its presentation. On the other hand, behaviors should be customizable.

Imagine you are creating a tree component based on SVG, like this one:

tree component

You want to provide the SVG display and behavior such as retracting node and text highlighting on click.

A problem arises when you decide to not hard-code these behaviors and let the user of the component free to override them.

A simple solution to expose these behaviors would be to add methods and events to the component.

You’ll end up with something like:

<script>
export default {
  mounted() {
    // pseudo code
    nodes.on('click',(node) => this.$emit('click', node));
  },
  methods: {
    expandNode(node) {
      //...
    },
    retractNode(node) {
      //...
    },
    highlightText(node) {
      //...
    },
  }
};
</script>

To add behavior to the component, the consumer of the component will need to use a ref in a parent component, something like:

<template>
  <tree ref="tree" @click="onClick"></tree>
</template>

<script>
export default {
  methods: {
    onClick(node) {
      this.$refs.tree.retractNode(node);
    }
  }
};
</script>

This approach has several drawbacks:

  1. It’s not possible to provide a default behavior anymore
  2. Behaviors end up as a cookbook that you need to duplicate
  3. Behaviors are not reusable

Let’s see how renderless slots can solve these issues.

Renderless Slots

A behavior consists basically of proving a reaction to an event. So let’s create a slot that receives access to events and component methods:

<template>
  <div>
    <slot name="behavior" :on="on" :actions="actions">
    </slot>
  </div>
</template>

<script>
export default {
  methods: {
    expandNode(node) { },
    retractNode(node) { },
   //...
  },
  computed:{
    actions() {
      const {expandNode, retractNode} = this;
      return {expandNode, retractNode};
    },
    on() {
      return this.$on.bind(this);
    }
  }
};
</script>

The on attribute is the $on method of the parent component, so it’s possible to listen to all events.

Implementing a behavior can be done as a renderless component. Let’s write the expand-on-click component:

export default {
  props: ['on','action']

  render: () => null,

  created() {
    this.on("click", (node) => {
      this.actions.expandNode(node);
    });
  }
};

Usage:

<tree>
  <template #behavior="{ on, actions }">
    <expand-on-click v-bind="{ on, actions }"/>
  </template>
</tree>

The main advantages of this solution are:

  • The possibility to provide a default behavior by providing a fallback content:

For example, by declaring the graph component as:

<template>
  <div>
    <slot name="behavior" :on="on" :actions="actions">
      <expand-on-click v-bind="{ on, actions }"/>
    </slot>
  </div>
</template>
  • The possibility to create a reusable component that implements standard behavior that the component’s user can cherry pick

Let’s consider a highlight-on-hover component:

export default {
  props: ['on','action']

  render: () => null,

  created() {
    this.on("hover", (node) => {
      this.actions.highlight(node);
    });
  }
};

Overriding standard behavior:

<tree>
  <template #behavior="{ on, actions }">
    <highlight-on-hover v-bind="{ on, actions }"/>
  </template>
</tree>
  • Behavior slot are composable

Let’s add two pre-defined behaviors:

<tree>
  <template #behavior="{ on, actions }">
    <expand-on-click v-bind="{ on, actions }"/>
    <highlight-on-hover v-bind="{ on, actions }"/>
  </template>
</tree>
  • Readability of the solution

Component as behavior are self-explanatory.

  • Extensibility

The on attribute gives access to all the component events. New events are by default available for the slot.

Wrapping Up

Renderless slots present an interesting alternative to expose method and events in a component. They provide more readable and reusable code.

The code of the tree component implementing this pattern is available on github: Vue.D3.tree

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Developer and author at DigitalOcean.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment
Leave a comment...

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click here to Sign up and get $200 of credit to try our products over 60 days!