Giter VIP home page Giter VIP logo

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

Forms And Basic Associations 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

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

Watchers

James Cloos avatar

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.