1

I have two associated models/tables

app/models/city.rb
has_many :businesses
Fields: id, city_name, state_code

app/models/business.rb
belongs_to :city
Fields: id, biz_name, address, city_name, state_code, zip

In the new business form where you enter the business address, it is common to have a dropdown select box of states and/or countries. But for cities there are thousands so that wouldn't work. Is there a way to check that the city is listed in the cities table before allowing it to be saved? And possibly even putting the city_id in that field instead of the city_name?

Steve Carey
  • 2,876
  • 23
  • 34

3 Answers3

0

I don't think validates_associated will work for this situation. I'd just make a custom validate method. This will work if your cities are all stored in lowercase, otherwise use humanize instead of downcase.

validate :city_must_exist_with_city_name

def city_must_exist_with_city_name
    if !City.find_by(name: city_name.downcase)
        errors.add(:item_name, "must be a valid city.")
    end
end

Edit: You can take things a step further with capitalization issues by doing this:

if !City.where("lower(name) = ?", city_name.downcase).first
Jason
  • 2,725
  • 2
  • 14
  • 22
  • This worked, thanks. It doesn't directly use the association between the business and city tables so I could use this and remove the has_many/belong_to statements. – Steve Carey Oct 14 '15 at 03:01
0

Adding an auto-completion feature to your city_name field would solve your problem.

First of all, because of the association between your Business and City objects, you WILL want to return a business[city_id] parameter rather than a city_name parameter (assuming you're using a form_for @business helper).

If you're using jQuery, you can use the devbridge jQuery-Autocomplete plugin: https://github.com/devbridge/jQuery-Autocomplete

Create a simple method that returns a list of cities in JSON format (will require the jbuilder gem, which should be included by default in your Gemfile), for instance:

in app/controllers/businesses_controller.rb:

def ac_cities
  @query = params[:query]
  @results = City.where('city_name LIKE ?', "#{@query}%").order(city_name: :asc).limit(10)
  respond_to do |format|
    format.json
  end
end

in app/views/businesses/ac_cities.json.jbuilder:

json.query @query
json.suggestions @results do |result|
  json.value "<div data-id=\"#{result.id}\">#{result.city_name}</div>"
end

Initialize the devbridge instance and connect it to your city_name field with CoffeeScript (or JS). The devbridge plugin also has an onSelect feature that will allow you to populate the hidden business[city_id] form field. For instance, given the JSON returned above:

in app/assets/javascripts/businesses.js.coffee:

BusinessCitySelect = (suggestion) ->
  $(this).next('#business_city_id').val(suggestion.value.match(/data-id="(\d+)"/i)[1])
  $(this).val(suggestion.value.replace(new RegExp('<.*?\/?>', 'gi'), ''))

SearchACFormat = (suggestion, cv) ->
  pattern = '(<div.*?>.*?)('+cv+').*?'
  suggestion.value.replace(new RegExp(pattern, 'gi'), '$1<strong>$2<\/strong>')

$("#city_name").devbridgeAutocomplete({
  serviceUrl: "/businesses/ac_cities.json",
  formatResult: SearchACFormat,
  onSelect: BusinessCitySelect,
  noCache: true
})

the SearchACFormat method makes use of devbridge's formatResult feature to clean up our JSON for proper display within the city_name input field while also providing highlighting.

Thomas Hennes
  • 9,023
  • 3
  • 27
  • 36
  • Thanks, this was absolutely the right direction for my question. Meaning using Autocomplete. I had been using the wrong terminology when trying to find a solution. But, I'm pretty weak on JavaScript so I opted to use a Gem that takes care of that behind the scenes. I posted what I did as one of the solutions. – Steve Carey Oct 22 '15 at 05:57
  • You're welcome, Steve. As long as you found an implementation that worked for you, it's really all that matters in the end. Although there might come a time where you will need greater control over the autocomplete response and/or rendering, and then you'll really want to pop the hood and delve into the javascript - and believe me, it's not that complicated ;) – Thomas Hennes Oct 22 '15 at 09:11
0

Based on the response suggestions and my own further investigation here is my conclusion for the best way to get a large number of options in a form from an associated table.

It looks like autocomplete is the best solution. That is a regular text field but when you type in the second letter it pulls in the first 10 matches that it finds listing them below. Each additional letter refines the search. Since I am not highly skilled at JavaScript, rather than adding a plugin supplemented with my own JS as suggested, I opted to abstract the JS coding by using a gem, specifically rails-jquery-autocomplete.
Here are the steps:

Setup

Generate scaffolds:

rails generate scaffold City name state
rails generate scaffold Business name address city:references state zip
rake db:migrate

Install the gem. It uses jQuery-UI's autocomplete widget so install the rails gem for that as well.

# gemfile
gem 'jquery-ui-rails'
gem 'rails-jquery-autocomplete'

Add these to your javascript and stylesheet assets:

# app/assets/javascripts/application.js > put these after //= require jquery_ujs
//= require jquery-ui/autocomplete
//= require autocomplete-rails

# app/assets/stylesheets/application.css     
*= require jquery-ui/autocomplete

Models

First create the association:

# app/model/city.rb
has_many :businesses

# app/model/business.rb
belongs_to :city

If you want to ensure that the city entered is in the cities table then add a validation:

# app/models/business.rb
validates :city, presence: true

The business table has a field for city_id not city name. We can create a virtual attribute in the business model file using getter and setter methods. There's probably a more up-to-date way to do this since I got it from a 4 year old RailsCast but it works.

#getter method
def city_name
  city.try(:name)
end
#setter method
def city_name=(name)
  self.city = City.find_by(name: name) if name.present?
end

Routes

Here's where the gem's magic kicks in. Add a route to the business REST resources formatted as autocomplete_model_attribute:

# config/routes.rb
resources :businesses do
  get 'autocomplete_city_name' on: :collection
end

Controller

At the top of the controller call the autocomplete action passing it the class name and the attribute. By default autocomplete will match the text starting at the beginning of the word. If you want to search anywhere in the word set the 'full' option to true, otherwise leave it out.

# app/controllers/business_controller.rb
autocomplete :city, :name, full: true

Since we created a city_name virtual attribute in the business model we need to add it to the permitted params list

def business_params
  params.require(:business).permit(:name, :address, :city_id, :state, :zip, :city_name)
end

Views

In the form to create or edit a new business instance change the city_id field to the city_name virtual attribute using the autocomplete_field helper. The 'data-auto-focus' option will automatically select the first value in the list. Leave it off if you don't want that.

# app/views/businesses/_form.html.erb
<div class="field">
  <%= f.label :city_name %><br>
  <%= f.autocomplete_field :city_name, autocomplete_city_name_businesses_path, 'data-auto-focus' => true %>
</div>

Done!

Steve Carey
  • 2,876
  • 23
  • 34