Getting started with Rails + Vue

Jordano Moscoso
Tekton Labs
Published in
5 min readFeb 11, 2019

UPDATE: This post was written few years ago. On Rails 7 you can use importmaps. Please check new Rails 7 documentation.

Nowadays, web application users require pretty interactive interfaces. To achieve this objective developers work with front-end frameworks like Angular, React or Vue. However, a common drawback is how long it takes to create a front-end and an API and many companies cannot afford the time and costs it implies.

In the GoRails website, Chris Oliver managed a way to make Vue.js and Rails work great together. So I wanted to take it to the next level using forms and Vue validations with Vuelidate.

Let’s get started.

Basic setup with Webpack and Tailwind CSS

To start, just create a new rails project with web pack:

rails new vuerails --webpack

Before writing our main javascript to import vue, let's add normalize.css and Tailwind CSS, a powerful library to write fast user interfaces. For this, we should generate the initial javascript configuration:

yarn add tailwindcss
node_modules/.bin/tailwind init app/javascript/css/tailwind.js

And our CSS import file for tailwind:

/* app/javascript/css/application.css */
@tailwind preflight; /* Basic Normalize */
@tailwind utilities; /* Tailwindcss utilities */

Finally, add it to Postcss to make it available on web pack on our application.js file:

/* .postcssrs.yml */
plugins:
postcss-import: {}
postcss-cssnext: {}
tailwindcss: {}
/* app/javascript/packs/application.js */
/* eslint no-console:0 */
import "../css/application.css";

On our application layout view we should add the web packer tags to get the proper javascript and stylesheets:

<!DOCTYPE html>
<html>
<head>
<title>Vuerails</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<style>
html, body {
height: 100%;
}
</style>
<div class="relative block h-full" data-behavior="vue">
<div class="relative flex items-center justify-center h-full">
<div class="block login-box-container w-5/6 md:w-3/4 xl:w-1/4 ">
<%= yield %>
</div>
</div>
</div>
</body>
</html>

Great, we now have Tailwind CSS installed to work with.

Let’s create a page and protect it. To achieve this, we create a controller with a simple action:

rails g controller Home index

We need to overwrite routes config/routes.rb

Rails.application.routes.draw do
root to: 'home#index'
end

Devise and Vue views

Let's add Devise to protect the page, generate our model, migrate it and generate the login page (don’t forget to add it to your gemfile first):

rails generate devise:install
rails generate devise User
rake db:migrate
rails generate devise:views -v sessions

Let's protect our page:

class HomeController < ApplicationController
before_action :authenticate_user!
def index
end
end

Currently, we have a simple page protected by Devise, now let's change our login to use Vue, first, install it:

rails webpacker:install:vue

It generates some default files and we don’t need, so let's remove them:

rm app/javascript/app.vue
rm app/javascript/packs/hello_vue.js

Let's add Turbolinks and Vuelidate and write our first application loader for Vue:

yarn add vue-turbolinks vuelidate

Then import them on our main web pack application file and set Vue to use them, and define where our components will be available, in this case on an element with data-behavior as Vue:

yarn add vue-turbolinks vuelidate/* eslint no-console:0 */
import "../css/application.css";
import TurbolinksAdapter from 'vue-turbolinks';
import Vuelidate from 'vuelidate';
import Vue from 'vue/dist/vue.esm';
import Login from '../components/login.vue';

Vue.use(Vuelidate)
Vue.use(TurbolinksAdapter)

Vue.component('login', Login);

document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: '[data-behavior="vue"]'
})
})

And we should add the data behavior tag to make components available inside this element:

<!DOCTYPE html>
<html>
<head>
<title>Vuerails</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<style>
html, body {
height: 100%;
}
</style>
<div class="relative block h-full" data-behavior="vue">
<div class="relative flex items-center justify-center h-full">
<div class="block login-box-container w-5/6 md:w-3/4 xl:w-1/4 ">
<%= yield %>
</div>
</div>
</div>
</body>
</html>

Great, now let’s use Vue components instead of the regular view. First, create our login component:

<style scoped>
.login-footer {
background-color: #e6e6e6;
}
</style>
<template>
<div class="bg-white rounded text-wktext">
<form :action="action" method="post">
<div class="rounded-t py-4 px-5 bg-blue">
<span class="text-base text-white text-wktext-bold">Welcome, please login</span>
</div>
<div class="my-10 mx-8">
<input name="authenticity_token" type="hidden" v-bind:value="token">
<div class="relative block w-full mb-10 material-input">
<input class="block text-lg p-2 w-full border-0 border-b-2 border-grey-dark focus:outline-none" type="text" name="user[email]" required v-model.trim="$v.email.$model">
<span class="relative block bar"></span>
<label class="block text-lg absolute pointer-events-none">Email</label>
<span class="form-text text-danger" v-if="$v.email.$error">Campo inválido.</span>
</div>
<div class="relative block w-full mb-10 material-input">
<input class="block text-lg p-2 w-full border-0 border-b-2 border-grey-dark focus:outline-none" type="password" name="user[password]" required v-model.trim="$v.password.$model">
<span class="relative block bar"></span>
<label class="block text-lg absolute pointer-events-none">Password</label>
<span class="form-text text-danger" v-if="$v.password.$error">Campo inválido.</span>
</div>
</div>
<div class="login-footer rounded-b py-5 px-12">
<input type="submit" value="Log In" class="text-base text-white py-2 px-3 rounded w-full cursor-pointer bg-blue" :disabled="$v.$invalid">
</div>
</form>
</div>
</template>
<script>
import { required, email } from 'vuelidate/lib/validators'
export default {
props: ['action', 'token'],
data() {
return {
email: "",
password: ""
}
},
validations: {
email: {
required,
email
},
password: {
required
}
}
}
</script>

Then add it to our Vue import file:

/* eslint no-console:0 */
import "../css/application.css";
import TurbolinksAdapter from 'vue-turbolinks';
import Vuelidate from 'vuelidate';
import Vue from 'vue/dist/vue.esm';
import Login from '../components/login.vue';

Vue.use(Vuelidate)
Vue.use(TurbolinksAdapter)

Vue.component('login', Login);

document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: '[data-behavior="vue"]'
})
})

And finally use it on our view:

/* new.html.erb */
<login :token="<%= form_authenticity_token.to_json %>" :action="<%= user_session_path.to_json %>"></login>

We are passing our csrf token directly to our component. Our form is fine but we need styling for our inputs, let's add it (dont forget to rename it to scss, it is our main application on app/assets/stylesheets):

@charset "utf-8";

// Material design input
.material-input {
label {
color: #3490dc;
left:5px;
top:10px;
transition:0.2s ease all;
}
input {
&:focus ~ label, &:valid ~ label {
top:-20px;
font-size: 1rem;
color: #3490dc;
}
&:focus ~ .bar:before, &:focus ~ .bar:after {
width:50%;
}
&[type=submit]:disabled {
background: grey;
}
}
.bar {
&:before, &:after {
content: '';
height: 3px;
width: 0;
bottom: 0px;
position: absolute;
background: #3490dc;
transition:0.2s ease all;
}
&:before {
left:50%;
}
&:after {
right:50%;
}
&.error:before, &.error:after {
background: red;
width:50%;
}
}
.error {
color: red;
font-size: 1rem;
}
}

Most of the time you would need to run rails server and web pack dev server separately, to use only one terminal for it, I use foreman as the tool to manage it:

gem install foreman

Let's create our procfile and run it:

web: rails s
webpack: bin/webpack-dev-server
foreman start

That’s all!

This way we are passing the CSRF token and the action path, pretty easy without making a separate API and making an extra call or disabling CSRF protection.

Hope this helps in your future endeavors!

[1] https://gorails.com/series/using-vuejs-with-rails

Powered by Tekton Labs

--

--