I have implemented a nested attribute form using the nested attributes Railscast as a guide. As a result, the user can click an icon to dynamically add "child" rows to my view.
Unfortunately, I can only make this work for the last icon in my view (illustrated here). This icon is generated in my view, but the others are generated in the partial which is used to render each row.
Is it possible to do this? If so, what is the best approach?
Here is my latest attempt.
Sheet has_many
Slots. In the sheet edit view, I use a sheet form builder (sheet
) to render my slot partial and also pass it to a helper link_to_add_fields
which renders a link which will generate a new row when clicked (this part works fine). You'll notice I am also attempting to pass sheet
to the partial so that I can call link_to_add_fields
from there but this is where it breaks down:
The view - edit.html.haml
:
= sheet.fields_for :slots do |builder|
= render 'slots/edit_fields', f: builder, sheet:sheet
= link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'
The partial - _edit_fields.html.haml
:
- random_id = SecureRandom.uuid
.row.signup{:id => "edit-slot-#{random_id}"}
.col-md-1
%span.plus-icon
= link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'
%span.minus-icon
= image_tag "minus.jpg", size:"18x18", alt:"Minus"
.col-md-2= f.text_field :label
... other fields ...
The helper method:
def link_to_add_fields(name, f, association, partial)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render(partial.to_s.singularize + "_fields", f: builder, name: name)
end
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
I get undefined local variable or method 'sheet'
on the call to the helper from the partial. Basically, I need the sheet (parent) form builder to be available on each link for the helper to work. Or I need to give up on this approach and use AJAX (also tried that).
UPDATE
After debugging a bit, it is clear that sheet
is getting passed down to the partial. The root issue is that I seem to be setting up an endless recursion:
link_to_add_fields
so that my +
icon can serve as the "add child" link.link_to_add_fields
renders the partial so that the fields can be generated when the +
icon is pressed.The other issue I am running into is that when the original children are rendered, they get sequential indexes in the attribute collection (0, 1, 2,...). So, even if I figure out a way to render new child rows among the originals, I'm not sure how I will be able to maintain the order of children when the form is submitted without a lot of jQuery gymnastics or something.
undefined local variable or method 'sheet'
Is being caused by the way you are rendering the partial.
= render 'slots/edit_fields', f: builder, sheet:sheet
Is not sufficient for passing variables to a partial. You need:
= render partial: 'slots/edit_fields', locals: {f: builder, sheet:sheet}
That will make f available in your partial.
I wound up solving this with jQuery. Not super elegant but very effective. Basically, I just added a click handler for the +
icons which built the new row and inserted it where needed (basically just hacked out the jQuery necessary to replicate the HTML produced by Rails). In case my use case helps someone else in the future, here is the jQuery (see imgur link in original post for reference):
//click handler to add edit row when user clicks on the plus icon
$(document).on('click', '.plus-icon', function (event) {
//build a new row
var one_day = 24 * 3600 * 1000;
var d = new Date();
var unique_id = d.getTime() % one_day + 10000; // unique within a day and offset past original IDs
var new_div =
$('<div/>', {'class': 'row signup'}).append(
$('<div/>', {'class': 'col-md-1'}).append(
$('<span/>', {'class': 'plus-icon'}).append(
'<img alt="Plus" src="/assets/plus.jpg" width="18" height="18" />'
)
).append(
$('<span/>', {'class': 'minus-icon'}).append(
'<img alt="Minus" src="/assets/minus.jpg" width="18" height="18" />'
)
)
).append(
$('<div/>', {'class': 'col-md-2'}).append(
'<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][label]" id="sheet_slots_attributes_'+unique_id+'_label">'
)
).append(
$('<div/>', {'class': 'col-md-2'}).append(
'<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][name]" id="sheet_slots_attributes_'+unique_id+'_name">'
)
).append(
$('<div/>', {'class': 'col-md-2'}).append(
'<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][email]" id="sheet_slots_attributes_'+unique_id+'_email">'
)
).append(
$('<div/>', {'class': 'col-md-2'}).append(
'<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][phone]" id="sheet_slots_attributes_'+unique_id+'_phone">'
)
).append(
$('<div/>', {'class': 'col-md-2'}).append(
'<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][comments]" id="sheet_slots_attributes_'+unique_id+'_comments">'
)
);
var new_input =
'<input id="sheet_slots_attributes_'+unique_id+'_id" type="hidden" name="sheet[slots_attributes]['+unique_id+'][id]" value="'+unique_id+'">';
//insert new row before clicked row
$(new_div).insertBefore($(this).parent().parent());
$(new_input).insertBefore($(this).parent().parent());
});
I was stuck here for a while... a reminder that there are usually multiple ways to solve a problem and it is important to not get stuck on one idea. Thanks all for your suggestions and inputs.
This sounds remarkably close to a problem I had to solve a few months back.
If I have this correct, your user has a list of 'things' and you wish your user to be able to add another 'thing' using the form being displayed.
I tackled it like this. My 'thing' is empires, users can change the name, delete the empire or add a new empire, within the context of managing their personal data such as changing their email address and passwords.
<%= form_for @user do |f| %>
<%= render layout: "users/partials/fields",
locals: {f: f} do %>
<b>Empires</b><br>
<% n = 0 %>
<%= f.fields_for :empires do |empire_form, index| %>
<% n += 1 %>
<%= empire_form.label :name, class: 'ms-indent' %>
<%= empire_form.text_field :name, class: 'form_control' %>
<%= empire_form.label :_destroy, 'Delete' %>
<%= empire_form.check_box :_destroy %>
<br>
<% end %>
<!-- Empty field for new Empire -->
<b class='ms-indent'>Name</b>
<input class="form_control" value="" name="user[empires_attributes][<%=n+1%>][name]" id="user_empires_attributes_0_name" type="text"> <b>new</b>
<br>
<% end %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
The interesting bit is the counter to get the number of existing empires (I cannot remember why I didn't just use size or length) and using 'n+1' in an html input field to ensure the new empire was included in the form submitted, with its correct id. Note the use of the html input tag, Rails does not have an equivalent.
[Just an additional note in light of DickieBoys comment. This 'n+1' approach works but might be replaced by a random number and still work. I intend to experiment with this at a later date, but it works so I'm not in a hurry.]
The partial 'fields' looks like this, note the 'yield' in the middle.
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control', autofocus: true %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= yield %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control', value: "" %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
The 'fields' partial includes a do-end block containing my empires code. The partial includes this block where it states 'yield'.
Finally, the update controller code.
def update
@user = User.find(params[:id])
updated = false
begin
updated = @user.update_attributes(user_params)
rescue ActiveRecord::DeleteRestrictionError
flash[:warning] = "Empire contains ships. Delete these first."
end
if updated
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
I won't pretend this is the only solution, nor even necessarily the best. But it took a few days to nut out (Google and StackOverflow failed me!) and has worked reliably since then. The key was really to drop attempts to find a Rails solution for adding a new empire and just use html with a new id for the new empire. You may be able to improve on it. Hope it helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With