Streamlining Email Integration
Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …
In last decade, there has been a significant emphasis on single-page applications (SPA) and many modern frontend
frameworks encourage their use. However, we believe that although the SPA approach has its advantages, the amount of
additional complexity it introduces is not justified. Most of the time, when developing web applications, the goal is
to build the best possible product efficiently. At Mailsnag, we have chosen to utilize modern frontend frameworks like
Vue
and Vuetify
. However, instead of following the typical Vue + Router SPA approach, we have opted for a Multi-Page
Application (MPA) approach.
In this article, we will demonstrate how to set up a Rails 7
project with Vuetify 3
(and Vue 3
) using the MPA approach. We will also use Turbo Drive
to get the performance benefits of SPA without the added complexity.
You can access the project’s source code in our GitHub repository . Additionally, each section will include a link to the corresponding changes.
This guide was written using Ruby v3.2
, Node.js v18
, and Yarn v1.22
. Please ensure that you have these tools
available in your development environment so that you can follow along.
To begin, install the rails 7
gem and create a new app named rails7-vuetify3
. Since we won’t be using default
JavaScript bundler and the Asset Pipeline, we’ll instruct Rails to skip them during app creation. Feel free to customize
the app as needed, such as specifying database options. Additionally, we’ll add a simple blogs
controller with index
and show
pages, which will be used later to render our Vue
pages.
$ gem install rails -v 7.0.5
$ rails new --skip-asset-pipeline --skip-javascript rails7-vuetify3
$ cd rails7-vuetify3
$ rails g controller blogs index show
Fix routes by replacing contents of config/routes.rb
with:
Rails.application.routes.draw do
resources :blogs
# Defines the root path route ("/")
root "blogs#index"
end
Vuetify 3
is using vite
for dev
server and building production assets. It has a
good support for rails
through vite-rails
gem. Let’s
add it to our project:
$ bundle add vite_rails
$ bundle exec vite install
$ yarn install
Now that we have vite
working with rails
, we need to add and configure vuetify
app. First, let’s add vuetify
package to our package.json
and couple extra packages that will be used by vuetify
and vite
:
$ yarn add vue vuetify @mdi/js
$ yarn add -D vite-plugin-rails vite-plugin-vuetify @vitejs/plugin-vue
Since we will be using some of the vue
syntax in our rails views, we will need to use vue
with runtime compiler.
Take a look at vite.js.ts
to see how that is achieved. We will also need to configure vite
and rails
to create
vuetify
app. Take a look at changes needed below.
Replace vite.config.ts
with:
import { defineConfig } from "vite";
import RailsPlugin from "vite-plugin-rails";
import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify";
export default defineConfig({
plugins: [RailsPlugin({ stimulus: false }), vue(), vuetify()],
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
},
extensions: [
".mjs",
".js",
".ts",
".jsx",
".tsx",
".json",
".vue",
".sass",
".scss",
".css",
".png",
".svg",
],
},
appType: "mpa",
clearScreen: false,
});
replace app/frontend/entrypoints/application.js
with:
import { createApp } from "vue";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const app = createApp({});
app.use(vuetify);
app.component("App", App);
app.mount("#app");
replace app/views/layouts/application.html.erb
with:
<!DOCTYPE html>
<html>
<head>
<title>Rails7Vuetify3</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= vite_client_tag %>
<%= vite_javascript_tag 'application' %>
</head>
<body>
<div id="app">
<app>
<%= yield %>
</app>
</div>
</body>
</html>
Let’s create vuetify
plugin in app/frontend/plugins/vuetify.js
:
import { createVuetify } from "vuetify";
import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
export default createVuetify({
icons: {
defaultSet: "mdi",
aliases,
sets: {
mdi,
},
},
});
<template>
<v-app id="vuetify-app">
<v-main>
<p>Vuetify 3 App Running 🍾🎉</p>
<slot></slot>
</v-main>
</v-app>
</template>
<script>
export default {};
</script>
Now if you refresh blogs#index
route (http://0.0.0.0:3000/blogs
) you should see Vuetify 3 App Running 🍾🎉
Now that we have vuetify
app working in our rails
views
, we can start adding individual pages for Blog index
and
show
actions. For this we will create pages/blogs/index.vue
and pages/blogs/show.vue
Vue SFC components in our app/frontend
directory.
Then we will reference these components in our rails
views
and pass data to it.
Create app/frontend/pages/blogs/index.vue
component:
<template>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-text>
<v-table hover>
<template #default>
<thead>
<tr>
<th class="text-start" style="width: 70px;">ID</th>
<th class="text-start">Name</th>
</tr>
</thead>
<tbody>
<tr v-for="blog in blogs" :key="blog.id">
<td>
<code>{{ blog.id }}</code>
</td>
<td>
<v-btn variant="plain" :href="blogLink(blog)">
<v-icon start>{{ mdiPost }}</v-icon> {{ blog.name }}
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mdiPost } from "@mdi/js";
export default {
props: {
blogs: { type: Array, required: true },
},
data() {
return {
mdiPost,
};
},
methods: {
blogLink(blog) {
return `/blogs/${blog.id}`;
},
},
};
</script>
And app/frontend/pages/blogs/show.vue
component:
<template>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-title>
<v-icon start>{{ mdiPost }}</v-icon>
Showing Blog {{ blog.id }}
</v-card-title>
<v-card-text> Blog contents go here: {{ blog.name }} </v-card-text>
<v-card-actions>
<v-btn variant="plain" :href="blogsLink()"> Back to Blogs </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mdiPost } from "@mdi/js";
export default {
props: {
blog: { type: Object, required: true },
},
data() {
return {
mdiPost,
};
},
methods: {
blogsLink() {
return `/blogs`;
},
},
};
</script>
Let’s register those components as async
components so they are only loaded when rendered. Update app/frontend/entrypoints/application.js
with:
import { createApp, defineAsyncComponent } from "vue";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const BlogsIndex = defineAsyncComponent(() => import("~/pages/blogs/index"));
const BlogsShow = defineAsyncComponent(() => import("~/pages/blogs/show"));
const app = createApp({});
app.use(vuetify);
app.component("App", App);
app.component("BlogsIndex", BlogsIndex);
app.component("BlogsShow", BlogsShow);
app.mount("#app");
Now, lets add some content in our BlogsController
and render components in erb
view files.
Update app/controllers/blogs_controller.rb
with:
class BlogsController < ApplicationController
def index
@blogs = [
{ id: 1, name: "My first blog"},
{ id: 2, name: "Another blog"}
]
end
def show
@blog = { id: params[:id], name: "Some random blog #{params[:id]}"}
end
end
Update app/views/blogs/index.html.erb
with:
<blogs-index :blogs="<%= @blogs.to_json %>" />
And app/views/blogs/show.html.erb
with:
<blogs-show :blog="<%= @blog.to_json %>" />
If you refresh toy blogs#index
(http://0.0.0.0:3000/blogs) page, you should see our vue
components rendering the
blogs table with links to navigate to individual blog pages.
Every time we navigate to a different page, we usually have to wait for the browser to fetch the necessary assets and
render the page. This process can cause some slowdown, even if we have effective caching settings in place. The browser
often needs to “recompile” or “run” these assets, further contributing to the delay. However, we can take advantage of
Turbo Drive
to circumvent the need for loading and “compiling” assets
with every page load. This approach leads to a faster and smoother user experience, resembling that of SPA.
First, let’s add required packages:
$ yarn add @hotwired/turbo-rails vue-turbolinks
Then update app/views/layouts/application.html.erb
to use turbolinks by adding 'data-turbo-track': 'reload'
to
vite_javascript_tag
. Your vite_javascript_tag
should like following:
<%= vite_javascript_tag 'application', 'data-turbo-track': 'reload' %>
Finally, update app/frontend/entrypoints/application.js
to utilize Turbo Drive
:
import * as Turbo from "@hotwired/turbo";
Turbo.start();
import { createApp, defineAsyncComponent } from "vue";
import TurbolinksAdapter from "vue-turbolinks";
import vuetify from "~/plugins/vuetify";
import App from "~/app";
const BlogsIndex = defineAsyncComponent(() => import("~/pages/blogs/index"));
const BlogsShow = defineAsyncComponent(() => import("~/pages/blogs/show"));
document.addEventListener("turbo:load", () => {
const app = createApp({});
app.use(TurbolinksAdapter);
app.use(vuetify);
app.component("App", App);
app.component("BlogsIndex", BlogsIndex);
app.component("BlogsShow", BlogsShow);
app.mount("#app");
});
Now if you inspect your networks
tab in your browser’s dev tools
, you will see that when navigating between pages,
related assets are only loaded once. You should also notice that rendering of the page is very fast compared to previous
experience.
We showed you how to set up brand new Rails 7 project with modern frontend framework. We also showed how to utilize Vue
components within your App’s erb
view templates, keep your frontend MPA while having speed close to SPA. We hope this
will help you in your next project.
Until next time amigos 🙌
Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …
Introduction Emails have become an essential means of communication in both personal and professional settings. But have …
No credit card required to sign up. All paid plans come with 30 day cancellation and full refund.
Get Started for Free