Rails with Vue - Accessing Rails Data
Written by Eddie • 3 March 2020
This tutorial assumes you have read the first article, found here. If you want to skip straight to this step, pick up a copy of the code you'll need from this repo
1. Adding a rails model
Vue components are fantastic on their own, but they really shine when we can pass data from our Rails backend into them. I'm going to explain how to achieve this, but first we need some data to use.
This part very briefly covers the creation of a model, an index page, and seeds for your database so that we have mock data to work with
# Create the model with a title and a description
$ rails g model film title:string description:text
> invoke active_record
> create db/migrate/20200303090850_create_films.rb
> create app/models/film.rb
> ...
# Create a controller with just an index action
$ rails g controller films index
> create app/controllers/films_controller.rb
> route get 'films/index'
> invoke erb
> create app/views/films
> create app/views/films/index.html.erb
> ...
# And run the newly created migrations
$ rails db:migrate
> == 20200303090850 CreateFilms: migrating ======================================
> -- create_table(:films)
> -> 0.0485s
> == 20200303090850 CreateFilms: migrated (0.0485s) =============================
# config/routes.rb
# For the sake of future expansion, we will update this to be a resource:
resources :films, only: :index # -> Formerly get 'films/index'
Your codebase should be looking a lot more fleshed out. Navigate to localhost:3000/films to see the new page.
Cleanup
To make this a touch nicer to use, we need to make a few changes.
-
Move your
<%= javascript_pack_tag 'hello_vue' %>
fromapplication.html.erb
tolanding.html.erb
(You can use this opportunity to clear out the default text as well)
<%# app/views/layouts/application.html.erb %> <!DOCTYPE html> <html> <head> <title>VueTestApp</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= javascript_pack_tag 'navigation' %> </head> <body> <%= yield %> </body> </html>
<%# app/views/static_pages/landing.html.erb %> <%= javascript_pack_tag 'hello_vue' %>
-
Add seed films
# db/seeds.rb (0..9).each do |i| Film.create(title: "Film-#{i}", description: "This is film #{i}") end
$ rails db:seed
-
Add a second link to your navigation component
(You could achieve this by adding a second element to your template, and expanding the data... I'm going to take it a step further)
// app/javascript/navigation.vue <script> export default { data: function () { return { links: { home: { path: '/', text: 'Home', id: 1 }, films: { path: '/films', text: 'Films', id: 2 } } } } } </script>
Here I've updated our data object to be a collection of
links
as opposed to separate ones. This is so we can iterate over them and create a link for each one, meaning that in order to expand these in the future we just need to add an entry to the data.I've also added an
id
field to each. This is for the vue loop, which needs a key for each item in the loop. While you can just use the entire item, it is recommended for efficiency to use a unique id.<template> <div class="navigation-bar"> <a v-for="link in links" :key="link.id" class="navigation-item" :href="link.path"> </a> </div> </template>
This is the syntax for the Vue
for loop
. It has been instantiated within the tags as this is the element you want to create for every item. As mentioned above you can see thekey
has been assigned to the link id, and as per usual the syntax has been updated for variable attributes.Save your changes and visit your page without refreshing - you should see your navigation bar has become slightly more useful!
6. A Rails API
To access your seed data in your frontend, we're going to add an API view to your rails app. If you haven't created a rails API before, let me assure you now that it is astoundigly easy.
-
Retrieve the data
In your
films_controller
#index
action, get a list of the available films.# app/controllers/films_controller.rb def index @films = Film.all end
-
Add a
json
response usingjbuilder
# CREATE app/views/films/index.json.jbuilder json.array! @films, :id, :title, :description
-
That's it. Really.
Navigate to localhost:3000/films.json if you don't believe me
7. Accessing your data
At this stage we have:
- Data in our Rails backend
- An API to access this data
- The knowledge required to make Vue components, including iterating over a dataset
Now we put them all together!
-
Creating the base
Vue
componentCreate a new component as following:
> app > javascript + films-list.vue
<!-- app/javascript/films-list.vue --> <template> <ul class="films"> <li v-for="film in films" :key="film.id" class="film-item"> : </li> </ul> </template>
<style scoped> ul { padding-left: 0; } li { list-style: none; text-align: center; } </style>
For the script, we're going to need to access the api. To that end, we're going to use a node module called axios.
$ yarn add axios > ... > success Saved 2 new dependencies. > info Direct dependencies > └─ axios@0.19.2 > info All dependencies > ├─ axios@0.19.2 > └─ follow-redirects@1.5.10 > ✨ Done in 3.61s.
This lets us make API calls from within our Vue components, like so:
<script> import axios from 'axios' export default { data () { return { films: [] } }, created () { axios .get('/films.json') .then(response => (this.films = response.data)) } } </script>
This might need some explaining...
We've created our usual data structure. By default we're using an empty array for
films
so that the for loop will just skip.created ()
is a lifecycle hook in Vue. It's similar to the Railsbefore_save
andbefore_action
callbacks. More information is available here, but this istance says that once the element has been created run the axios request.The axios request itself sends a
GET
request tolocalhost:3000/films.json
- much like you do when you visit this page in browser. As such you can confirm that this returns a json object in the format:"films": { "0": { "id": "1", "title": "Film-0", "description": "This is film 0" } }
And this json structure is what we're assigning to our Vue data.
-
Creating the js middleman to add the Vue component
> app > javascript > packs + films-list.js
// app/javascript/packs/films-list.js import Vue from "vue" import List from "../films-list.vue" document.addEventListener("DOMContentLoaded", () => { new Vue({ el: "#filmList", render: (h) => h(List), }) })
This is slightly different to the ones we've seen before - we aren't adding it at a relative point (ie we're not injecting it at the top of the html body). Instead we're telling it to look for a element with the id
filmList
that it will attach to. -
Update webpacker...
As before, exit your
webpack-dev-server
withctrl + C
.# Update with the new Vue components $ bin/webpack > [./app/javascript/packs/application.js] 802 bytes {application} [built] > [./app/javascript/packs/films-list.js] 224 bytes {films-list} [built] > [./app/javascript/packs/hello_vue.js] 347 bytes {hello_vue} [built] > [./app/javascript/packs/navigation.js] 305 bytes {navigation} [built] > + 47 hidden modules # And restart the server $ bin/webpack-dev-server > ... > ℹ 「wdm」: Compiled successfully.
-
Import your Vue component, and give it an element to attach to
<%# app/views/films/index.html.erb %> <%= javascript_pack_tag 'films-list' %> <div id='filmList'></div>
All being well, refresh your localhost:3000/films and see your rails data!