If you ever have implement Vue.JS or any others UI libraries like React in your backend application, you probably know that there are multiples ways to achieve that.

Vue.JS is a progressive framework to build UIs. It means that you don't have to use it everywhere to make it work. You can add component after component without rewriting your app from the ground up.

In these examples, I will use the ERB templating language, but it's exactly the same with Twig, Blade or any other templating system.

How to use Vue.JS in backend

Basically, there are three ways to use both tools together.

1. Inject props in component

<BlogPost title="<%= @post.title %>" content="<%= @post.content %>"></BlogPost>

This code will be rendered:

<BlogPost title="My awesome blog post" content="My content"></BlogPost>

Here, title and content will be accessible in your BlogPost component as variables:

<template>
  <div>
    <h2>{{ title }}</h2>

    <div>{{ content }}</div>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String
    },

    content: {
      type: String
    }
  }
}
</script>

Tip: You can use v-bind to convert strings to Boolean, Number, Object or Array.

A better approach to achieve that would be to serialize the post using the v-bind shorthand.

<BlogPost :post="<%= @post.to_json %>"></BlogPost>

And use post as an object in your component.

<template>
  <div>
    <h2>{{ post.title }}</h2>

    <div v-pre>{{ post.content }}</div>
  </div>
</template>

<script>
export default {
  props: {
    post: {
      type: Object
    }
  }
}
</script>

2. Retrieve data from an API

For more complicated components, sometimes it's easier to call an API to retrieve data.

For instance, to load all the blog posts in your ERB template:

<p>Hello <%= current_user.name %></p>

<BlogPosts />

A simplified version of this component would be:

<template>
  <BlogPost v-for="post in posts" :post="post" :key="post.id">
</template>

<script>
export default {
  data: () => ({
    posts: []
  }),

  mounted () {
    fetch('/api/posts')
      .then(response => response.json())
      .then(data => {
        this.posts = data.posts
      })
  }
}
</script>

With this approach, your component is a black box for your backend application.

Moreover, your browser needs to download all your JavaScript and load Vue.JS in order to retrieve your posts data. So for a moment your page will be blank.

3. Using scoped slots

Scoped slots are not easy to understand. They are a particular form of slots.

A slot defines a location to inject content in a component.

<BlogPost>
  <h2><%= @post.title %></h2>
</BlogPost>

In your component, <slot></slot> will be replaced by the content inside your component declaration:

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

It's useful because they are easier than props to inject complex content in your component and use Vue.JS reactivity around that.

It would be even greater to use Vue to change your injected content and not only send data from parents to children! That's where scoped slots are used for.

Using Scoped slots

Sometime you would like to re-use the markup from your backend application for multiple reasons.

You have your helpers, your custom Form Object, your validators or whatever.

For instance, we want to add a DatePicker component in our form, without rewriting the whole form in Vue.JS:

<%= form_with(url: "/search", method: "get") do %>
  <%= label_tag(:start_at, "Start date:") %>

  <DatePicker>
    <%= text_field_tag(:start_at) %>
  </DatePicker>

  <%= submit_tag("Search") %>
<% end %>

If we want to send data from children to parent, we need to use scoped slots:

<DatePicker v-slot="slotProps">
  <%= text_field_tag(:start_at, nil, ':value': 'slotProps.selectedDate') %>
</DatePicker>

Here, the slotProps contains all the parameters sent from DatePicker component to the slot. It's an Object and can contain anything, like an other Object, a String or even a Function.

In your DatePicker component:

<template>
  <div>
    <slot :selectedDate="selectedDate"></slot>
  </div>
</template>

<script>
export default {
  data: () => ({
    selectedDate: new Date()
  })
}
</script>

To make selectedDate available in the parent, you need to bind it as an attribute of the slot. This is where all the magic happens!

And of course, you can send multiple parameters.

You can clean this up by using destructuration:

<DatePicker v-slot="{ selectedDate }">
  <%= text_field_tag(:start_at, nil, ':value': 'selectedDate') %>
</DatePicker>

The value of your text_field_tag will be dynamically updated by Vue.JS when selectedDate is modified.

You can also use Function which blew my mind the first time I used it:

<ExampleComponent v-slot="{ handleClick }">
  <div @click.prevent="handleClick">Click me!</div>
</ExampleComponent>

Vue.JS reactivity and custom events in ERB template 🤯.

Conclusion

I think scoped slots are a nice way to progressively add Vue to your application.

You can do so many things with scoped slots like renderless components.

Here's a video explaining how it works in details.

I hope this helps.

Thanks. 👋