Giter VIP home page Giter VIP logo

forms-and-basic-associations-rails's Introduction

Forms And Basic Associations in Rails

Objectives

  1. Populate select options based on association options.
  2. Assign a foreign key based on an input box value directly through mass assignment (post[category_id]).
  3. Define a belongs_to association writer.
  4. Build a form field that will delegate to a belongs_to association writer (post#category_name=) through controller mass assignment.
  5. Define a has_many association writer.
  6. Build a form field that will delegate to a has_many association writer (category#post_ids=) through controller mass assignment.

The problem

Let's say we have a simple blogging system. Our models are Post and Category. A Post belongs_to a Category.

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :category
end

# app/models/category.rb
class Category < ActiveRecord::Base
  has_many :posts
end

Now we need to build the functionality for a user to create a Post. We're going to need a form for the Post's content, and some way to represent what Category the Post belongs to.

Using the category ID

As a first pass, we might build a form like this:

<%= form_for @post do |f| %>
  <%= f.label :category_id, :category %><%= f.text_field :category_id %>
  <%= f.text_field :content %>
  <%= f.submit %>
<% end %>

This will work if we wire up our PostsController with the right parameters:

class PostsController < ApplicationController
  def create
    Post.create(post_params)
  end

  private

  def post_params
    params.require(:post).permit(:category_id, :content)
  end
end

But as a user experience, this is miserable. I have to know the ID of the category I want to use. As a user, it is very unlikely that I know this or want to.

We could rewrite our controller to accept a category_name instead of an ID:

class PostsController < ApplicationController
  def create
    category = Category.find_or_create_by(name: params[:post][:category_name])
    Post.create(content: params[:post][:content], category: category)
  end
end

But we'll have to do this anywhere we want to set the category for a Post. When we're setting a Post's categories, the one thing we know we have is a Post object. What if we could move this logic to the model?

Specifically, what if we gave the Post model a category_name attribute?

Defining a custom setter and getter (convenience attributes on models)

Since our Active Record models are still just Ruby classes, we can define our own setter and getter methods:

# app/models/post.rb
class Post < ActiveRecord::Base
   def category_name=(name)
     self.category = Category.find_or_create_by(name: name)
   end

   def category_name
      self.category ? self.category.name : nil
   end
end

The setter method #category_name= is called whenever a Post is initialized with a category_name field. We can expand Post.create(post_params) to

Post.create({
  category_name: params[:post][:category_name],
  content: params[:post][:content]
})

so that you can see that #category_name= will indeed be called. Since we have defined this setter ourselves, Post.create does not try to fall back to setting category_name through Active Record. You can think of #category_name= as intercepting the call to the database and instead shadowing the attribute category_name by, one, making sure the Category exists; and, two, providing it in-memory for the Post model. We sometimes call these in-memory attributes "virtuals".

Now we can set category_name on a post. We can do it when creating a post too, so our controller becomes quite simple again:

class PostsController < ApplicationController
  def create
    Post.create(post_params)
  end

  private

  def post_params
    params.require(:post).permit(:category_name, :content)
  end
end

Notice the difference โ€“โ€“ we're now accepting a category name, rather than a category ID. Even though there's no Active Record field for category_name, the category_name key in the post_params hash prompts a call to the category_name= method.

We can change the view as well now:

<%= form_for @post do |f| %>
  <%= f.label :category_name %>
  <%= f.text_field :category_name %>
  <%= f.text_field :content %>
  <%= f.submit %>
<% end %>

Now the user can enter a category by name (instead of needing to look up its ID), and we handle finding or creating the Category in the black box of the server. This results in a much friendlier experience for the user.

Selecting from existing categories

If we want to let the user pick from existing categories, we can use a Collection Select helper to render a <select> tag:

<%= form_for @post do |f| %>
  <%= f.collection_select :category_name, Category.all, :name, :name %>
  <%= f.text_field :content %>
  <%= f.submit %>
<% end %>

This will create a drop down selection input where the user can pick a category.

However, we've lost the ability for users to create their own categories.

That might be what you want. For example, the content management system for a magazine would probably want to enforce that the category of an article is one of the sections actually printed in the magazine.

In our case, however, we want to give users the flexibility to create a new category or pick an existing one. What we want is autocompletion, which we can get with a datalist:

<%= form_for @post do |f| %>
  <%= f.text_field :category_name, list: "categories_autocomplete" %>
  <datalist id="categories_autocomplete">
    <% Category.all.each do |category| %>
      <option value="<%= category.name %>">
    <% end %>
  </datalist>
  <textarea name="post[content]"></textarea>
  <%= f.submit %>
<% end %>

datalist is a new element in the HTML5 spec that allows for easy autocomplete. Check below in Resources for further reading.

Updating multiple rows

Let's think about the reverse association. Categories have many posts.

# app/models/category.rb
class Category < ActiveRecord::Base
  has_many :posts
end

Given a category, how do we let a user specify many different posts to categorize? We can't do it with just one <select> because we can have many posts in that category.

Using array parameters

Rails uses a naming convention to let you submit an array of values to a controller.

If you put this in a view, it looks like this.

<%= form_for @category do |f| %>
  <input name="category[post_ids][]">
  <input name="category[post_ids][]">
  <input name="category[post_ids][]">
  <input type="submit" value="Submit">
<% end %>

When the form is submitted, your controller will have access to a post_ids param, which will be an array of strings.

We can write a setter method for this, just like we did for category_name:

# app/models/category.rb
class Category < ActiveRecord::Base
   def post_ids=(ids)
     ids.each do |id|
       post = Post.find(id)
       self.posts << post
     end
   end
end

If we're certain that the post ids being submitted in the form all belong to existing posts, we don't even need this setter method! The following code is valid and will automatically assign the new category id to each post:

# As long as posts 5, 2, 3 exist, this will work! The category_id for each of these
# posts will be set to the new category's id
Category.create(name: 'This and That', post_ids: %w[5 2 3])

Now we can use the same wiring in the controller to set post_ids from params:

# app/controllers/categories_controller.rb
class CategoriesController < ApplicationController
  def create
    Category.create(category_params)
  end

  private

  def category_params
    params.require(:category).permit(:name, post_ids: [])
  end
end

Resources

forms-and-basic-associations-rails's People

Contributors

annjohn avatar aviflombaum avatar bhollan avatar brennenawana avatar brunoboehm avatar febbraiod avatar gj avatar hellorupa avatar ihollander avatar jeffpwang avatar kayjsmyth avatar lkwlala avatar maxwellbenton avatar pletcher avatar queerviolet avatar sophiedebenedetto avatar victhevenot avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

forms-and-basic-associations-rails's Issues

More HTML showing at bottom

Seems to be recurring. In this case, it's in the resources section; in others it's "view this lab in GitHub."

strong param arrays

Hi there,

Sorry for the bother. This lesson provides an example for making an array (post_ids) a strong parameter via the category_params method. This example may be incorrect. According to the rails strong parameter documentation on github (https://github.com/rails/strong_parameters), arrays must be explicitly called out in a permit method.

In other words, students may run into problems trying to emulate

def category_params
    params.require(:category).permit(:name, :post_ids)
  end
end

when the following is closer to what the documentation says:

 def category_params
    params.require(:category).permit(:name, {:post_ids=> []})
  end
end

(I mention it because I had problems with this in the next lab!)

Thanks for all your hard work,
C

unclear input / output

in the rails c portion when creating a new_person its unclear that there is break from the input and the return from the console. a break there would help with the confusion...

2.2.3 :019 > new_person.addresses_attributes={"0"=>{"street_address_1"=>"33 West 26", "street_address_2"=>"Floor 2", "city"=>"NYC", "state"=>"NY", "zipcode"=>"10004", "address_type"=>"work1"}, "1"=>{"street_address_1"=>"11 Broadway", "street_address_2"=>"Suite 260", "city"=>"NYC", "state"=>"NY", "zipcode"=>"10004", "address_type"=>"work2"}}

=> {"0"=>{"street_address_1"=>"33 West 26", "street_address_2"=>"Floor 2", "city"=>"NYC", "state"=>"NY", "zipcode"=>"10004", "address_type"=>"work1"}, "1"=>{"street_address_1"=>"11 Broadway", "street_address_2"=>"Suite 260", "city"=>"NYC", "state"=>"NY", "zipcode"=>"10004", "address_type"=>"work2"}}

Very confusing

It introduces a lot of new concepts and knowledge points without explaining them clearly on what it is, why it is used or written in this way, when to use it or just whether it's important for us to understand it or just nice to know. I ended up having to ask the technical coaches, and google a lot of the new stuff showing up here. It's not an efficient or productive learning experience but rather pretty frustrating.

params problem

the params snippit
params.require(:category).permit(:name, post_ids: [])
gave many problems and i had to change this to
params.require(:category).permit(:name, post_ids: => [])

Code snippet 9 results in an error if you follow the tutorial exactly; New posts results in error if unchanged

Salutations!

While following the Forms and Basic Association reading, I found a few errors in the code that's never addressed in the tutorial.

Error 1: New posts results in error if unchanged

If the code snippet from the tutorial here is left unchanged:

 def category_name
      self.category.name
 end

And you follow the tutorial to the first category selection option:

<%= f.label :category_name %>
<%= f.text_field :category_name %>
<%= f.text_field :content %>

You will get the error:

image

This happens because new post objects don't have categories and the category overloader doesn't check if a category is defined. New posts will find nothing and the error will crop up. Because the tutorial is for creating a new post object, there's no way to avoid this without knowing it's there. There's probably a better way to fix this, but I just told it to return an empty string if no category is defined. That fixed the issue.

My fix

def category_name
   !self.category.nil? ? self.category.name  : ""
end

Error 2: The 9th code snippet results in an error if you follow the tutorial exactly

When you get to this code snippet:

<%= form_for @post do |f| %>
  <%= f.collection_select :category, Category.all, :id, :name %>
  <%= f.text_field :content %>
<% end %>

You will get a nil error when the site is redirected to the post's show page after creating a new post or editing it since a category was never defined.

image

This happens because the form submits a :category object instead of a :category_name object.
Looking at the HTTP Request in the logs shows this:

image

This happens because we changed the permit earlier to:

params.require(:post).permit(:category_name, :content)

It's doesn't find :category_name and just ignores :category so the category is never changed.

My Fix

I fixed it by changing :category to :category_name.
The final code looked like this:

<%= form_for @post do |f| %>
  <%= f.collection_select :category_name, Category.all, :id, :name %>
  <%= f.text_field :content %>
<% end %>

Looking at the HTTP_Request, the data is correct:
image

Personally, I changed it a bit more so the category name is passed to the ruby code instead of the id.
The change is the third parameter is now :name instead of :id.

<%= form_for @post do |f| %>
  <%= f.collection_select :category_name, Category.all, :name, :name %>
  <%= f.text_field :content %>
<% end %>

image

I created a git that contains the errors (it's commented out) with the fixed code.

I used the displaying-associations-rails-v-000 lab code to test all of this and follow along with the reading; that's how I found the errors. I changed the :content to :description since that's the variable name in the project I'm using.

I accidentally overwrote my lab :(

Anyway, the fixes are shown here:
https://github.com/Naomi-Dennis/displaying-associations-rails-v-000

possible error. Please respond so I can be sure.

<%= form_for @category do |f| %>
  <input name="post_ids[]">
  <input name="post_ids[]">
  <input name="post_ids[]">
<% end %>

should be

<%= form_for @category do |f| %>
  <input name="category[post_ids][]">
 <input name="category[post_ids][]">
<input name="category[post_ids][]">
<% end %>

Description for datalist is not clear

Screen Shot 2019-08-08 at 2 15 24 PM
The description gives the impression that you will be able to create a new category or pick an existing one with datalist, however datalist is just iterating through existing categories and making them searchable. The textarea field is where you would create the new category object, however instead of showing how a new category object is created (as mentioned in the description, it's being used to add new content to the post. I found this to be very unclear.

Feedback from student

  • why is the example building a form_for with HTML like ?
  • what does "You can define your own attributes on models" mean? I know we are using a setter, but why?
  • what does this mean:
    Even though you don't have an ActiveRecord field for category_name, because there is a key in the post_params hash for category_name it still calls the category_name= method. Oh hey! we created our own category_name= method! So convenient.
    • and why is the view after that using ? Is there a specific reason we aren't using f.label, f.text_field, etc?
    • it doesn't explain about collection_select (I'm guessing because it was already mentioned in that Forms Overview lesson?).
    • it doesn't explain how a datalist works. It just gives a link and a link to a codepen. How do you even use a codepen? This is one of those times where I just feel like I can look up information on my own for free, but I'm paying to receive a comprehensive lesson. Links can be great when they are additional sources, but not THE source. Maybe datalists aren't super important, or maybe they are; I wish there would have been more insight into how they work, especially since the example is different than the one in the link.

The readme lesson differs from the associated lab regarding submitting an array of values to a controller

The lesson describes under "Using array parameters" manually creating input fields (such as whereas the lab that follows it mentions the more recent "fields_for" form helper.

Ideally, the forms-and-basic-associations-rails readme lesson would explicitly demonstrate this rather than rely on adding it only under References in the lab (especially since the solution file uses the "fields_for" coding. It's a great feature that might otherwise be overlooked.

Not sure where to put my feedback on this entire module, but if anyone cares, hi.

This whole module needs videos to accompany it; almost every person I've talked to has told me they had issues with this module, and it's probably because a lot of people are visual learners. It's also very hard to connect the dots or see the bigger picture with an online platform and only text that disappears when you move to the next lesson. It's different when I read through a book, where I can flip back through pages, etc. Something needs to change I believe.

Please improve this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.