36

When a user submits a form and leaves certain fields blank, they get saved as blank in the DB. I would like to iterate through the params[:user] collection (for example) and if a field is blank, set it to nil before updating attributes. I can't figure out how to do this though as the only way I know to iterate creates new objects:

coll = params[:user].each do |c|
    if c == ""
       c = nil
    end
end

Thanks.

chrishomer
  • 4,900
  • 5
  • 38
  • 52
  • Rather than waiting until later, why not build this into the logic of the form? As the values are collected, put a check that switches `nil` for any entry containing only whitespace or nothing at all. That way blanks never get into the stored values. – Telemachus Jul 26 '09 at 12:03
  • I think the problem would be that the nil values would then not overwrite the db value. – chrishomer Jul 26 '09 at 13:50
  • Because this is now on first page of google for related search, note to other searchers: there is now a gem which will do this automatically for you - https://github.com/grosser/clear_empty_attributes – Damien Roche Aug 29 '12 at 09:34
  • Shouldn't that `ends` in the OP code snippet be `end`? – evanrmurphy Feb 11 '13 at 17:17

14 Answers14

29

Consider what you're doing here by using filters in the controller to affect how a model behaves when saved or updated. I think a much cleaner method would be a before_save call back in the model or an observer. This way, you're getting the same behavior no matter where the change originates from, whether its via a controller, the console or even when running batch processes.

Example:

class Customer < ActiveRecord::Base
  NULL_ATTRS = %w( middle_name )
  before_save :nil_if_blank

  protected

  def nil_if_blank
    NULL_ATTRS.each { |attr| self[attr] = nil if self[attr].blank? }
  end
end

This yields the expected behavior:

>> c = Customer.new
=> #<Customer id: nil, first_name: nil, middle_name: nil, last_name: nil>
>> c.first_name = "Matt"
=> "Matt"
>> c.middle_name = "" # blank string here
=> ""
>> c.last_name = "Haley"
=> "Haley"
>> c.save
=> true
>> c.middle_name.nil?
=> true
>>
Matt Haley
  • 4,304
  • 4
  • 25
  • 17
  • "before_save :blank_if_nil" should be "before_save :nil_if_blank". I like this approach the best so far, with the code pushed into the model. – jdl Jul 27 '09 at 14:16
  • 3
    Somewhat new to Ruby/Rails, but wouldn't wouldn't before_validate be better than before_save here? – Matt Huggins Mar 21 '11 at 16:16
  • The only problem i see with this is that the callback will be called every time a model is validated (or saved), and it will have to iterate over all the same attributes regardless of the context. For example, there may be one controller action affecting on set of attributes, and another affecting another set. I think this callbacks should somehow be attached to individual parameters, like validations are. – Alexey Sep 16 '12 at 12:52
  • 10
    In mvc, it is part of the controller's role to mediate input. The model should not manipulate values passed to a create/update function, only accept or reject through validation. – pduey Oct 10 '12 at 15:29
  • Remember to also update existing data in the database (on development, staging and production) by running this in the respective consoles: `Customer.all.each do |c| c.save end` – Magne Feb 16 '15 at 14:57
  • Here's a mini-framework based on this, so it can be applied to all model classes: https://gist.github.com/mahemoff/dbd33b0a34ff21cc8787 – mahemoff Apr 07 '15 at 18:42
8

If you just want to kill the blanks, you can just do params.delete_if {|k,v| v.blank?}.

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • 28
    Careful with this. If you're using update_attributes, and you have fields which are allowed to be blank, the user will never be able to clear those fields if you remove them from the params hash. – jdl Jul 26 '09 at 05:33
  • thank you. so many answers based on engineering when simple programming will do the trick. filters, callbacks, observers, all super awesome things. but when one simple line of ruby code works perfectly, why not use it? btw @jdl, this can be tightened like I did in order to handle password fields in rails 3.1... params[:user].delete_if {|k,v| /.*password.*/.match(k) && v.blank?} Can anyone say DRY KISS? – pduey Oct 10 '12 at 15:52
  • 2
    To address @jdl's concern, make the params you want to kill explicit in the condition: `params.delete_if {|k,v| %w(killme1 killme2).include?(k) and v.blank?}` – evanrmurphy Feb 11 '13 at 19:49
  • Also this won't handle nested params, which is how form values get submitted – Yarin Feb 04 '15 at 19:27
  • 1
    He will end never erasing a field when using `update`. – Andre Figueiredo May 29 '15 at 21:07
  • also `false.blank?` => true – jazzytomato Jan 10 '17 at 14:15
5

A good gem for handling this in the model: https://github.com/rmm5t/strip_attributes

It defines a before_validation hook that trims whitespaces and sets empty strings to nil.

Yarin
  • 173,523
  • 149
  • 402
  • 512
3

before_save seems like the wrong location to me, what if you want to use the value before saving. So I overrode the setters instead:

# include through module or define under active_record
def self.nil_if_blank(*args)
  args.each do |att|
    define_method att.to_s + '=' do |val|
      val = nil if val.respond_to?(:empty?) && val.empty?
      super(val)
    end
  end
end

#inside model
nil_if_blank :attr1, :attr2

Just to be complete I put the following in lib/my_model_extensions.rb

module MyModelExtensions
  def self.included(base)
    base.class_eval do
      def self.nil_if_blank(*args)
        args.each do |att|
          define_method att.to_s + '=' do |val|
            val = nil if val.respond_to?(:empty?) && val.empty?
            super(val)
          end
        end
      end
    end
  end
end

and use it like this:

class MyModel
  include MyModelExtensions
  nil_if_blank :attr1, :attr2
end
Hampei
  • 3,838
  • 1
  • 18
  • 18
3

In the ApplicationController:

class ApplicationController < ActionController::Base

  def nilify(p)
    p.transform_values!{|v| v.present? ? v : nil }
  end

end

In your controller, modify the strong parameters filter method to call nilify:

class UserController < ApplicationController

  def user_params
    nilify params.require(:user).permit(:email, :name)
  end

end
Bill Lipa
  • 2,039
  • 1
  • 19
  • 11
2

You could do this using inject, which is obvious as to what is happening.

params = params.inject({}){|new_params, kv| 
  new_params[kv[0]] = kv[1].blank? ? nil : kv[1]
  new_params
}

There is also a hack you can do with merge by merging with itself, and passing a block to handle the new value (although this isn't really the intended use for it, but it is more concise)

params.merge(params){|k, v| v.blank? ? nil : v}
madlep
  • 47,370
  • 7
  • 42
  • 53
2

Ordinarily I would encourage functionality to be moved into the model, as stated in other answers this means that you will get the same behavior no matter where the change originates from.

However, I don't think in this case it is correct. The affect being noticed is purely down to not being able to encode the difference between a blank string and nil value in the HTTP request. For this reason it should be remedied at the controller level. It also means that in other places it is still possible to store an empty string in the model (which there could be for a legitimate reason for, and if not it is simple to cover with standard validations).

The code I'm using to overcome this problem is:

# application_controller.rb
...

def clean_params
  @clean_params ||= HashWithIndifferentAccess.new.merge blank_to_nil( params )
end

def blank_to_nil(hash)
  hash.inject({}){|h,(k,v)|
    h.merge(
      k => case v
      when Hash  : blank_to_nil v
      when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
      else v == "" ? nil : v
      end
    )
  }
end

...

I've tried to keep the code as concise as possible, although readability has suffered somewhat, so here is a test case to demonstrate its functionality:

require "test/unit"
class BlankToNilTest < Test::Unit::TestCase

  def blank_to_nil(hash)
    hash.inject({}){|h,(k,v)|
      h.merge(
        k => case v
        when Hash  : blank_to_nil v
        when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
        else v == "" ? nil : v
        end
      )
    }
  end

  def test_should_convert_blanks_to_nil
    hash =        {:a => nil, :b => "b", :c => ""}
    assert_equal( {:a => nil, :b => "b", :c => nil}, blank_to_nil(hash) )
  end

  def test_should_leave_empty_hashes_intact
    hash =        {:a => nil, :b => "b", :c => {}}
    assert_equal( {:a => nil, :b => "b", :c => {}}, blank_to_nil(hash) )
  end

  def test_should_leave_empty_arrays_intact
    hash =        {:a => nil, :b => "b", :c => []}
    assert_equal( {:a => nil, :b => "b", :c => []}, blank_to_nil(hash) )
  end

  def test_should_convert_nested_hashes
    hash =        {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => "",  :g => "",  :h => 5}, :i => "bar"}}
    assert_equal( {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => nil, :g => nil, :h => 5}, :i => "bar"}}, blank_to_nil(hash) )
  end

  def test_should_convert_nested_hashes_in_arrays
    hash =        {:book_attributes => [{:name => "b", :isbn => "" },{:name => "c", :isbn => "" }], :shelf_id => 2}
    assert_equal( {:book_attributes => [{:name => "b", :isbn => nil},{:name => "c", :isbn => nil}], :shelf_id => 2}, blank_to_nil(hash))
  end

  def test_should_leave_arrays_not_containing_hashes_intact
    hash =        {:as => ["", nil, "foobar"]}
    assert_equal( {:as => ["", nil, "foobar"]}, blank_to_nil(hash))
  end

  def test_should_work_with_mad_combination_of_arrays_and_hashes
    hash =        {:as => ["", nil, "foobar", {:b => "b", :c => "",  :d => nil, :e => [1,2,3,{:a => "" }]}]}
    assert_equal( {:as => ["", nil, "foobar", {:b => "b", :c => nil, :d => nil, :e => [1,2,3,{:a => nil}]}]}, blank_to_nil(hash))
  end

end

This can then be used in a controller like so:

...
@book.update_attributes(clean_params[:book])
...
Theozaurus
  • 955
  • 1
  • 8
  • 21
2

You can use attribute_normalizer gem and use the blank normalizer that will transform empty strings in nil values.

1

Use the "in place" collect method (also known as map!)

params[:user].collect! {|c| c == "" ? nil : c}
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 1
    Tried this with Rails 3.2.3 and Ruby 1.9.3p0. Any idea why this would give `undefined method 'collect!' for #`? Just noticed that this thread is 3 years old. Is there a better way now? – Mark Berry Aug 20 '12 at 22:32
  • That is a [Hash](http://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html), not an Array -- hashes don't understand `collect` – glenn jackman Aug 20 '12 at 22:54
  • Okay, then I guess I don't understand your original answer. Isn't `params[:user]` the hash of parameters passed to the UsersController? – Mark Berry Aug 20 '12 at 23:16
  • This suffers from the same limitation as mentioned by jdl in the comment to Chuck's answer. – Magne Feb 16 '15 at 14:48
1

Chris,

Here is a recursive parsing of params that have blanc values.

before_filter :process_params

......



private
def process_params
....
  set_blanc_values_to_nil(params)
end

# Maybe move method to ApplicationController
# recursively sets all blanc values to nil
def set_blanc_values_to_nil!(my_hash)
    my_hash.keys.each do |key|
        val = my_hash[key]
        next if val.nil?
        my_hash[key] = nil if val.is_a?(String) && val.empty?
        set_blanc_values_to_nil!(val) if val.is_a? Hash
    end
end
Vlad Zloteanu
  • 8,464
  • 3
  • 41
  • 58
  • Chris, Please also notice that it is not good to do something like: container.each{|| ... #modifying container here } if you plan to delete/add elements in container (not the case in here, but just keep that in mind) – Vlad Zloteanu Jul 26 '09 at 11:47
0

Here is how I did it.

def remove_empty_params(param, key)
  param[key] = param[key].reject { |c| c.empty? }
end

and call it with

remove_empty_params(params[:shipments], :included_clients)

No need to get super tricky in the model. And this way you can control which params get cleaned up.

params = {
      "shipments"=>{
        "included_clients" => ["", "4"]
      }
    }

will turn into

>> params["shipments"]
=> {"included_clients" => ["4"] }
bfcoder
  • 3,042
  • 2
  • 28
  • 35
0

If you know which attributes you want to encode blanks as nils for you can use the following attribute setter override:

def colour=(colour)
  super(colour.blank? ? nil : colour)
end

A bit bulky if you have a lot of attributes to cover though.

Brendon Muir
  • 4,540
  • 2
  • 33
  • 55
0

I generalized an answer and made a hook/extension that can be used as an initializer. This allows it to be used across multiple models. I've added it as part of my ActiveRecordHelpers repo on GitHub

sethvargo
  • 26,739
  • 10
  • 86
  • 156
0

Rails 6.1+ Answer with compact_blank

The compact_blank method will do this exactly what you're looking for. For example:

def user_params
  # removes first_name, middle_name, last_name if blank (i.e. '')
  params.require(:user).compact_blank.permit('first_name', 'middle_name', 'last_name')
end

If the user leaves the middle_name field empty, Rails forms will generate params as: { user: { first_name: 'Bob', middle_name '', last_name: 'Loblaw' }}. By default, this adds an empty string into your database for middle_name, instead of null. Not what we generally want.

The compact_blank method as shown above will remove the middle_name from params since it's blank. It removes blank values from params, such as empty strings (full list: nil, '', ' ', [], {}, and false).

Example form for User

# users/_form.html.erb
<%= form_for(@user) do |f| %>
  <%= f.text_field :first_name %>
  <%= f.text_field :middle_name %>
  <%= f.text_field :last_name %>
  <%= f.submit "Create User" %>
<% end %>

Using the above form along with the user_params method will remove any fields left blank by the user so that they're saved to the database as null.

Matt
  • 5,800
  • 1
  • 44
  • 40