Skip to main content

17 Using VueJS for Nested Forms in Rails: Part 2

Episode 185 · 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

Welcome to episode 2 of our vuejs Nested forms with rails episode. Last episode we actually went through all of this where we set up webpacker and all of that so that we have this nested form, that works pretty nicely. We can have several of these records that we would add, and we could be removing them if we decided we didn't want that player, and we know need to be able to submit this to the database, and that's going to give us the ability to persist it, and then we can go work on editing this form where it's very similar, but we want it to function a little bit differently in a few different cases so let's dive into what we've got so far.

First off, we have replaced the rails form with this content tag, and the id of "team-form", so vue will mount this, we've also given it some data that we can load into vue initially, and this is going to be for the most part empty objects that we have at the beginning for creating a new record, and then we've actually implemented our vue html template right here inside of the html. Our vue code for this is actually getting sort of long, but most of this is setting up our vue app, so this is actually taking all the data and getting it ready for vue, and then we simply set that as the data set for vue, and we are good to go. We've also added an "Add Players" function which simply pushes an item onto the player's attributes array, and this team.players_attributes.splice(index, 1), and it should be this.team.players_attributes.push so that we're always accessing that variable appropriately so that did work without this, but we probably want that in there. The next piece that we need really is we need a submit button, so if we had a:

_form.html.erb

<button v-on:click="saveTeam">Save Team</button>

hello_vue.js

methods: {
    //addPlayer code 

    //removePlayer code

    saveTeam: function() {
        this.$http.post('/teams', { team: this.team }).then(response => {
            console.log(response)
        }, response => {
            console.log(response)
        })
    }
}

If we go give a test team, and we add myself as a player, and now we click "Save Team", nothing happens, but you will see that we get a console.log from response from the url: "/teams". We know that it was successfully created, and we really would like to redirect to that team. So here inside of the body you can see there's the id of one, and then gave us a url, but it gave us the url to the .json version of it, and we want to redirect to the html version of it, so we can either modify the way that that url gets generated and change the format for it, or we could just link to "team/1". It is up to you how you want to do that, but when it is successful, we want to redirect to that. We have two options. We could do

hello_vue.js

methods: {
    //addPlayer code 

    //removePlayer code

    saveTeam: function() {
        this.$http.post('/teams', { team: this.team }).then(response => {
            window.location = `/teams/${response.body.id}`
        }, response => {
            console.log(response)
        })
    }
}

We could do this, the other thing we could do is

Turbolinks.visit =(`/teams/${response.body.id})`

because we have rurbolinks available to us , we might as well do something like that. If we give ourselves a second team, with Chris as another player, we can "Save team", and we go to "teams/2" and Turbolinks has done the navigation for us, so this is really great, we get to use all the Turbolinks goodness that we get, and this is going to maintain all the window history and all of that because we're using Turbolinks, and it is just a good way to go about it if you're using Turbolinks as well. That works pretty nicely, we don't have the ability to see if we actually got those players saved, so let's go to the team's show page, and here let's do

show.html.erb

<h4>Players</h4>
<% @team.players.each do |player| %>
    <div><%= player.name %></div>
<% end %>

<hr>

We should see that we get the players names listed out but if we hit "Edit", this works pretty well, but if we do "Remove" there, it's not going to actually remove the player correctly, because actually what you see here is that we created a new team entirely which wasn't the plan. Our team 2 is still there, we didn't remove Chris, but we actually created a whole new team, so we have to go modify our vue code quite a bit, even though it appears to be working pretty well, it's not. That is because we need a different url for the update, and that should go 'teams/2' instead of '2/teams' and it should be a PUT request or a PATCH instead of making it a POST request. We have some modifications to make to our vue code, and I'd like to make it so that if you remove a player who currently exists, we just display that undo because that is more useful for us when we are editing a team, and we're like: We need to remove this player, but it's useful for us to see which ones we're removing versus which ones we're adding. Probably the best place we can start is to actually check inside of saveTeam if there is an existing team in the database or not. if (this.team.id == null) then we know it's a brand new team and we can do our POST request. If if isnt then we know that we are updating an existing team, and this should actually be a PUT request. One thing that we want to do is we want to put to the team with the id and the url, so if we change this to use the interpolation syntax, we can do

this.$http.put(`/teams/${this.team.id})`

That will make a PUT request to that url, and we should be set. This is going to allow us to edit an existing team or to create a new team.

This can be cleaned up a lot, but I'm going to leave that up to you as a challenge but we are going to do it quick and dirty right now, and what we want to do is test this out here, and see if I was to change my name and hit "Save Team", we go back to the correct "teams/2" and the player has been updated appropriately, so that is good. We now have that ability to edit existing records, and if we were to add a player. So we say "Bob" and we click "Save Team", Bob is now then added to that team, but if we wanted to remove Bob, we remove Bob and click "Save" and Bob still exists, so this is because we have to pass in that _destroy flag, and our remove button should actually check to see if the player has an id, and if they do, then we can set that flag, otherwise we actually just remove them from the array. So this is a remove player code, and really we need to grab that player. So if we say

hello_vue.js

saveTeam: funtion(index) {
    var player = this.team.players_attributes[index]

    if (player.id == null) {
        this.team.players_attributes.splice(index, 1)
    } else {
        this.team.players_attributes[index]._destroy = "1"
    }
},

That is all we have to do to set that flag, and going back into our browser, if we actually pull up the players attributes for Bob and display that here, we see that it's null by default, and then we click "Remove", and that didn't really update immediately but it does who if we refresh the data in the view thing, so it actually has been update, it didn't instantly reflect it here, but Bob has actually been removed or will be removed when we click "Save Team", so it did get removed, but our vue app doesn't actually understand what the destroy flag should do, and there are some options for it. One is:

_form.html.erb

<div v-if="player._destroy != '1'">
    -- Player name  
</div>

If we click "Remove Player", the Chris player has been removed, we can refresh the vue template down here, and we'll see that destroy is set to 1 and that player is still there, so it will still be submitted to rails but it will get destroyed when rails sees that flag. That works, but it's not the most intuitive because there's no way for us to know who already exists that is removed, and it isn't the best user experience. What I really want is two different things here, so I want a:

_form.html.erb

<div v-if="player._destroy != '1'">
    {{ player.name }} will be removed 
</div>
<div v-else>
    <label>Player Name</label>
    ...
</div>

That will show you that "Chris 2" will be removed, so this works really really nicely to be able to work with any of these, and use if statements in vue code, and have those templates change. Now, of course we can click remove, but then we have no way to undo it, and it's especially important if we have this visible that we should be able to remove or undo that, so we can add a button in here as well called "Undo", so we'll grab this button code, and we'll paste that up here as well, and change the name of that:

_form.html.erb

<div v-if="player._destroy != '1'">
    {{ player.name }} will be removed. <button v-on:click="undoRemove">Undo</button>
</div>
<div v-else>
    <label>Player Name</label>
    ...
</div>

hello_vue.js

undoRemove: function(index) {
    this.team.players_attributes[index]._destroy = null
},

We don't need to look up the player because we can already assume that if you were able to click this button, then this player must have already existed in the database because of the layout only shows this button when the player has been marked as destroyed, so we know that player is going to be on the database, that way this code can be a one-liner, and there's no conditional here either. If we go back and we hit "Remove", "Chris 2" will be removed, we click undo, and it goes right back to that form where we can edit and we can save, so we can have Bob here, save the team, and it updates and removes correctly.

We can see all of those reflected in the rails logs, if you go down to the very bottom, you will see there is a delete for Chris, and the update to the Bob record to name him "Bob 2", all of this we know for sure is updating the existing records because we're passing in those ids appropriately, our parameters match up exactly with what rails would normally expect. If you were using Cocoon or another nested form of doing things, even if you built it from scratch with your own JavaScript, you can go ahead and pass those attributes the same way, so we are taking care of all of that, using vue, and we did a little bit of naming hacks to make sure that our player's attributes are set correctly. The one thing that we have yet to do is the Unpermitted parameter: id and this is just that we're submitting the id of the team and it's not allowed to be changed, and that we do want to remove from our our POST or PUT request because that is something that we don't want people to change.

One solution for that would be to add the id:

_form.html.erb

<%= content_tag :div, 
    id: "team-form", 
    data: {
        id: team.id,
        team: team.to_json(except: [:id, :created_at, :updated_at]),
}

hello_vue.js
this.team.id changes to this.id

Then our POST or our PUT will use the appropriate id, and the only other thing that we have to do is assign that to

hello_vue.js

var id = element.dataset.id

Pass that in as part of our initial data that goes into vue when it loads up: return { id: id, team: tem } If we do that, we should be able to hit "Edit", and we should be able to then edit, hit "Save" and taking a look at our rails logs, we don't get any Unpermitted parametees any more, So you do have to keep in mind those, because when you're doing the json dumps into that, we need it to do it the same way that a rails form would do, but a rails form doesn't actually need the id, except when it generates the url of the form, and the same goes for our vue code, but we need to get that id somehow and the best way is not to put it inside of the team json object, it's actually to have it separate.

That is one solution to that problem, and there's a lot of things we can improve here and clean up, but I wanted to walk you through that process of building out nested forms with vuejs where we could go create a brand new team, like a GoRails team, and add Chris to it, hit "Save", create it, and we can use the exact same vue code to edit the team and the players and all of that stuff and it will work just as you would expect if, and actually I think that our code for this turned out really really nicely because while this is quite a bit of stuff to handle all the actions, that form is actually quite complex because you have the adding players, removing players, the undoing of remove, and you have the submit to rails and all of that layout stuff has been nicely written in tha save form file as your rails form would have been as well, so all this code is actually where you expect is, so if you were to go change the way the form looked, you could just dive into this and you know that this is going to change the way the forms looks, so this works pretty nicely and gives you the ability to go and use vuejs for complex things like a nested form without having to go the whole full single page app route and diving into vue router and json webtokens, and forcing your app to be an API and then doing pre-rendering so that you get SCO. That is a lot of work to do, when you could actually just drop this in on those little locations where you need something more complex.

That is it for this episode I hope you liked this two part series on vuejs and nested forms, if it was useful, let me know, and if you want to see more of this stuff let me know. Any suggestions or topics that we can dive into more, I'm having a lot of fun using vue alongside of rails without doing the single page app approach, and I'd like to explore that some more, so let me know if you have any ideas and we will get on to those in the future. Peace v

Transcript written by Miguel

Discussion