Nested ajax form within a ruby form - ruby-on-rails

Thanks in advance for whoever tries to resolve my issue.
I am new to rails so please help, I have
rails g scaffold City name:string
rails g scaffold Area name:string city:references
rails g scaffold User name:string brief:text
rails g migration CreateAreasUsers id:false area:references user:references
rake db:migrate
CreateAreasUsers is a table join bw area and user
City and Areas are pre-populated
Two issues
1) How to add functionality in user form so that :areas_users (join table) is also updated.
2) City and Area are separate models so how can I change select tag for city with select tag for area (however area depends on city)
like:
<div id="foo">
<select>
<% #cities.each do |city|%>
<option value="<%=city.id%>"> <%=city.name%> </option>
<%end%>
<select/>
<div id="bar">
<select>
<% #areas.each do |area|%>
<option value="<%=area.id%>"> <%=area.name%> </option>
<%end%>
<select/>
</div>
div foo should be interchanged with div bar within the new user form
Please help.

Okay so since you're a Rails newbie, I'll explain what you need to know.
Association
Firstly, you're dealing with ActiveRecord associations. I guess you have some idea as to how they work, as you've asked pertinent questions.
If you need to know the specifics, you've been making a has_and_belongs_to_many relationship:
This does not require any extra models - it gives you the ability to invoke associated data directly with the join table itself.
--
To answer your first question:
How to add functionality in user form so that :areas_users (join
table) is also updated.
The answer is to pass the appropriate data through your habtm association:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :areas
end
#app/models/area.rb
class Area < ActiveRecord::Base
has_and_belongs_to_many :users
end
This allows you to reference #user.areas and #area.users respectively, including forms:
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
<%= f.select :areas, Area.all, multiple: true %>
<% end %>
Normally you'd use accepts_nested_attributes_for for this type of functionality. This is a slightly different matter which I'd have to explain with another post.
You can then alter your #user object with the following:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new user_params
end
private
def user_params
params.require(:user).permit(:user, :params, :areas)
end
end
For your second question:
City and Area are separate models so how can I change select tag for
city with select tag for area (however area depends on city) like
This is a little more involved.
First of all, you need to have your associations set up correctly. I recommended the above based on what you posted; I am not a mind reader, so if it's wrong, it's because you didn't explain yourself properly.
The bottom line is that your City and Area models need to have the following associations:
#app/models/city.rb
class City < ActiveRecord::Base
belongs_to :area
end
#app/models/area.rb
class Area < ActiveRecord::Base
has_many :cities
end
This means that you'll be able to call #city.area and #area.cities. As such, you'll be able to load a form as follows:
What you need to do is create a way for the selectbox to pull the associated values from your server. Whilst there is likely a gem or simpler way to do this, I'll just write it for you whilst I'm in the mood:
#config/routes.rb
resources :areas do
get ":id/cities", to: "areas#show" # -> we'll be using the 'show' action
end
#app/controllers/areas_controller.rb
class AreasController < ApplicationController
def show
#area = Area.find params[:id]
respond_to do |format|
format.html
format.json { render json: #area.cities }
end
end
end
This gives us a hook with which we can use Ajax to pull the relevant cities when an area is selected:
#app/assets/javascripts/application.js
$("select.areas").on("change", function(){
id = $(this).val();
$.ajax({
url: id + "/cities"
success: function(data) {
cities = JSON.Parse(data);
$('select.cities').find('option').remove();
$.each(cities, function(index, item) {
$("select#cities").append( // Append an object to the inside of the select box
$("<option></option>") // Yes you can do this.
.text(item.description)
.val(item.id)
);
});
}
});
});

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Having problems with associations in Rails

I'm creating a restaurant app that will list restaurants as a rails starting project. For now, I want the restaurant to have one menu.
class Restaurant < ActiveRecord::Base
has_one :menu
end
class Menu < ActiveRecord::Base
belongs_to :restaurant
end
I've also added a migration for restaurant_id on the menu by running rails generate migration AddRestaurantIDToMenus restaurant_id:integer and ran rake db:migrate
My problem now is how do I associate a new menu to a restaurant? When I create a new menu post it does not automatically associate with a restaurant. Is there anyway I can create a menu directly from the restaurant's show page? What would I need to set up in the restaurant controller area for this?
First, when you do a migration of a model that belongs to another model, don't add the foreign key manually, instead use references for the column type, like this
rails g migration AddRestaurantRefToMenus restaurant:references
Now, when you want to create a menu that belongs to an instance of Restaurant, you can do something like this in your controller:
#menu = #restaurant.menus.create(some_attribute: some_value, another_attibute: another_value)
Definitely read through the rails guides on associations.
Oh my... You have so much to learn...
First of all – it sounds like you're still in the early stages of your app, so unless you have a strong reason to keep any data that's already in your database, there's no real reason to create a whole new migration just to add the restaurant_id to your Menu table. Just rake db:migrate VERSION=0, edit your create_menus migration, and then `rake db:migrate' up again.
As far as actually creating a menu and associating it with a restaurant, you can do it any number of ways:
Restaurant or Menu controller:
def create_menu
#restaurant = Restaurant.find(params[id])
#menu = Menu.create(:restaurant_id => #restaurant.id, :other_attribute => "something")
#Or...
#restaurant.menu = Menu.create(:restaurant_id => #restaurant.id, :other_attribute => "something")
#Or...
#menu = Menu.new(:other_attribute => "something")
if #menu.save #returns true if saved properly
#restaurant.menus << #menu
end
end
With regards to how to do both together, you could say in your Restaurants model
accepts_nested_attributes_for :menu
Then in the new of your controller you would have something like
#restaurant = Restaurant.new
#restaurant.menu = Menu.new
Your new view would now look something like
form_for (#restaurant) do |r|
<%= r... %> #whatever they can set for the restaurant (like a name maybe)
#
r.fields_for :menu do |m|
<%= m... %> #whatever they can set for the menu
end
r.submit "button"
end
Lastly create in your controller would look something like
#restaurant = Restaurant.new(params[:restaurant])
if (#restaurant.save)
...
else
...
end
You can do this with edit/update almost the exact same way (the main difference there being that your restaurant, and perhaps its menu, already exist) so your edit would look something like
#restaurant.find(params[:id])
then in your update it would look like
#restaurant.find(params[:id])
if #restaurant.update_attributes(params[:id])
...
else
...
end
Hope that helps answer some questions you have.
p.s. Here is a good place to start if you have questions about form_for and the different inputs.

How to pass object id?

I have a relationship between two models
Category Model
class Category < ActiveRecord::Base
belongs_to :product
end
Product Model
class Product < ActiveRecord::Base
has_many :categories
end
I have category_id in products table but When I create a new product in my products table
category_id is null. I am new to rails can anyone help please?
First off, a thought - most of the time, products have many categories, but each category also contains many products. Perhaps your association should be a many-to-many? On to your actual question.
If I understand correctly, your question is really about how to create categories and products that are related to each other in the database, i.e. how to set the value of product_id when building a new category.
For clarity in case you need it, product_id would only be set for a category. The category, after all, belongs to that product, so it has to hold its owner's ID.
So, let's say you want to build a new category that belongs to an existing product - you can do this:
# in your view, where you link from products/show.html.erb to category/new.html.erb
<%= link_to "Build new category for this product", new_category_url(:id => #product.id) %>
# you must have #product defined, and you also must have
# 'resources :categories' in your routes file
# in your controller action categories/new, set the new category's product id:
def new
#category = Category.new(:product_id => params[:id])
end
# include a hidden field to contain the product_id in your new form
<%= form_for #category do |f| %>
<%= f.hidden_field :product_id %>
... other fields, labels, etc.
<% end %>
# save the record as you normally would (analogous to the code in your comment to #Chowlett).
#category = Category.new(params[:category])
if #category.save
redirect_to :action => "list", :notice => "Category saved successfully."
else
render :action => "new"
end
The above code allows you to build a product, then each category one-by-one. So, we are building your product first, then including a link from the product/show page to your category/new form, passing in the ID of the product you want that category to be part of.
If you want to build a product and some categories at the same time, it is a bit more complicated. For more information on this, take a look at http://railscasts.com/episodes/196-nested-model-form-part-1 (this is the first of a three-part series) and https://github.com/ryanb/nested_form. I don't suggest this course of action unless you are very comfortable with the above basics. I once got mired in this code for a week when I was new to Rails!
First off, you have the _id field in the wrong table. If Category belongs_to :product, then your categories table needs a field product_id.
Look at it this way: each Product can have many Categories - so what single value would you expect to find in a category_id field?
If you still have problems after correcting that, let me know.
Edit: Once you've got your tables set up, you still need to tell Rails what the link should be. You've got a few options. Assuming you've got a Category in hand, your best bet is new_prod = my_cat.create_product(). Alternatively, you can use new_prod = Product.create(:category => my_cat).
Later on, you can associate the models together like this:
my_prod.category = my_cat
my_prod.save

nested form & habtm

I am trying to save to a join table in a habtm relationship, but I am having problems.
From my view, I pass in a group id with:
<%= link_to "Create New User", new_user_url(:group => 1) %>
# User model (user.rb)
class User < ActiveRecord::Base
has_and_belongs_to_many :user_groups
accepts_nested_attributes_for :user_groups
end
# UserGroups model (user_groups.rb)
class UserGroup < ActiveRecord::Base
has_and_belongs_to_many :users
end
# users_controller.rb
def new
#user = User.new(:user_group_ids => params[:group])
end
in the new user view, i have access to the User.user_groups object, however when i submit the form, not only does it not save into my join table (user_groups_users), but the object is no longer there. all the other objects & attributes of my User object are persistent except for the user group.
i just started learning rails, so maybe i am missing something conceptually here, but i have been really struggling with this.
Instead of using accepts_nested_attributes_for, have you considered just adding the user to the group in your controller? That way you don't need to pass user_group_id back and forth.
In users_controller.rb:
def create
#user = User.new params[:user]
#user.user_groups << UserGroup.find(group_id_you_wanted)
end
This way you'll also stop people from doctoring the form and adding themselves to whichever group they wanted.
What does your create method look like in users_controller.rb?
If you're using the fields_for construct in your view, for example:
<% user_form.fields_for :user_groups do |user_groups_form| %>
You should be able to just pass the params[:user] (or whatever it is) to User.new() and it will handle the nested attributes.
Expanding on #jimworm 's answer:
groups_hash = params[:user].delete(:groups_attributes)
group_ids = groups_hash.values.select{|h|h["_destroy"]=="false"}.collect{|h|h["group_id"]}
That way, you've yanked the hash out of the params hash and collected the ids only. Now you can save the user separately, like:
#user.update_attributes(params[:user])
and add/remove his group ids separately in one line:
# The next line will add or remove items associated with those IDs as needed
# (part of the habtm parcel)
#user.group_ids = group_ids

Best practice: How to split up associations-functions in controllers with equal-access models

I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.

Resources