Vue3 on Rails
Tags: rails, vue3, vue, webdev
Published: 2020-10-11 17:09:48

___________
Versions:

Rails 6
Vue 3
___________

## Foreword

At work, I do the frontend for the project [ZAGE](https://zage.life) by [The Life Company](https://www.thelifecompany.com). The application is written in Rails. As I joined, I wanted to add Vue for some parts of the Frontend. But I couldn't find any tutorial on how to add Vue 3 to Rails. So, I wrote that tutorial, I was looking for.

## What you should know at the end 

This article explains how to install Vue 3 in a Ruby on Rails app. In the end, you should be able to implement a Vue app within an ERB view template. The code of this tutorial can be found in my Github Repository [rails-vue3-app](https://github.com/Vannsl/rails-vue3-app).


Content:

1. [Introduction](#introduction)
1. [Create Rails app](#create-rails-app)
  - [Setup Rails app](#setup-rails-app)
  - [Install yarn](#install-yarn)
1. [Install Vue3 & Co.](#install-npm-packages)
1. [Configure Webpack environment](#configure-webpack-environment)
  - [Set an alias](#webpack-alias)
  - [Vue Loader](#vue-loader)
  - [Include .vue files](#include-vue-files)
  - [Set Vue properties](#vue-properties)
1. [Create Vue app](#create-vue-app)
  - [Create entry point and SFC](#create-entry-sfc)
  - [Create Home controller and view as root route](#home-route)
  - [Connect Vue and Rails](#vue-and-rails)
1. [Next Steps](#next-steps)

 
## <a name="introduction"></a> Introduction

Evan You released the [Vue 3](https://www.npmjs.com/package/vue/v/3.0.0) in September 2020. This article does neither focus on the new features like the Composition API nor explain nor explain how to migrate from Vue2 to Vue3. Check out the [official documentation](http://v3.vuejs.org) and [migration guide](https://v3.vuejs.org/guide/migration/introduction.html) for that.

While the Vue CLI and Vite are great tools to configure new Vue projects easily, current resources lack information on how to install Vue3 in existing codebases. When you have a Rails 5+ full-stack application, you most likely already have a [webpacker](https://github.com/rails/webpacker) configuration. As of the date I'm writing this article, Webpacker offers a skeleton for Vue 2 via `rails webpacker:install:vue`, but not for Vue 3 yet. I opened a PR, check the state [here](https://github.com/rails/webpacker/pull/2759). So, let's dive right into how to add Vue3 into your tech stack.

## <a name="create-rails-app"></a> Create Rails app (optional)

### <a name="setup-rails-app"></a> Setup rails app

To test the setup before adding it to your "real" codebase, you can create a new rails app.

```bash
rails new rails_vue3_app --webpack
```

> I'm not a rails developer, so feel free to give me tips and hints, if there are other best practices for rails specific code and commands.

### <a name="install-yarn"></a> Install yarn

If the output of the previous command says something like: 

```bash
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/
```

...you need to install yarn and install the packages afterward.

```bash
npm i -g yarn
cd rails_vue3_app
yarn install
```

## <a name="install-npm-packages"></a> Install Vue3 & Co.

> If you run your rails app inside of Docker, you don't need to install anything on your host machine. You can install the packages within your docker container.

To use Vue3, you'll need (guess what :) ) Vue in version 3, Vue-Loader in version 16 beta, and the SFC compiler.

Vue3 is released on npm with the tag `next`. The `current` version is still 2.6.x to prevent developers from having to migrate to Vue3 if they don't want to. The same applies to the [vue-loader](https://www.npmjs.com/package/vue-loader).

```bash
# in rails_vue3_app
yarn add vue@next vue-loader@next @vue/compiler-sfc
```

Check the `package.json` to see the installed versions. The minimal versions should be:

```json
// ##############################
// package.json
// ##############################
{
  "name": "rails_vue_app",
  "private": true,
  "dependencies": {
    "@vue/compiler-sfc": "^3.0.0",
    "vue": "^3.0.0",
    "vue-loader": "^16.0.0-beta.8"
    // ...
  }
  // ...
}
```

## <a name="configure-webpack-environment"></a> Webpack environment configuration

Next, we need to tell Webpack what to do with `*.vue` files. For that, go to the file `webpack/environment.js`

By default, it should look like this:

```js
// ##############################
// webpack/environment.js
// ##############################
const { environment } = require('@rails/webpacker')

module.exports = environment
```

### <a name="webpack-alias"></a> Set an alias (optional)

I like to put my Vue applications into a separate folder. I also want to use a Webpack alias for an easier path handling when importing files. I did that with the following configuration:

```js
// ##############################
// webpack/environment.js
// ##############################
// const { environment } = require('@rails/webpacker')
const path = require("path")

const customConfig = {
  resolve:{
    alias: {
      "@": path.resolve(__dirname, "..", "..", "app/javascript/src")
    }
  }
}

environment.config.merge(customConfig)

// module.exports = environment
```

### <a name="vue-loader"></a> Add Vue Loader

Now it's time to add the loader. It tells Webpack what to do with files that match to the Regex `.vue`. 

```js
// ##############################
// webpack/environment.js
// ##############################

// const { environment } = require('@rails/webpacker')
// const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

// const customConfig = {
//   resolve:{
//     alias: {
//      '@': path.resolve(__dirname, '..', '..', 'app/javascript/src')
//     }
//   }
// }

// environment.config.merge(customConfig)

environment.plugins.prepend(
    'VueLoaderPlugin',
    new VueLoaderPlugin()
)

environment.loaders.prepend('vue', {
    test: /\.vue$/,
    use: [{
        loader: 'vue-loader'
    }]
})

// module.exports = environment
```

Because I like to keep the file `webpack/environment.js` as clean and readable as possible, I outsourced the configuration of Vue in an own file within the folder `webpack/loaders`. By default it does not exist, so create it first using the terminal or your IDE. The end result should look like this:

```js
// ##############################
// webpack/loaders/vue.js
// ##############################

module.exports = {
    test: /\.vue$/,
    use: [{
        loader: 'vue-loader'
    }]
}
```

```js
// ##############################
// webpack/environment.js
// ##############################

// const { environment } = require('@rails/webpacker')
// const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const vue = require('./loaders/vue');

// const customConfig = {
//   resolve:{
//     alias: {
//       '@': path.resolve(__dirname, '..', '..', 'app/javascript/src')
//     }
//   }
// }

// environment.config.merge(customConfig)

environment.plugins.prepend(
    'VueLoaderPlugin',
    new VueLoaderPlugin()
)

environment.loaders.prepend('vue', vue)

// module.exports = environment
```

### <a name="include-vue-files"></a> Include .vue files

Next, open the file `config/webpacker.yml` and add `.vue` to the extensions:

```yml
// ##############################
// config/webpacker.yml
// ##############################

default: &default
  # ...


  extensions:
    # ...
    - .vue
```

### <a name="vue-properties"></a> Set Vue properties

It is strongly recommended to properly configure some properties of Vue in order to get proper tree-shaking in the final bundle. You can find more information in [Vue3's README](https://www.npmjs.com/package/vue/v/3.0.0) under `Bundler Build Feature Flags`.

``` js
// ##############################
// webpack/environment.js
// ##############################

// const { environment } = require('@rails/webpacker')
// const path = require('path')
const { DefinePlugin } = require('webpack')
// const { VueLoaderPlugin } = require('vue-loader')
// const vue = require("./loaders/vue");

// const customConfig = {
//   resolve:{
//     alias: {
//       "@": path.resolve(__dirname, "..", "..", "app/javascript/src")
//     }
//   }
// }

// environment.config.merge(customConfig)

// environment.plugins.prepend(
//     'VueLoaderPlugin',
//     new VueLoaderPlugin()
// )

environment.plugins.prepend(
    'Define',
    new DefinePlugin({
        __VUE_OPTIONS_API__: false,
        // or __VUE_OPTIONS_API__: true,
        __VUE_PROD_DEVTOOLS__: false
    })
)

// environment.loaders.prepend('vue', vue)

// module.exports = environment
```

## <a name="create-vue-app"></a> Create a Vue app

Everything should now be correctly set up. It's finally time to add our first Single File Component and load it into a container.

### <a name="create-entry-sfc"></a> Create entry point and SFC

As explained above, I'd like to collect all Vue related code in a single directory. Therefore, create the folder `./app/javascript/src` in your root directory. In there, create the file `main.js`. It will be the entry point for the Vue application. Leave it empty as it is, we'll come back to it again.

Next, let's create a Vue component. I propose to create the folder `./app/javascript/src/components`. In there, create the file `HelloWorld.vue`. You can name the file also `hello-world.vue` if you prefer that syntax. Insert the following code:

```vue
// ##############################
// app/javascript/src/components/HelloWorld.vue
// ##############################

<template>
  <p>
    {{ message }}
  </p>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'HelloWorld',
  setup() {
      const message = ref('Hello World')

      return {
        message
      }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>
```

> Of course you can write sassy CSS. Therefore add the lang attribute `lang='scss'` to the `<script>` section.

Now, let's head back to our `main.js` file and create the Vue app:

```js
// ##############################
// app/javascript/src/main.js
// ##############################

import { createApp } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'

export default () => {
    document.addEventListener('DOMContentLoaded', () => {
        const app = createApp(HelloWorld)
        app.mount('#vue-app')
    })
}
```

Okay, let's recap what has happened. We created an SFC (Single File Component). We created a Vue instance and mounted it into an HTML element with the id `vue-app`. But what, we haven't written this element yet. So let's do that now.

### <a name="home-route"></a> Create Home controller and view as root route

For that, let's create a HomeController with a view. Alternatively, you can go directly to the `.erb` file you want to add the Vue app to.

```bash
rails generate controller Home index 
```

Next, set the home controller as base route in `config/routes.rb`:

```ruby
# -----------------
# config/routes.rb
# -----------------

# Rails.application.routes.draw do
#   get 'home/index'

  root to: 'home#index'
# end
```

### <a name="vue-and-rails"></a> Connect Vue and Rails

Finally, our configuration is done. Now we have a home page. We want to load the Vue app directly in this file. Head to `app/views/home/index.html`. Add or replace the dummy content with the following line:

```html
<!-- app/views/home/index.html -->

<div id='vue-app'></div>
```

Let's check out what's going on in the browser. Open your terminal and start the rails and the Webpack server with:

```bash
# in one tab
rails server

# in another tab
./bin/webpack-dev-server
```

Open a browser and go to [localhost:3000](http://localhost:3000). If everything works, you should see nothing (but the dummy HTML code if you haven't removed it). When inspecting the DOM, there should be the empty `div container` with the id `vue-app`. Our last step to bring it all together is to import the entry point of our Vue application.

To keep it simple, we will add the entry point directly to the `application.js` in this tutorial. Of course, you can create a single pack for it. You can also use the split chunks feature of webpack(er). But for now, let's open `app/javascript/packs/application.js` and import our entry point:

```js
// ##############################
// app/javascript/packs/application.js
// ##############################

// require("@rails/ujs").start()
// require("turbolinks").start()
// require("@rails/activestorage").start()
// require("channels")
import initVueApp from "@/main.js"

initVueApp()
```

Reload the page, you should see "Hello World" now! Have fun playing around with it for a while. Change the style, change the message, change the template. Don't forget, that you don't need to have a single root element in Vue3 anymore. Therefore no wrapping div container or similar.

## <a name="next-steps"></a> Next Steps

Congratulations, you have just installed Vue3 in a Rails app. Next, we will talk about how to:

- pass properties from Rails to Vue
- configure Linters
- [configure TypeScript](https://dev.to/vannsl/webpacker-vue-3-and-typescript-1i99)

Follow me to receive notifications, when I post those articles. I will link them here at the bottom of this article as soon as they're published.


------

 

Photo by [Christian Holzinger](https://unsplash.com/@pixelatelier) on [Unsplash](https://unsplash.com/s/photos/rails)