2

If I save an array to the database, it is persisted properly, but when I look at the form, it puts concatenated values in the array in all array input fields.

E.g. in the form below I have 2 input fields, so you can store 2 nicknames for a Pet inside the array column. I put "a" in field 1 and "b" in field 2 and press save. I inspect in the Rails console and it's stored correctly: nicknames: ["a", "b"]. But when I open up the form, both input fields contain "a b". So the values are concatenated somehow. What am I doing wrong?

To reproduce:

rails g scaffold Pets

Then in the new migration:

class CreatePets < ActiveRecord::Migration
  def change
    create_table :pets do |t|
      t.string :nicknames, array: true
      t.timestamps null: false
    end
  end
end

In the controller:

def pet_params
  params[:pet].permit(nicknames: [])
end

In the form:

<%= simple_form_for(@pet) do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <%= f.text_field :nicknames, name: 'pet[nicknames][]' %>
    <%= f.text_field :nicknames, name: 'pet[nicknames][]' %>
  </div>

  <div class="form-actions">
    <%= f.button :submit %>
  </div>
<% end %>

Create a new Pet with nicknames "a" and "b". Go to edit the Pet and the inputs now contain "a b" and "a b", but it's stored as 'nicknames: ["a", "b"]' in the database according to the Rails console.

5
  • You need to provide a lot more context for someone to help you. What is the form object? How is it initialized in your controller? How does your controller handle validation errors? Commented May 31, 2017 at 23:43
  • @AdamLassek good point, I've updated the description to make it more generic and have complete reproduction steps! Commented Jun 1, 2017 at 4:07
  • @PeterEvjan I think this is not a correct way to do it, because pets can have many nicknames, so you can't know how many fields you need to add. You can create another model called Nickname and set up a relationship so that Pets will have many nickname and nickname belongs to Pet, then using nested form. If you still want to do in this way, maybe this article can help you: tenforward.consulting/blog/… Commented Jun 1, 2017 at 4:27
  • @Thanh thanks for the input! Adding a variable number of input boxes is not a problem. Plus this is legacy code that and it works well except for this little problem which should be solvable. Commented Jun 1, 2017 at 4:30
  • @PeterEvjan, so I think you will need to show nicknames manually on edit page, check if @pets is persisted, than iterate @pets.nicknames and insert separate text field with value of nickname. Commented Jun 1, 2017 at 4:47

4 Answers 4

2
+100

Check if @pet.persisted

if yes, loop through the values and display.

if no, do as you do always for new.

<div class="form-inputs">
  <% if @pet.persisted? && (@pet.nicknames.length > 0) %>   
    <% @pet.nicknames.each do |nickname| %> 
        <%= f.text_field :nicknames, name: 'pet[nicknames][]', value: nickname %>
    <% end %>
  <% else %>
    <%= f.text_field :nicknames, name: 'pet[nicknames][]' %>
    <%= f.text_field :nicknames, name: 'pet[nicknames][]' %>
  <% end %>
</div>

Here is the documentation for persisted?

Sign up to request clarification or add additional context in comments.

3 Comments

This approach is very imperative. What if user wants to add more pets? What if there was only one pet and user loads edit? (there will be only one field) What you do is differentiate logic for no pets and at least one pet. Your @pet.nicknames.length > 0 is redundant, because there will be no iteration anyway if @pet.nicknames.length == 0. @pet.persisted? breaks the logic if validation fails: they won't be persisted, and edit won't render what you just entered. Therefore this approach is incorrect. Look at my approach, it works in every possible scenario, even without javascript.
First of all If there is only one pet only one loop will be iterated for it. If the validation fails, breaking the logic is the correct way and already persisted values will be shown anyway
I misspoke, add more nicknames, not pets. And in second sentence 'only one nickname and user loads edit. If validation fails, your approach will show two fields, each with the same problem OP already has. Because, nicknames won't be persisted and you fallback to the approach OP already using.
1

Simple Form doesn't know how to do this, so you could either write your own custom Input (described in the gem README and in @Thanh's link above), or you could just tell it which values to use:

<div class="form-inputs">
  <%= f.text_field :nicknames, name: 'pet[nicknames][]', value: @pet.nicknames.try(:first) %>
  <%= f.text_field :nicknames, name: 'pet[nicknames][]', value: @pet.nicknames.try(:second) %>
</div>

Comments

1

I presume that nicknames are typed in by the user, rather than selected from a list. As such, have you given thought to how nicknames will be added and removed? Having only two isn't too onerous, but storing them in an array means there could at some point be more?

One option here would be to keep the form simple, but use a JavaScript library such as Selectize.js to add functionality (either integrated directly, or through selectize-rails). This allows users to work with nicknames as a list, and saves you the effort of writing additional JS in order to add or remove entries.

Then your form simplifies to:

<%= simple_form_for(@pet) do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <%= f.input :nicknames %>
    <%# untested - you might need to append `input_html: { value: @pet.nicknames.join(',') }` %>
  </div>
  ...

And you can initialise the input like this:

$('.pet_nicknames').selectize({
  delimiter: ',',
  placeholder: 'Type in nickname(s)',
  create: function(input) {
    return {
      value: input,
      text: input
    }
  }
});

You can check the usage options for Selectize here e.g. if you need to restrict the max number of nicknames that a user can input.

Comments

1

You have the same a b in each field because you have the same field: :nicknames

What you should do, is generate multiple fields for each one, almost as Paul here said, but with a slightly different approach.

In your view:

<div class="form-inputs">
  <% @pet.nicknames.each do |nickname| %> 
    <%= f.text_field :nicknames, name: 'pet[nicknames][]', value: nickname %>
  <% end %>
  <%= f.text_field :nicknames, name: 'pet[nicknames][]' %>
  <%= f.hidden_field :nicknames, name: 'pet[nicknames][]', disabled: true, class: 'js-nickname-input-template' %>
</div>

This will render one field for nickname even on new record, where there are no nicknames present yet. If you edit, there will be field for each nickname, and one empty for a new one.

There is also one hidden disabled field, for the next task presented to you.

What you should do to make this fully functional is add javascript functionality for adding or destroying fields for nicknames.

In your javascript assets:

$('#some-button-for-new').click(function(e){
  $templ = $('.js-nickname-input-template');
  $el = $templ.clone();
  $el.removeClass('.js-nickname-input-template');
  $el.removeAttr('disabled');
  $el.attr('type', 'text');
  $templ.before($el);
});

$('.some-button-for-destroy').click(function(e){
  $el = $($(e.target).data('identifier'));
  $el.remove();
});

So, first handler adds new field for nicknames, and the second removes element. Pay attention: this is not copypaste-ready, especially removal. You might want to work on which element should be removed (maybe some li or div containing field and remove button itself).

tl;dr: the idea is creating fields for each existing array element server-side, plus one empty, plus template. You can add and remove fields with javascript, using template.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.