Skip to main content

45 Rails & Vue.js Trello Clone - Part 1

Episode 217 · November 29, 2017

Set up Rails models, scaffolds, and webpacker with Vue.js to start rendering our views

Javascript VueJS


Transcripts

What's up guys? This episode we're going to be building a clone of Trello, which we've been talking about in previous episodes, where we're going to use Vue.js to build the drag and drop support for the list themselves and the cards that are underneath them, and then we're going to be syncing this back to the rails app using AJAX, so that's what we're going to be doing in this episode and I hope you enjoy it. Let's dive in and take a look at Trello real quick if you aren't familiar with it.

They have the ability for you to add lists, where you can add multiple columns in here, and you can archive them and so on, but then you can underneath them add cards which you can give Titles and you can drag them around. You can also drag and drop those lists around, and so every time you make one of these changes that will be persisted server side, so that all of your changes are synced and saved in the database, so next time you open this up it will be visible. We're going to be replicating this where you can add lists and cards underneath these lists, and then have the same drag and drop functionality using Vue.js on the front end.

This is one of those things where you are required to use JavaScript for drag and drop so it makes sense for us to use something like Vue so that we can have some of that functionality taken care of for us when we're building out all of the JavaScript for the front end. What we've got right now is just a blank rails application that has devise and bootstrap installed, we're not even using devise at all, we don't care about user accounts in this example but we do want devise to give it some sort of layout and some helper classes in CSS that we can use to make this look a little prettier. What we're going to do is we're going to go and generate a scaffold for the list model, this is going to need a name for that title of the list, and then it's also going to need a position of integer which will denote which column it should be, so it will start at one, two, three, four, and if you drag and drop it will be able to remember the position, so next time you refresh the page it will be in the same spot.

rails g scaffold list new position:integer

So we're going to need that, so we're going to generate that scaffold, and then we're also going to need to generate a scaffold for cards that refer to the list

`

That's going to allow us to point back to the list, those are going to have names, they can have descriptions, assigned users, all kinds of other things you might want to add. We're not going to worry about any of those, but we're also going to have a position here, so we know if you drag it down in the list we can save it in that position, and keep it in the same correct list as well, so we're going to generate both of those, and then we're going to hop into the Gemfile, and we'll add the

gem 'acts_as_list'

This is going to be the gem that allows us to do that sorting for those positions and all of that fun stuff, then we're going to rails db:migrate to run those migrations, and our models folder is going to have a list, and we want to make sure that we have the has_many :cards association in here, but we also want to tell it by default we always want you to order these by position ascending, and so that's going to make sure that these cards are always in order based upon the acts_as_list position, then we're also going to say dependend: :destroy because if you remove the list we want to also remove those cards, and this should be acts_as_list so that the lists themselves are organized appropiately. Then, let's also add a validation here for validates :name, presence: true keep that validation in there just to make sure that our database is set well, we're going to have acts_as_list here as well, but this one is going to have a scope of the list but you're going to have as a belongs_to :list here. So the card belong to a list, and then they use that list as the scope and then this is going to valiate name, presence is true as well, and that's going to make sure that our names on our cards are always added as well. Before we start hopping in our views, we also want to

lists_controller.rb

def index
  @lists = list.order(position: :asc)
end

So it also reflects the same ordering that our lists are in the database, so acts_as_list we want to apply here and use that same sorting as well. We could also do sorted as a method here and we could go into lists and say let's add scope :sorted, ->[ order(position: :asc)] and that would be the same thing, we can do that as well just to give that a better naming scheme so it could be referenced in other places as well. With this said, we can go to our routes file, and let's move the cards and lists down here to the bottom, where this should be, and then we have our resources list that we want to make as our root route. So when you come to the website, we'll start up our server, when you come to the server you'll be presented with the list index page, so once that is loaded, we'll be able to create a couple lists here and we'll go and change up that UI so that we have the list in columns instead of where they will be in a table right now. Right now we'll have name, we'll have our backlog, we'll have a new list for in progress, we'll have a new list for completed, and we can go to that form if we wanted and say: Well, our list forms shouldn't actually add the position in there because that is automatically handled by the database acts_as_list plugin, so that's going to take care of those in order, but what we should do is in our index we should maybe get rid of this table, and handle this some other way, so what we might do is we might say: Well, let's give it a class of 'row' and this is just bootstrap stuff for us, but we might say that all of these columns or div

index.html.erb

<div class="row">
    <% @lists.each do |list| %>
    <div class="col-3">
        <h5><%= list.name %></h5>
    </div>
    <% end %>
</div>

This isn't going to be exactly the same as Trello, where you would actually want a div that overflows and scrolls left and right as opposed to this where it wwill actually wrap, so we would have to come back and change some of the CSS later on in order to make these the proper widths and so on, but for now we can prototype this out with bootstrap pretty easily and get well on our way, and then we can go back and change the CSS up to make it actually scrollable if we have multiple columns like we do in Trello. Really, we still have rails controlling our UI here, and it would be nice if we actually had this automatically handled for us by Vue.js. That's what we're going to do here, we're going to create

<%= tad.div id: :boards, data { lists: @lists.to_json(include: :cards)} %>

What this is going to do is take our database list the cards, convert them to JSON with the nested structure and everything, and then we're going to give that as preview data for Vue.js to load up, so when the page loads it knows all of the lists and cards that are available, and then we can render those out using Vue.js instead of using rails. What this will look like now is nothing, but we will be able to then use webpacker to set that up.

What I'm going to do is go to our console, we're going to run rails webpacker:install:vue This is going to give us all the defaults for Vue, that will go ahead and give us the configurations file stuff and a really simple example, but we're going to make some modifications to this, and we're also going to yarn add vuedraggable which we'll use as the library to make drag and drop and sorting of our lists, so we'll add that requirements right now, and then we'll also go up to our application, or our head tag, and then change javascript_include_tag to javascript_pack_tag and stylesheet_pack_tag for application on both of those, that's going to set it up so that it will use and include the stylesheets in JavaScript for our webpacker and application js file, and so here we'll go to packs/application.js and we're going to define our Vue.js application in here. Now, we're going to use Vue import:

import Vue from 'vue/dist/vue.esm'
import App from '../app.vue'

window.store = {}

document.addEventListener("turbolinks:load", function() {
  var element = document.querySelector("#boards")
  if (element != undefined) {
    window.store.lists = JSON.parse(element.dataset.lists)

    const app = new Vue({
      el: element,
      data: window.store,
      template: "<App :original_lists='lists' />",
      components: { App }
    })
  }
});

This is basically saying: Any app tag that you see in the template is going to reference the app variable, which is our app.vue file, and so that is going to get everything loaded up in our app.vue file, so from here we can just try this out and you should see we get hello vue, and everything is loaded correctly, but we can actually go and say: Well, we received some props called: original_lists and when we do that, that says that if we go back to application.js, so if we go to that in our text file, we are passing in an original_lists and that equals the data.list. This is actually giving tht original_lists in as a prop, and so we have the original list, here's a prop which we can accept, and then we can say

<div v-for="list in original_lists">
    {{ list.name }}
</div>

We can print all of those list names out, and here you will see backlog, in progress and completed, then if we wanted to make those columns once again, we can have our classes row at the top, and then for each of those lists we can have

<div v-for="list in original_lists" class='col-3'>

and we can go back and now we can see we have three columns. If we want to give those h6 headers, we can do that, and then we could go and give them, say a little breaker there, and we could have

<div v-for="(card, index) in list.cards" class="card card-body">
{{  card.name  }}
</div>

Because we haven't created any cards yet, we can go create one in the database, and if we run rails console here, we can have

List.first.cards.create name: "Test"

That will create a card in our database, and we can now see that we have a card here and if we did that, same thing with the last list, we could put one over the end as well, and so what we want to be able to do is have these as drag and droppable, but we also want to be able to create new cards in the UI as well as drag and drop them in the columns themselves, so there's quite a lot that we want to do here, but for now we have the visual UI laid out really easily, so I'm going to cut this off here, and we're going to come back next episode and start working on the drag and drop functionality as well as creating new cards using AJAX and Vue draggable to do all of that dragging stuff.

Discussion


Gravatar
Dan Chap on

Glad you decided to do this :)


Gravatar
Thomas Galibert on

Great episode ! Do you think it's better to grab the json data rendered inside the html view or making an ajax call instead ?

Gravatar
Chris Oliver (169,620 XP) on

AJAX will always be slower because you're making a second network request and will always force a 'loading' state on the Vue app. I personally hate waiting for widgets to load so I'd rather preload the JSON on the page and have an instantly available frontend.

Gravatar
Thomas Galibert on

thanks Chris, ...yes it makes sense :-)


Gravatar
Francisco Quinones (7,370 XP) on

Great ep. Chris


Gravatar
Tyler Bodway on

I love these videos! I feel so pressured to go all-or-nothing with JavaScript, but I love the way you integrate the best parts of it into an already beautiful Rails app. Rails and Vue are a great pair


Gravatar
James Shepherd (600 XP) on

Hey Chris, I feel like a missed a couple of episodes leading up to this one. You use some gems here I am not familiar with like 'devise_masquerade' and a few others. Do you have either a copy of the code base that you started out with for this episode or can you point me to the episode(s) where you built everything we are starting with here? Thanks!

Gravatar
Chris Oliver (169,620 XP) on

Yeah, I have a really basic template that installs Bootstrap, Devise, and Administrate. It's really nothing more than if you install those gems yourself and create a root page. I'll publish it sometime soon once I fix a couple bugs in it.

cc @mikemcclintock:disqus


Gravatar
mikemcclintock on

Chris, seconding ccsalespro's comment, is there an episode where you kind of have a "stock" rails new app that has all of your favorite gems in them? I think I missed something here where you generated the admin stuff for Devise as well as a couple other things. Good work on these, I'm really enjoying it.


Gravatar
Justin Mullis (20 XP) on
Chris, loved this series as well as the other on getting started with VueJS, Rails, and Webpacker.  How would you go about setting up an app with more than one Vue component?  I'm looking to sort of merge a Rails API only application with its seperate VueJS frontend.  However the application is somewhat complicated and has a lot of VueJS components.  It seems strange to create a new Vue instance for each of them, linked to a particularly ided element on the DOM.  It seems like a single Vue component, with multiple children, would make more sense but then I don't know how to set that up and still allow Rails to handle the majority of view rendering.
Gravatar
Chris Oliver (169,620 XP) on
Hey Justin,

You can do multiple components on a page by creating multiple Vue apps. You'd basically just mount your different Vue apps to the appropriate tag, if you find them on the page. You won't necessarily have them on every page, so you'll need to conditionally check to set them up. 

This is what I do with the Vue-Turbolinks integration. You can see an example here that checks each turbolinks pageview for the components and then sets up the vue app. https://github.com/excid3/vue-turbolinks#running-vue-only-on-specific-pages

You'd just do the same thing basically. 

I also noticed that Laravel seems to have an interesting way of doing things that is using the whole server sided rendered HTML as the main component. It seems like it might be a really useful way of doing an even nicer middle ground.

Gravatar
Justin Mullis (20 XP) on
Thank you Chris, I'll go with that approach.  It felt a little strange to have a dozen different components loading in that way but it definitely works!

Also, feel stupid but I can't figure out how to reply to your comment :)
Gravatar
Chris Oliver (169,620 XP) on
I literally just rebuilt the comment system (replacing Disqus) so there are some bugs. 🤓

You can think of that approach as similar to binding your events in jQuery. I worked on a nice little wrapper for that which I need to release. And once I get a chance to check out this other approach, I'll let you know. Might be a better way to structure things. 

Gravatar
Larry Tu (20 XP) on
HI, 
I've tried to add webpacker in existing app, and error showed up as bellow. I've googled it for hours and still can't find solution, plz help me with some hint, Thank you so so much.

Webpacker::Manifest::MissingEntryError - Webpacker can't find application.css in <App-Path>/public/packs/manifest.json. Possible causes:

1. You want to set webpacker.yml value of compile to true for your environment
   unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:
{
  "application.js": "/packs/application-6d6fda76e45f26772dbc.js",
  "application.js.map": "/packs/application-6d6fda76e45f26772dbc.js.map",
  "hello_vue.js": "/packs/hello_vue-8b47c03e0e5b1afe8a54.js",
  "hello_vue.js.map": "/packs/hello_vue-8b47c03e0e5b1afe8a54.js.map"
}
Gravatar
Filip Jerga (40 XP) on
Hey Larry

Do you have still issue ? Its because webpack compiling no css... 
Check file in "app/javascript/packs/application.js" - initialy you have there just console log. So webpack will compile no css. 
Lets add this line "import App from '../app.vue'" and then run in terminal "bin/webpack" , aplication css should be generated 

Gravatar
Ferran Cabrer i Vilagut (110 XP) on
Larry, me too:
Changing javascript_pack_tag is providing error
Webpacker::Manifest::MissingEntryError
Gravatar
Filip Jerga (40 XP) on
Check my comment above, or watch video more furthure. Reason is becasue you have no css in your javascript files. You can remove pack tags and add them after you add some vue code. Just follow tutorial
Gravatar
Mark Hapke (20 XP) on

HI there, 

i had the problem too. Could fix it with:
yarn install

If you have get the message "can't find executable webpack for gem webpacker" along the way you can fix it with:
bundle exec rails webpacker:binstubs

Hints came from:

Gravatar
Erik Jenks (40 XP) on
I am running into some issues while following along here. I have a Rails 5.2 app running with Ruby 2.5.1. I can get through the installation of Vue, but when I try to render the component on the page nothing appears. No console warnings either. I tried to modify the rails content-security-policy like it was suggested in the webpacker documents, but nothing worked. 

Any thoughts?
Gravatar
Chris Oliver (169,620 XP) on
Not sure, but you might check the git repo. I updated it to Rails 5.1 and Ruby 2.5 recently. Rails 5.2 shouldn't cause any differences but the webpacker gem does change frequently.
Gravatar
Erik Jenks (40 XP) on
Thanks Chris. I cloned the project repo and was able to get it up and running. Originally I was attempting this walkthrough with your `jumpstart` template. I'll have to see if I can get this project working in that template. Thanks for your input and the quality work you produce!

Login or create an account to join the conversation.