Sunday, 24 January 2021
Learning Ruby on Rails - Part 2
Last week I went by the traditional way of building web apps - using server side rendered Ruby templates .erb
files. But I really like using Vue on the frontend, and Inertia.js as the adapter between the frontend & backend. I've been building quite a few Laravel apps that way and I really like the speed and simplicity of this stack. Not to mention that by now I have a growing collection of Vue components and layouts for lots of scenarios. So let's convert our Matzati app to a Modern monolith.
Inertia Dependancies
The Inertia docs have instructions how to get started in a Ruby app:
gem 'inertia_rails'
Add the above to the Gemfile
, which is like composer.json
or package.json
, then run bundle install
to install the dependencies.
Root layout
We also have to update the root view (application.html.erb
) to load the js assets that boot up the Vue frontend, and insert <%= yield %>
in the body - that Inertia will populate with the Vue app.
Inertia Responses
We can now send Inertia responses from the controller! An Inertia response is comprised of 2 parts:
- The path to the Vue component to render.
- The json data that will be passed as props to that Vue component.
Let's update the aveidas/index
route to return an Inertia response:
def index aveidas = Aveida.all render inertia: 'Aveidas/Index', props: { aveidas: aveidas.as_json } end
Now, when reloading the page it's blank! That's because haven't yet setup Vue for the frontend. 😀
Vue Frontend
Let's use Vue 3, because it's fun to be on the cutting edge:
yarn add vue@next
Then install the vue-inertia client-side adapter:
yarn add @inertiajs/inertia @inertiajs/inertia-vue3
The we follow the instructions to setup the main application.js
:
import { createApp, h } from 'vue'import { App, plugin } from '@inertiajs/inertia-vue3'const el = document.getElementById('app')createApp({ render: () => h(App, { initialPage: JSON.parse(el.dataset.page), resolveComponent: name => require(`./Pages/${name}`).default, })}).use(plugin).mount(el)
We can see in line 9 that resolveComponent
expects the page components to reside in ./Pages/${name}
relative to this file. So I guess we should create a Pages
folder, and then Aveidas/Index.vue
which we passed as the path in the aveidas_controller#index
method's response.
Here we can also see how the core of Inertia works: the server renders a single div#app
, that looks like this:
<div id="app" data-page="{'component':'Aveidas/Index','props':{'aveidas':[{'id':1,'title':'Menorah','body':'Lost a silver Menorah on Zos Channukah','created_at':'2021-01-16T21:06:11.660Z','updated_at':'2021-01-16T21:06:11.660Z'},{'id':3,'title':'Smashed cake','body':'For my daughters cake smash party','created_at':'2021-01-16T22:01:59.487Z','updated_at':'2021-01-16T22:11:29.165Z'},{'id':4,'title':'hello','body':'world','created_at':'2021-01-16T22:02:14.313Z','updated_at':'2021-01-16T22:02:14.313Z'}]},'url':'/','version':null}"></div>
It simply inserts the component path & props as json in a data-page
attribute on the div#app
right in the HTML. Then, in application.js
Inertia retreives that with initialPage: JSON.parse(el.dataset.page)
(in line 8). That's how Vue gets the initial data from the server on. the initial visit.
Once the app is loaded and you click on a link to another page, Inertia intercepts the click for that we use the <inertia-link href="..."/>
component instead of a regular <a href="..."/>
, and instead of making a full page reload, sends an XHR request, and updates the page & props with the JSON response. This makes subsequent page visits really smooth.
Now that I wrote it out, I think Inertia is really simple to understand!
As I'm completely new to Rails ans its ecosystem, I got stuck at loading Vue SFCs with Webpack. This article saved me. See this commit for the code at this point in time.
So now we create the Aveidas/Index.vue
component:
<template> <div> <h1>Aveidas</h1> <ul> <li v-for="aveida in aveidas" :key="aveida.id"> <h4>{{ aveida.title }}</h4> <p>{{ aveida.body }}</p> <inertia-link :href="`/aveidas/${aveida.id}`"> Show </inertia-link> </li> </ul> </div></template><script>export default { name: 'AveidasIndex', props: { aveidas: Array },}</script>
We get aveidas
as a prop from the Inertia response, and use it like a regular Vue prop. Also Notice the usage of <inertia-link>
to link to the show
page for each aveida. That ensures that Inertia will handle the page visit.
Some Gotchas
When redirecting in Rails, we use redirect_back
or redirect_to ...
. I expected a redirect to an Inertia route to render the Vue page, instead I got a Turbolinks.visit(...)
response. I had to disable the default Turbolinks, as we want to handle navigation with Inertia and not the default Turbolinks. So just removing gem 'turbolinks', '~> 5'*
from the Gemfile
did the trick.
When performing a POST
or PATCH
request, Rails tries to verify the CSRF token. In traditional apps the CSRF token is attached to every request. But as we're using Inertia to make XHR requests, we need to configure it to also attach the CSRF token. One way of doing this is telling Rails to attach tje token to a cookie:
class ApplicationController < ActionController::Basebefore_action :set_csrf_cookie# Store the CSRF token in a non-session cookie so Axios can access it def set_csrf_cookie cookies['XSRF-TOKEN'] = form_authenticity_token endend
Then configure axios
(which is what Inertia uses under the hood) to pass on the cookie:
import axios from 'axios'// Tell Axios to send the CSRF token (taken from the cookie)// in the header named as "X-CSRF-Token", as this is the name// expected by Railsaxios.defaults.xsrfHeaderName = 'X-CSRF-Token'axios.defaults.withCredentials = true;
To get the dev-server with HMR running, run this command from project root:
./bin/webpack-dev-server
Wrap
That's it for now. Setting up Inertia and Vue with Rails was fairly straightforward for the most part, I just stumbled upon configuring the Vue SFC loader, and the dev server. But this looks promising! Next up I want to add TailwindCSS (v2) to this app to make styling fast and fun💡.