Skip to main content

19 Using VueJS for Nested Forms in Rails: Part 1

Episode 184 · May 1, 2017

Learn how to use VueJS as an alternative to Cocoon and other methods of building dynamic nested forms with Rails

Javascript Forms VueJS


Transcripts

What's up guys? This episode we have a lot to cover but we're building a nested form with rails using VueJS. So instead of using something like Cocoon, or building out your own JavaScript to handle this, we're going to be building a form like this where we can create a team, and the players can be added dynamically, So we'll have Chris, we'll have, Bob. And if we hit "Save team", that's going to save it to the database, and we can see Chris and Bob is in there, and if we were to remove an existing record, we actually get this cool "undo" functionality, where we can hit remove, and if it knows that it was already in the database before, then it will show you an "Undo", and you can undo that, or you can go and for example edit an existing record and add somebody to the team, and if you decide anytime on create or edit that you want to remove someone, if they aren't saved to the database that will just plain old remove them, but the existing records in the database will actually be flagged as "removed". That is pretty interesting if we hit "Save Team" we will see that Bob 2 is the only player in the GoRails team now, so this is a lot to cover because it's a fairly complicated subject, but let's dive in real quick to look at our controller and our models for this. This is all standard from all the rails side, all of this is very standard nested attributes and nested form stuff, so we have a Team that has many players, and the main thing here is that accepts_nested_attributes_for :players is going to allow us to create a team and submit player details alongside of it, and allow_destroy: true is going to allow to destroy them just like you saw in the example, and with our player model there's nothing at all special here. Players have a name, they have a position, you'll notice I didn't include it in the example, but they do have a position so you could add in other things like: They are a point guard or center and their number is 24 or whatever, you can go ahead and add in those extra attributes as you might like but we're just going to keep it simple for this example, because it is a lot of stuff to talk about. So our teams controller is the last piece that is in rails, this is all very standard stuff, so far we have a team scaffold actually for all of this. The important piece here is that we want the json response to render an OK response for when it gets created or updated, so we want to make sure that it was successful in both cases, and then our location for each of these is going to give us the url back that we can redirect you after it was successful, so we're going to make sure that we have that if it was successful, and if it was unsuccessful, we get an unprocessable_entity response back so our Ajax xhr request is going to recieve a failed post in both cases, and then it will return errors back and we can go ahead and use those in Vuejs to display the errors, and last but not least, we have added the players attributes, and some attributes for the players to the strong params method, so this namely is the same as normal, except it's the nested ones, and we include the id so that we can update existing records like you saw with Bob 2, and we have _destroy, so for existing records we can look it up with the id, and then we can call destroy on it if this flag was set to be true. All of our rails stuff here is set up to be very standard but it's our JavaScript and our form template that is going to be where all of the work is, so let's dive into that.

This application I have set up with webpacker, so we've used webpacker install to install webpacker, and then we've also used the webpacker install vue to get this hello view example. We're just going to modify this a little bit, and we're going to change that from DOM content loaded to turbolinks:load, and then from there we're going to get rid of the way that it sets itself up, and we're going to do a few different things. The first thing that we need to do is we need to make sure that we grab that element for the form, so this is going to be var element = document.getElementById("team-form") and if *element is not set to null, we will set this up so basically, we will set up vue only if that element exist on the page, so any other pages that we might have that doesn't have the teams form, this code will run, but it will not actually initialize the view element. Rather than using the template inside of a single file vue app, we're going to actually implement the template inside of the html form inside of the rails view instead of putting that in our JavaScript, so we can get rid of this components as well, and the import App because we don't need those for this situation. The other thing that we want to do is we want to import TurbolinksAdapter from 'vue-turbolinks', and then we also want to import VueResource from 'vue-resource' so that we have a way to send Ajax request that are a little bit easier, and then last but not least, we want to say Vue.use(VueResource) which will initialize all of that.

app/javascript/packs/hello_vue.js

import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'
import VueResource from 'vue-resource'

Vue.use(VueResource)

document.addEventListener('turbolinks:load', () => {
    var element = document.getElementById("team-form")

    if (element != null) {

        const app = new Vue({
            el: element,
        })
    }
})

The last thing we need to do here then is go into our application and say yarn add vue-turbolinks vue-resource and that's going to add those two to our packages.son and set those up so that we have them as accessible then. The other thing I want to do is when we have setup VueResource, I'm going to set up

Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

We'll have access to that csrf token that show up in every page, and we'll be able to submit that across as part of the Ajax request automatically. The other things we need to do is that mixins: [TurbolinksAdapter] here inside the vue app, and we can actually go inside the form itself in "Teams", and start building things out here, so what we really need to be able to do is two different things here, we need the form to have that id, and then we also need when you're editing a team, we need to have some json that vue view can load so that it knows the initial state when you're creating a new team there is no initial state because everything is empty already, and that is going to give us the ability then when we go to edit, we have the team and all of the players that we can load up into the vue as the state, and then the vue app can render all the form fields necessary to do that. What we're going to do is really just get rid of this entire form. Now there is some other options you could do for example you could use this form_with, and you can take this, and you could use the csrf token to outside of that form that it will generate a csrf token, you could use that. We're going to just do a div, but instead of doing one like this where we just write it all out ourselves, we're going to use a content_tag :div, and this way we can pass an id: "team-form", and we can do a few other options here. What we're going to do is say:

app/views/teams/_form.html.erb

<%= content_tag :div, 
    id: "team-form",
    data: {
        team: team.to_json,
        players_attributes: team.players.to_json, 
    } do %>

<% end %>

If you were to serialize the players inside of the team's json, which you could do, so you could do team: team.to_json(includes: :players), this is going to create an attribute or property on the json called players, but rails needs us to submit players attributes for the accepts_nested_attributes for, so what we're going to do is we're not going to include players inside of the to_json on this line, we're going to have a separate line here where we say: team.players.to_json on it's own line, that's going to give us the ability then to separate those out so that we can customize how they work, and then we can go through and combine them inside of our JavaScript just as a setup thing so that w e can make sure that the players attributes name is the name of the property. Now you can have them all together and rename it in your JavaScript, I'm going to do it this way so that we have them kind of separated out, but that is up to you, how you want to go about loading this data into your Vue app. We can go back to our JavaScript portion here, and we can change the way that our state gets loaded, our initial data by doing something like

var team = JSON.parse(element.dataset.team)

You can grab that data attribute called team and parse that out, we also have the player's attributes, which we can do the same thing on

var players_attributes = JSON.parse(element.dataset.playersAttributes)
players_attributes.forEach(function(player) { player._destroy = null });
team.players_attributes = players_attributes

We have to camel case that in order to access it properly, and then for this one we want to do something additional here, so that we can set that up automatically so that all of these will have the _destroy when we go and do an edit form. This will give us the _destroyed property that we can monitor with vue. If we don't have these ahead of time, vue is not going to be able to notice that we added a new property to our data set, so if we set this up ahead of time, it can monitor when this destroy property gets triggered or modified. We'll use that, and then last but not least we'll say: team.players_attributes = players_attributes now that we've gone and set that up correctly, and then inside of our vue app, we can say:

data: function() {
    return { team: team }
}

That's going to give us our initial data, so that we can get things set up, and one modification that I want to make into our html here is that I want to add:

_form.html.erb
team: team.to_json(except: [:created_at, :updated_at])

We don't really need those attributes for our form, and we're going to do the same thing for the players attributes. This would have included all of that stuff, and we don't really need those things, and the same goes for team id, we already know the team id on this line, so when we're setting the data in our vue app, we don't really need those, we can keep them, but of course if you take this data and you submit it over to rails, it's going to strong params all of this and say: Well, it wasn't allowed for you to submit the team id, and the created_at and the updated_at and so on, so we're going to get rid of these, and the only one that we're going to have that isn't allowed to be submitted is the team's id which we're going to use for the url when we edit, so we will have one attribute that will be permitted param and that will be the team's id, so this is going to get rid of all those other ones that we don't want for our form. Now we've done a lot of setups so far, but we need to build out part of this form, at least a little bit just to make sure that it works. If we were to set team name as a label here, and we do an input here, we should be able to something which will connect our input to vuejs's data, the internal state of the vue app, and this is going to look for the team property, and insde of the team object, it's going to look for the name property, and so this will automatically be populated with whatever is on the dataset, and then if you were to edit this, it's going to update the dataset in vue and keep those in sync, so this is basically data bindings if you're familiar with angular, and that is how this is going to be connected. Of course before we check this out in the browser, we need to make sure that we are loading our JavaScript pack tag for our team editor form thing. JavaScript pack tag is what you would add to your application html erb, and then you want to reference the file that you wrote your code in. In our case we just put it in that hello_vue javascript file

application.html.erb

<%= javascript_pack_tag 'hello_vue' %>

That is going to load the JavaScript file, and so that is going to load this code right here that we have. Of course, you should rename this to a better name so it's more descriptive of the team form, but that's going to give us the ability for us to go in and check the scripts here, and we'll see that the script hello_vue.js from port 8080 is being loaded because I am serving that from the web pack dev server. If we go to new team, we will see that we get this form, and it actually will show up with or without vue js. Now what won't happen if you don't have vue, is you won't get this development mode vue thing, and I have the Chrome extension for vue, so we can click on that, and it will hightlight the elements on the page that are the vue app, and that is going to also show us the data set for that. If we were to type in here, we will see that it automatically updates the team name, and this is going to be how we can take the inputs, map them to views data set, and then we can take the data set and sent that right over to rails because we've designed this so that the team and players attributes and all this stuff map exactly to the way that rails strong params is expecting us to send that stuff over. What we've done, is designed that json to set up vue to understand the exact same mapping that we need in rails, and that's going to give us a really seamless way of building our nested forms and submitting that with xhr requests. This is a start, we have the team name being set up, and that is good. If we want to save this, we don't have the ability to do that yet, but first let's talk about adding players and building that out before we go submit this to rails. Let's go back to our template here, and we get to use all of vuejs's nicities, which means that we can add a player section, let's build a div that will be:

app/views/teams/_form.html.erb

<h4>Players</h4>
<div v-for="(player, index) in team.players_attributes">

</div>

This is going to create a dive that inside, whatever template you define here, is going to list out those, but of course we have no ability to add a new player, so none of this is going to be very useful. What we need is a button down here at the bottom, and

app/views/teams/_form.html.erb

<button v-on:click="addPlayer">Add Player</button>

app/javascript/packs/hello_vue.js

methods: {
    addPlayer: function() {
        team.players_attributes.push({
            id: null,
            name: "",
            //position: "",
            _destroy: null
        })
    }
}

If we go to our vue app again, we should now be able to click "Add Player", and we can see that that index number is showing up, and that means that we're successfully adding items into the player's attributes array, and you can see that right here. We see those, and all of that is working correctly.

Now of course, this index by itself isn't terribly useful, we want to actually do a similar thing where we want to have the player name as a label, and then we want to have an input type:

_form.html.erb

<label>Player Name</label>
<input type="text" v-model="player.name">

This is actually pretty smart because this is going to associate it to the correct player, so when you edit one of these, it's going to associate it to that player, and then modify that player's name, which is pretty cool, so if we go back to our browser, we add two players, and we take a look at our dataset for the player's attributes, and we open these up so you can see their names. Then you can type into the second one, and that will get updated, and the first one will get updated when you update that one. So the v for stuff is actually pretty wise when you define this, because then we can have our player names automatically associated with the correct one and we don't have to worry about: Is it player with the index of whatever that we would update? None of that. All of that is taken care of for us, and that is pretty cool. Now we also need the ability for us to remove the player, and we can do that by adding a similar button called "remove":

_form.html.erb

<button v-on:click="addPlayer">Add Player</button>

hello_vue.js

methods: {
    //addPlayer code 

    removePlayer: fuction(index) {
        this.team.players_attributes.splice(index, 1)
    }
}

Assuming we did everything correctly, we can have "test", and we can remove these, so if we do numbers as well, so you can see which ones we're removing, it will correctly remove the correct item when you click the associated button, so we know that it's taking the index, and it's removing the correct element out of vue state which then goes and triggers re-rendering the form.

I'm going to cut this episode off here because we still have quite a bit to do where we want to be able to take this dynamic form that we've created, and we want to be able to persist that over to the database, but we also want to make sure that this works when you're editing an existing team, not just creating a new team. So there's a lot of intrecacies to the editing that's different, then the set up originally, and we're going to be able to build our vue app that handles both, but I'm going to do that in a part 2 of this episode because I don't want this one to be an hour long. I hope you enjoyed this so far, and I'll see you in the next episode

Transcript written by Miguel

Discussion