- Read Tutorial
- Watch Guide Video
Since this is going to be a fairly complex topic, I'm going to break the next few guides into smaller pieces than usual. I really want to focus on exactly what we are doing, and I think if we take this an iterative step at a time, that will make the concepts easier to understand. This is a process that you'll do fairly regularly, however, it's not been quite as intuitive to me or to a lot of the students I've taught over the years. Accordingly, I want to take it one small portion at a time, so it will make sense to you, as you'll be implementing this feature quite a bit.
Making a Plan
Go ahead and open your portfolio.rb
file.
class Portfolio < ApplicationRecord has_many :technologies include Placeholder validates_presence_of :title, :body, :main_image, :thumb_image
Right now we have the concept of a Portfolio
, and this portfolio has many :technologies
associated with it. With that in place, what we want to do now is create the ability to actually define those technologies in the portfolio form. In other words, we want the ability to list each technology by adding things like "Ruby on Rails", "Angular", “Swift”, or whatever technology we want to have associated with a particular portfolio item. And we want the ability to do that here in our model as well as in the form.
If you look in the seeds file, I originally planned on doing this in our subtitle, but I have changed my mind. This process of changing plans happens quite a bit. The reason why I am redirecting now is partly because I had that one idea, that was not a planned one: at the last moment, I decided to have the scope
action, where I have the following method in portfolio.rb
def self.angular where(subtitle: 'Angular') end
Connected to this method in the portfolio_controller:
def angular @angular_portfolio_items = Portfolio.angular end
Though this is fine, it is also fragile because it is essentially forcing your portfolio to have very strict kinds of strings
. Let's say you make a typo when inputting "Angular", then this method will throw a bug and your portfolio
item wouldn't be caught inside this query.
I wanted to use that process to show data flow, but then I really liked the concept of this as a feature. However, we can turn it into something more functional. Instead of using our subtitle
, as a way to highlight the items, a better way would be to use our technologies
. This way, our technologies
can have some effect on the scoping and what's shown on the page. This is my long term plan and why I’ve changed my mind on it.
So, in order to understand how to build this out on the form page itself, I think it will help to get a visual. Start the rails server, and go to localhost:3000/portfolios. Click on the Create New Item button
On this page, I want to have the ability to add multiple technologies. Let’s imagine we have an additional form field called technologies
where I can type in the technology I want. For example if I type in "Rails" and then type in "Angular", the system will actually go and create two separate technology records in the database automatically. Even though it's one form, we can have it communicating with multiple models, which is what this type of functionality allows for.
In this guide, I'm going to focus on building out the backend functionality. Then in the next guide, we'll implement the front end to get it all working.
Creating Nested Attributes
We are going to begin in our portfolio.rb file. To start with, we're going to add something called accepts_nested_attributes_for :technologies
. This is the base implementation, and will actually get the process working.
class Portfolio < ApplicationRecord has_many :technologies accepts_nested_attributes_for :technologies include Placeholder validates_presence_of :title, :body, :main_image, :thumb_image
We also have to keep in mind that there are some data validations that we want to have in place. For example, imagine a situation where you have fields for these technology items, but they are empty. We want to make sure that when you click Save, the system doesn't go and save them as empty technology items.
I am going to show you one way to do it is, which is what the founder of Rails recommends you use with these nested attribute situations. To begin, I will place the code all on one line and then I will show you a better way to present the code. We begin with reject_if:
followed by lambda
- accepts_nested_attributes_for :technologies + accepts_nested_attributes_for :technologies, reject_if: lambda
Don't let this code scare you! Lambda
is the same as ->
, but we're using the word here. Lambda indicates that we want to encapsulate a process. In this case we will encapsulate it and pass it to the reject_if
method.
We use curly braces { }
to indicate a block which is what we are encapsulating. Inside the block we have a block variable |attrs|
, attrs is short for attributes. We could call the variable anything we wanted, such as |x|
because it is just a block variable . Next, with attrs[‘name’]
we're selecting the attribute item. In this section, if you had multiple items, then you would need to list each one, but we only need to select the name (If you open the seeds file, you'll see that there is only one attribute for technology
and that's name
).
Last, we use .blank?
So, using the blank method at the end, this code is essentially indicating do not accept this field if the attribute *name is blank*. Technically, we’re asking Rails to reject it and not let it pass thru.
- accepts_nested_attributes_for :technologies, reject_if: lambda + accepts_nested_attributes_for :technologies, reject_if: lambda {|attrs| attrs['name'].blank?}
As I mentioned, the code would work fine with just using the first section detailing accepts_nested_attributes :technologies
however, it is a best practice to include validations. Another best practice is to to separate long lines of code in multiple lines like this:
class Portfolio < ApplicationRecord has_many :technologies accepts_nested_attributes_for :technologies, reject_if: lambda {|attrs| attrs['name'].blank?} include Placeholder validates_presence_of :title, :body, :main_image, :thumb_image
This formatting is also easier to read, and works better for the ebook I am writing.
So, all of this code is going to help us create a single portfolio
item and slide in multiple technologies.
One Step at a Time
Before we end this guide let’s make sure that what we have just written is working. As you may have guessed by now, to test this, we need to open the rails console.
I really want to stress the importance of following this kind of pattern, where you pause at each step of the process and verify that what you have put in place is working. Consider: At this point we could have just moved on and tried to implement the full functionality that we want. For example, I could have surmised that I had set up the code correctly and just moved on to the controller to add the items we need there, and then updated the form in our view file. That could have worked too. However, I really want to stress the importance of taking small steps whenever you're developing an application, especially if it's a feature you've never done before. The smaller the increment you can make the development, the more successful you'll be.
For example, if I'm doing something that I'm unsure of in Rails, especially if it's in the model, then the first thing I will do is go and test it in the rails console. Remember, anything you create and anything you do in the application can be done in the rails console as well. The good part is if you create small pieces, you'll be able to test it more successfully in the console than in the browser.
Testing our Code
With that in mind, let's go to the console and create a portfolio item. Initially, we'll pass the regular values and we know our portfolio needs a title, subtitle, and body, because those are validated attributes.
Portfolio.create!(title: “Web app”, subtitle: “asdfasd”, body: “asdfasd”)
So the next piece is where the process is going to look a little bit different. You don’t really have to worry too much about the syntax here for the Rails console, this is just what we need to feed in to create the item here. When we get the data from the form, it’s going to be handled a little bit differently. However, this is a really important step in the process as it's going to show you how the data needs be formatted. Rails does a lot of it for you in the background, but right here, we are going to have to manually implement it, which is a good thing.
So, the next value we're going to pass is technologies_attributes
. Keep in mind: This has to be very specific, and cannot be a random variable. A good way to remember what you need to use is to reference your model file. By default, the first word is the item you have listed as a nested attribute, which in this case is technologies
, followed by an underscore and the word _attributes
. This code tells Rails that the next item we are sending in is a number (ie: list) of technology attributes:
- Portfolio.create!(title: “Web app”, subtitle: “asdfasd”, body: “asdfasd”) + Portfolio.create!(title: “Web app”, subtitle: “asdfasd”, body: “asdfasd”, technologies_attributes)
The way we know it's going to be a number of them is because it's a nested
set, which is going to allow for a number. You could pass in a single one if you like, and that's perfectly fine. In fact, that may even be the case many times. But, for this case at least, Rails has to be prepared to accept multiple items.
Now, the values that we pass to technologies_attributes
will be an array
. This is very important as well. If you do not pass in an array you will get an error message.
We are going to pass in a few array items, and all the items we pass in have to be hashes
. Because this is so important, I want to spend a little bit of time looking into it, as it may feel like an odd way to set things up. Remember, in Ruby, a hash is a key-value pair. Looking at our create command, we can see title and "Web app" is a hash as title
is the key
and "Web app"
is the value
. Likewise, subtitle and “asdfasd” are a key and a value, and so forth. In fact, technologies_attributes
is a key
. Notice: it is on the exact same line as all of the other attributes. However, since we have to pass in nested items as the value
, we are doing that inside of the array
with hashes
.
In other words, we have to pass in a collection. With this we are telling Rails that there is potential to have multiple items. Inside this collection, we need to have our own set of key-value pairs.
Keep in mind, right now the only nested attribute is technologies. If you open the schema file, you'll see that we have only a name
attribute for technologies. But what if we had name and a description? Then we would need the ability to put something like
, technologies_attributes: [{name: ‘something’, description ‘asdfas’}, {}, {}]
and we would need to put key value pairs for each of them. That is why it's important to tell Rails that we're not just passing an array, but we are passing in a set of key-value pairs inside of it.
This concept is very important to understand! So, with that in mind, let's create our values. Each hash will have a name as the key and then an associated value:
- Portfolio.create!(title: “Web app”, subtitle: “asdfasd”, body: “asdfasd”, technologies_attributes) + Portfolio.create!(title: “Web app”, subtitle: “asdfasd”, body: “asdfasd”, technologies_attributes: [{name: ‘Ruby’}, {name: ‘Rails’}, {name: ‘Angular’}, {name: ‘Ionic’}])
If we set this up correctly, we should be able to create a portfolio
item with a title, subtitle, body and four technologies associated with it.
It looks like everything worked. We can actually analyze the SQL code to see if it did. The first line INSERT INTO “portfolios” shows us that it created and updated a portfolio for us with our title, subtitle, body and the defaults for our main image and thumb image. Next, is where it gets interesting. Not only do we have one insert statement, we actually have four insert
statements that tell us that the four technologies we indicated were created. And each of them have a portfolio_id
of 16
, so they're associated with the portfolio
that was just created. So this worked perfectly!
Next you can run Portfolio.last.technologies.count
and this returns value of four => 4
, which is correct.
Great job. Now you know how to implement nested attributes from a data perspective, so you can save multiple items, in fact, a whole collection of items associated with a parent model.
Let's add it all to our GitHub branch.
git add .
git commit -m ‘Integrated Nested attributes for portfolio model’
git push origin data-feature
With this in place, we're now ready to wire up our view forms to take in multiple items.