Wednesday, June 7, 2017

Values are duplicated in Rails form for PostgreSQL array

Leave a Comment

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.

4 Answers

Answers 1

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?

Answers 2

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> 

Answers 3

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.

Answers 4

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment