0

Been struggling with this query for a few days. I have 3 models Books, Children and Hires. I have created a view for hires which allows a user to select 2 books and a single child and what i'm looking to do is insert two rows to reflect this into the 'hires' table. I have some JS that populates the hidden fields with the values that they require. Now, I don't think nested attributes is the way to go, because i'm trying to insert directly into the joining table.

So, what i'm trying now is the following:

hires/_form.html.erb

<%= form_for(@hire) do |f| %>    
 <% 2.times do %>
    <%= f.hidden_field :child_id %>
    <%= f.hidden_field :book_id %>
 <% end %>
<%= f.submit 'Take me home' %>
<% end %>

and then what I want to do is to run through the 'create' function twice in my controller and thus create two rows in the 'hires' model. Something like this:

hires_controller.rb

def create        
hire_params.each do |hire_params|
@hire = Hire.new(hire_params)
end
end 

Is this completely the wrong way to go? I'm looking for advice on the right way to do this? If this will work, what's the best way to format the create statement?

** Edit **

I have 3 models. Each Child can have 2 books. These are my associations:

class Child < ActiveRecord::Base
 has_many :hires
 has_many :books, through: :hires
end

class Hire < ActiveRecord::Base
 belongs_to :book
 belongs_to :child
 accepts_nested_attributes_for :book
 accepts_nested_attributes_for :child
end

class Book < ActiveRecord::Base
  has_many :hires
  has_many :children, through: :hires
  belongs_to :genres
end

hires/new.html.erb

<div class="form-inline">
  <div class="form-group">
<h1><label for="genre_genre_id">Pick Book 1:

  <%=collection_select(:genre1, :genre_id, @genres.all, :id, :Name, {prompt: true}, {:class => "form-control dropdown"})%></label></h1>
  </div>
  </div>

<div id="book1-carousel" class="owl-carousel owl-theme">
  <% @books.each do |book| %>
      <div data-id = "<%= book.id %>" class="tapbook1 tiles <% @genres.each do |g|%> <% if g.id == book.Genre_id %> book1genre<%=g.id %> <% end end%> <%=  %>"><a class="item link"><% if book.bookimg.exists? %><%= image_tag book.bookimg.url(:small), :class => "lazyOwl", :data => { :src => book.bookimg.url(:small)}%> <%end%></br><p class="tile_title" ><%= book.Title %></p></a></div>
  <% end %>
</div>



<div class="form-inline">
  <div class="form-group">
    <h1><label for="genre_genre_id">Pick Book 2:

      <%=collection_select(:genre2, :genre_id, @genres.all, :Name, :Name, {prompt: true}, {:class => "form-control dropdown"})%></label></h1>
  </div>
</div>

<div id="book2-carousel" class="owl-carousel owl-theme">
  <% @books.each do |book| %>
      <div data-id = "<%= book.id %>" id="<%= book.id %>" class="tapbook2 tiles <% @genres.each do |g|%> <% if g.id == book.Genre_id %> book2genre<%=g.id %> <% end end%> <%=  %>"><a class="item link"><% if book.bookimg.exists? %><%= image_tag book.bookimg.url(:small) , :class => "lazyOwl", :data => { :src => book.bookimg.url(:small)}%> <%end%></br> <p class="tile_title"><%= book.Title %></p></a></div>
  <% end %>
</div>

 <h1 class="child_heading1" >Now choose your name:</h1>

<div id="children-carousel" class="owl-carousel owl-theme">
  <% @children.each do |child| %>
      <div data-id = "<%= child.id %>" class="tapchild tiles"><a class="item link"><% if child.childimg.exists? %><%= image_tag child.childimg.url(:small), :class => "lazyOwl", :data => { :src => child.childimg.url(:small)} %> <%end%></br> <p class="tile_title"><%= child.nickname %></p></a></div>
  <% end %>
</div>




<%= render 'form' %>

and the coffeescript:

hires.coffee

$(document).on 'ready page:load', ->

  book1carousel = $("#book1-carousel")
  book2carousel = $('#book2-carousel')


  book1carousel.owlCarousel items: 5, lazyLoad : true
  book2carousel .owlCarousel items: 5, lazyLoad : true
  $('#children-carousel').owlCarousel items: 5, lazyLoad : true

  book1clickcounter = 0
  book2clickcounter = 0
  childclickcounter = 0

  book1selection = 0
  book2selection = 0



  $('.tapbook1').on 'click', (event) ->
    $this = $(this)
    book1id = $this.data('id')
    book1selection = book1id


    if $this.hasClass('bookclicked')
      $this.removeAttr('style').removeClass 'bookclicked'
      book1clickcounter = 0
      $('#hire_book_id').val("");
      book1selection = 0
    else if book1clickcounter == 1
      alert 'Choose one book from this row'
    else if book1selection == book2selection
      alert "You've already picked this book"
    else
      $('#hire_book_id').val(book1id);
      $this.css('border-color', 'blue').addClass 'bookclicked'
      book1clickcounter = 1

    return

  $('.tapbook2').on 'click', (event) ->
    $this = $(this)
    book2id = $this.data('id')
    book2selection = book2id

    if $this.hasClass('book2clicked')
      $this.removeAttr('style').removeClass 'book2clicked'
      book2clickcounter = 0
      book1selection = 0
    else if book2clickcounter == 1
      alert 'Choose one book from this row'
    else if book1selection == book2selection
      alert "You've already picked this book"
    else

      $this.css('border-color', 'blue').addClass 'book2clicked'
      book2clickcounter = 1

    return


  $('.tapchild').on 'click', (event) ->
   $this = $(this)
   childid = $this.data('id')
   if $this.hasClass('childclicked')
     $this.removeAttr('style').removeClass 'childclicked'
     childclickcounter = 0
     $('#hire_child_id').val("");
   else if childclickcounter == 1
     alert 'Choose one child from this row'
   else
     $this.css('border-color', 'blue').addClass 'childclicked'
     childclickcounter = 1
     $('#hire_child_id').val(childid);
   return

  jQuery ($) ->
  $('td[data-link]').click ->
    window.location = @dataset.link
    return
  return


return
Community
  • 1
  • 1
James Osborn
  • 187
  • 16

1 Answers1

0

My approach to this would be what's called a form object, a class that acts like a model but exists only to handle the creation of multiple objects. It provides granular control, but at the expense of duplicating validations. In my opinion (and that of many others), it's a much better option than nested attributes in most cases.

Here's an example. Note that I don't have any idea what your application does, and I didn't look at your associations close enough to make them accurate in this example. Hopefully you'll get the general idea:

class HireWithBookAndChild

  include ActiveModel::Model

  attr_accessor :child_1_id, :child_2_id, :book_id

  validates :child_1_id, presence: true
  validates :child_2_id, presence: true
  validates :book_id,  presence: true

  def save
    if valid?
      @hire = Hire.new(hire_params)
      @child_1 = @hire.child.create(id: params[:child_1_id])
      @child_2 = @hire.child.create(id: params[:child_2_id])
      @book = @hire.book.create(id: params[:book_id])
    end
  end
end

By including AR::Model, you get validations and an object you can create a form with. You can even go into your i18n file and configure the validation errors messages for this object. Like an ActiveRecord model, the save method is automatically wrapped in a transaction so you won't have orphaned objects if one of them fails to persist.

Your controller will look like this:

class HireWithBookAndChildController < ApplicationController

 def new
   @hire = HireWithBookAndChild.new
 end

 def create
   @hire = HireWithBookAndChild.new(form_params)
   if @hire.save
     flash['success'] = "Yay"
     redirect_to somewhere
   else
     render 'new'
   end
 end

 private

 def form_params
   params.require(:hire_with_book_and_child).permit(:child_1_id, :child_2_id, :book_id)
 end

end

Your form will look like this:

form_for @hire do |f|

  f.hidden_field :child_1_id
  ...

  f.submit
end

You'll notice right away that everything is flat, and you aren't having to mess with fields_for and nested nested parameters like this:

params[:child][:id]

You'll find that form objects make your code much easier to understand. If you have different combinations of children, books and hires that you need to create, just make a custom form object for each one.

Update

A solution that might be more simple in this case is to extract a service object:

class TwoHiresWithChildAndBook < Struct.new(:hire_params)

  def generate
    2.times do
      Hire.create!(hire_params)
    end
  end
end

And from your controller:

class HiresController

  def create
    generator = HireWitHChildAndBook.new(hire_params)
    if generator.generate
      *some behavior*
    else
      render :new
    end
  end
end

This encapulates the knowledge of how to create a hire in one place. There's more detail in my answer here: Rails 4 Create Associated Object on Save

Community
  • 1
  • 1
Brent Eicher
  • 1,050
  • 9
  • 14
  • Thanks for this @Brent. This may be a good way to refactor the form but my problem is not with nested_attributes but with creating two rows directly in the hires model. I don't need to create anything in 'books' or 'children'. These are created separately. I just need to post twice in 'hires'? – James Osborn Nov 14 '15 at 16:07
  • Yeah like I said, I knowingly didn't get your associations right and therefore this isn't a copy and paste solution. It's just a concept illustration. If what you need are two hires, create a form object that validates the child_id and book_id and creates two rows in the save method. Does that make sense? – Brent Eicher Nov 14 '15 at 16:16
  • Actually I'm going to update my answer with a second solution that would work in your case. – Brent Eicher Nov 14 '15 at 16:19