Coder Social home page Coder Social logo

basic-nested-forms's Introduction

Nested Forms

Objectives

  1. Construct a nested params hash with data about the primary object and a belongs_to and has_many association.
  2. Use the conventional key names for associated data (association_attributes).
  3. Name form inputs correctly to create a nested params hash with belongs_to and has_many associated data.
  4. Define a conventional association writer for the primary model to properly instantiate associations based on the nested params association data.
  5. Define a custom association writer for the primary model to properly instantiate associations with custom logic (like unique by name) on the nested params association data.
  6. Use fields_for to generate the association fields.

Data model

Let's say we're writing an address book.

Each Person can have multiple addresses. Each Address has a bunch of address info fields.

Our data model looks like this:

  • Person
    • has many addresses
    • has a name (string)
  • Address
    • has one person
    • has the first line of the street address stored as street_address_1 (string)
    • has the second line of the street address stored as street_address_2 (string)
    • has a city (string)
    • has a state (string)
    • has a zipcode (string)
    • has an address_type (string)

Creating people

How do we write our Person form? We don't want to require our user to first create an Address, then create that Person. That's annoying. We want a single form for a Person containing several slots for their addresses.

Previously, we wrote setters like Song#artist_name= to find or create an Artist and connect them to the song.

That won't work here, because an address contains more than one field. In the Artist case we were just doing the name. With Address, it's "structured data". All that really means is it has multiple fields attached to it. When we build a form for it, the form will send a different key for each field in each address. This can get a bit unwieldy so we generally try to group a hash within the params hash, which makes things much neater. Spoiler alert: Rails has a way to send this across as a hash.

The complete params object for creating a Person will look like the following. Using "0" and "1" as keys can seem a bit odd, but it makes everything else work moving forward. This hash is now more versatile. You can access nested values the standard way, with params[:person][:addresses_attributes]["0"] returning all of the information about the first address at 33 West 26th St.

{
  :person => {
    :name => "Avi",
    :addresses_attributes => {
      "0" => {
        :street_address_1 => "33 West 26th St",
        :street_address_2 => "Apt 2B",
        :city => "New York",
        :state => "NY",
        :zipcode => "10010",
        :address_type => "Work"
      },
      "1" => {
        :street_address_1 => "11 Broadway",
        :street_address_2 => "2nd Floor",
        :city => "New York",
        :state => "NY",
        :zipcode => "10004",
        :address_type => "Home"
      }
    }
  }
}

Notice the addresses_attributes key. That key is similar to the artist_name key we used previously. Last time, we handled this by writing a artist_name= method. In this case, we're going to do something super similar. Instead of writing our own addresses_attributes= method, we'll let Rails take care of it for us. We're going to use accepts_nested_attributes_for and the fields_for FormHelper.

Last time, we first wrote our setter method in the model. This time let's modify our Person model to include an accepts_nested_attributes_for :addresses line.

class Person < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses

end

Now open up rails c and run our addresses_attributes method that was created for us by accepts_nested_attributes_for.

2.2.3 :018 > new_person = Person.new
 => #<Person id: nil, name: nil, created_at: nil, updated_at: nil>

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"}}

2.2.3 :020 > new_person.save
   (0.2ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "people" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2016-01-14 11:57:00.393038"], ["updated_at", "2016-01-14 11:57:00.393038"]]
  SQL (0.3ms)  INSERT INTO "addresses" ("street_address_1", "street_address_2", "city", "state", "zipcode", "address_type", "person_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)  [["street_address_1", "33 West 26"], ["street_address_2", "Floor 2"], ["city", "NYC"], ["state", "NY"], ["zipcode", "10004"], ["address_type", "work1"], ["person_id", 3], ["created_at", "2016-01-14 11:57:00.403152"], ["updated_at", "2016-01-14 11:57:00.403152"]]
  SQL (0.1ms)  INSERT INTO "addresses" ("street_address_1", "street_address_2", "city", "state", "zipcode", "address_type", "person_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)  [["street_address_1", "11 Broadway"], ["street_address_2", "Suite 260"], ["city", "NYC"], ["state", "NY"], ["zipcode", "10004"], ["address_type", "work2"], ["person_id", 3], ["created_at", "2016-01-14 11:57:00.405973"], ["updated_at", "2016-01-14 11:57:00.405973"]]
   (0.6ms)  commit transaction
 => true

This is a bit hard to read, but you'll notice that we have a method called addresses_attributes=. You didn't write that; accepts_nested_attributes_for wrote that. Then when we called new_person.save it created both the Person object and the two Address objects. Boom!

Now, we just need to get our form to create a params hash like that. Easy Peasy. We are going to use fields_for to make this happen.

# app/views/people/new.html.erb

<%= form_for @person do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %><br>

  <%= f.fields_for :addresses do |addr| %>
    <%= addr.label :street_address_1 %>
    <%= addr.text_field :street_address_1 %><br>

    <%= addr.label :street_address_2 %>
    <%= addr.text_field :street_address_2 %><br>

    <%= addr.label :city %>
    <%= addr.text_field :city %><br>

    <%= addr.label :state %>
    <%= addr.text_field :state %><br>

    <%= addr.label :zipcode %>
    <%= addr.text_field :zipcode %><br>

    <%= addr.label :address_type %>
    <%= addr.text_field :address_type %><br>
  <% end %>

  <%= f.submit %>
<% end %>

The fields_for line gives something nice and English-y. In that block are the fields for the addresses. Love Rails.

Load up the page, and see the majestic beauty of what you and Rails have written together. What?! Nothing is there.

Creating stubs

We're asking Rails to generate fields_for each of the Person's addresses. However, when we first create a Person, they have no addresses. Just like f.text_field :name will have nothing in the text field if there is no name, f.fields_for :addresses will have no address fields if there are no addresses.

We'll take the most straightforward way out: when we create a Person in the PeopleController, we'll add two empty addresses to fill out. The final controller looks like this:

class PeopleController < ApplicationController
  def new
    @person = Person.new
    @person.addresses.build(address_type: 'work')
    @person.addresses.build(address_type: 'home')
  end

  def create
    person = Person.create(person_params)
    redirect_to people_path
  end

  def index
    @people = Person.all
  end

  private

  def person_params
    params.require(:person).permit(:name)
  end
end

Now, refresh the page, and you'll see two lovely address forms. Try to hit submit, and it isn't going to work. One last hurdle. We have new params keys, which means we need to modify our person_params method to accept them. Your person_params method should now look like this:

def person_params
  params.require(:person).permit(
    :name,
    addresses_attributes: [
      :street_address_1,
      :street_address_2,
      :city,
      :state,
      :zipcode,
      :address_type
    ]
  )
end

Avoiding duplicates

One situation we can't use accepts_nested_attributes_for is when we want to avoid duplicates of the row we're creating.

In our address book app, perhaps it's reasonable to have duplicate address rows. For instance, both Jerry and Tim live on 22 Elm Street, so there are two address rows for 22 Elm Street. That's fine for those purposes.

But say we have a database of songs and artists. We would want Artist rows to be unique, so that Artist.find_by(name: 'Tori Amos').songs returns what we'd expect. If we want to be able to create artists while creating songs, we'll need to use find_or_create_by in our artist_attributes= method:

# app/models/song.rb

class Song < ActiveRecord::Base
  def artist_attributes=(artist)
    self.artist = Artist.find_or_create_by(name: artist[:name])
    self.artist.update(artist)
  end
end

This looks up existing artists by name. If no matching artist is found, one is created. Then we update the artist's attributes with the ones we were given. We could also choose to do something else if we didn't want to allow bulk assigning of an artist's information through a song.

Note that accepts_nested_attributes_for and setter methods (e.g., artist_attributes=) aren't necessarily mutually exclusive. It's important to evaluate the needs of your specific use case and choose the approach that makes the most sense. Keep in mind, too, that setter methods are useful for more than just avoiding duplicates โ€“โ€“ that's just one domain where they come in handy.

Video Review

basic-nested-forms's People

Contributors

alemosie avatar annjohn avatar aviflombaum avatar blake41 avatar bperl avatar danielseehausen avatar drakeltheryuujin avatar franknowinski avatar gj avatar ihollander avatar jeffpwang avatar jmburges avatar kaileeagray avatar lawrend avatar lkwlala avatar maxwellbenton avatar meryldakin avatar pletcher avatar queerviolet avatar realandrewcohn avatar

Stargazers

 avatar

Watchers

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

basic-nested-forms's Issues

Local Test

They're no specs in the test for a local test to pass.

"Traceback" error in "rails console"

Hi there, the rails console in this lab also isn't working properly based on the information provided in the lesson. Stumbled upon this error after entering these two Terminal Commands: new_person = Person.new followed by: new_person.save

Error:
Screen Shot 2020-11-11 at 12 54 58 PM

According to the lesson, the output in the terminal from the rails console should be:
Screen Shot 2020-11-11 at 12 56 40 PM

Hope this helps!!

Thanks, again

Outdated Gems

This is my Gemfile (what I had to change to get bundle install to run.)

source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2', '>= 5.2.3'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0', '>= 5.0.7'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 5.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails', '~> 4.3', '>= 4.3.3'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'
  gem 'pry'
end

group :development do
  # Access an IRB console on exception pages or by using <%= console %> in views

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  # gem 'spring'
end

Lab using minitest and not rspec?

I'm not sure if this is intentional, but this lab seems to be using Minitest instead of RSpec. I couldn't get learn to run tests locally without adding my own specs.

Change in the way the material is presented

I noticed since a couple of lessons ago, the way the material is being presented is very different than how the curriculum has been teaching the concepts until now. I feel these last lessons have been a bit scarce, lacking in explanation and elucidation of the concepts. I understand we are now at a more advanced point in the curriculum, but new concepts are still being introduced. As such, I strongly recommend "enriching" these last section of the curriculum, spending just a few more lines explaining new concepts and HOW the methods work.

Just my humble opinion. Trying to be helpful.

This is a readme not a lab

This exercise is structured as a lab when it should be a readme. I can't get learn to mark it as complete because there are no tests to pass.

Wrong example code

Hi, I think there is an error in this lab's example code in the "Avoiding Duplicates" section. This is the example provided:

class Song < ActiveRecord::Base
  def artist_attributes=(artist)
    self.artist = Artist.find_or_create_by(name: artist.name)
    self.artist.update(artist)
  end
end

I noticed is that you are shadowing an instance variable's name artist with the local variable artist which can be confusing, and I think led to an error in this case. In the line self.artist = Artist.find_or_create_by(name: artist.name) self.artist on the left-hand side of the assignment is the instance variable @artist (which is an instance of the Artist class or nil) but the artist.name in the argument references the local variable (which will have "preference" in this scope). As such, artist (without a prefix of @ or self-dot) refers to the attribute hash passed in, so artist.name will cause a NoMethodError. To make this code work, I think it needs to be Artist.find_or_create_by(name: artist[:name]). You do want to reference the local variable here, but the artist local variable is a hash object, not an Artist instance, so it has no method name - to get the name information of the local variable you need to use artist[:name].

It seems to me like it would be less easy to fall into making the mistake between the two if you renamed the local variable something like artist_info or artist_hash, and then changed the third line down to
self.artist = Artist.find_or_create_by(name: artist_info[:name])
or something like that.

Thanks!

Can't use Rails Server or Console

I'm getting this message when I try to view the console and server.
/.rvm/gems/ruby-2.6.1/gems/activerecord-5.2.3/lib/active_record/dynamic_matchers.rb:22:in method_missing': undefined method raise_in_transactional_callbacks=' for ActiveRecord::Base:Class (NoMethodError)
Not sure what's going on there.

Wrong syntax in code along

Copy and pasting from a previous issue that was closed but not revised!

This lesson instructs you to use "f.fields_for", but the fields only appear when the syntax is entered as "fields_for", without the "f."

Lab won't complete

Lab will not mark as complete.

When I started the lab, there was a repo to fork, which I did. Now lab has correctly converted to a readme. I have two green lights, but lab does not register as complete.

Wrong syntax in code along

This lesson instructs you to use "f.fields_for", but the fields only appear when the syntax is entered as "fields_for", without the "f."

Passed Local Tests: no tests

I built a test for this lab (which was great fun!), but I'm not sure if we are supposed to do that. If so, could add a line to the lab saying, "Please build test(s) to check the validity of this form, using Capybara." Otherwise, there is no way to progress to the next lesson.

Bundler error

I ran into this error while trying to get started on this lab.

Bundler could not find compatible versions for gem "bundler":
  In Gemfile:
    rails (~> 4.2) was resolved to 4.2.11.1, which depends on
      bundler (>= 1.3.0, < 2.0)

  Current Bundler version:
    bundler (2.0.1)
This Gemfile requires a different version of Bundler.
Perhaps you need to update Bundler by running `gem install bundler`?

Could not find gem 'bundler (>= 1.3.0, < 2.0)', which is required by gem 'rails
(~> 4.2)', in any of the sources.

Feedback from Students

In the Rails Basic Nested Forms reading, is the "Avoiding Duplicates" section saying that when we want to avoid duplicates of the row we're creating, that we don't use accepts_nested_attributes_for ? And in those situations we just create a setter method such in as the example below that with songs and artists?

**people and addresses: you can have multiple people with the same address, so in the Person class, accepts_nested_attributes_for :addresses is ok.

**songs and artists: you can't have duplicates of the artists, because you want to be able to return all of the songs associated with that artist and if the artist is listed more than once, it may not be accurate. So in the Song class, there is no accepts_nested_attributes_for, but instead an artist_attributes= method.

So how come in the Rails Has Many Through in Forms lab, our Post class can still pass the tests with an accepts_nested_attributes_for :categories plus a categories_attributes= method?

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.