kristianmandrup / gmaps-autocomplete-rails

Google Maps v3 search with jQuery UI Autocomplete, ready for use with Rails asset pipeline
MIT License
73 stars 35 forks source link

Gmaps autocomplete for Rails

Extracted from gmaps-autocomplete and improved markedly and then packaged for use with Rails as an asset gem :)

General info

The script is now compiled from Coffeescript and allows you to have multiple fields linked to multiple instances of the GmapsCompleter class on the same page.

I recommend that you also check out: google maps and RoR

Upgrading

Version 1.3+ now comes only with a class based GmapsCompleter. The old static GmapsCompleter container, used in version 1.2 (and below) has been deprecated.

Please upgrade your configuration functionality as demonstrated in the usage/config instructions below.

Time for a redo!?

In my own humble opinion, this library is in dire need to be completely redone. I would love if someone out there was brave enough to refactor (or better redo from scratch!) this library. The time has come.... Thanks ;)

Install

Update gem dependencies

In your project Gemfile

gem 'jquery-rails'
gem 'gmaps-autocomplete-rails'

Or to always use the most recent version, use `gem 'gmaps-autocomplete-rails', github: "kristianmandrup/gmaps-autocomplete-rails"``

Then run bundle install;)

Packed and ready for use with the Asset pipeline :)

Update javascript dependencies

Add to javascript manifest file, fx app/assets/javascripts/application.js

//= require jquery_ujs
//= require gmaps-auto-complete

Update style dependencies

Add to stylesheets manifest file, fx app/assets/stylesheets/application.css

*= require gmaps-auto-complete

Include the google maps script before application.js, fx in your layout file:

application.html.erb

<script async defer type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>
<%= javascript_include_tag "application" %>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min.js"></script>

Note also that the autocomplete script currently depends on jQuery 1.6+. Please feel free to remove this dependency with a pull request :)

Using Google API keys

Google API keys

<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
  type="text/javascript"></script>

Note: Google apparently now requires use of the HTTPS protocol. This is enforced to ensure that your key (and location?) is not intercepted. Better safe than sorry ;)

Customization

If you want to customize the gem to use another autocomplete "engine" than the default (assumed to be jquery-ui), then fork the gem and change the following in the gmaps-auto-complete.coffee script to suit your needs and compile to js.

# around line 237
    autocompleteOpts = $.extend true, defaultAutocompleteOpts, autocompleteOpts

    $(@inputField).autocomplete(autocompleteOpts)

    # triggered when user presses a key in the address box
    $(@inputField).bind 'keydown', @keyDownHandler
    # autocomplete_init

  keyDownHandler: (event) =>
    if (event.keyCode == 13)
      @geocodeLookup 'address', $(@inputField).val(), true
      # ensures dropdown disappears when enter is pressed
      $(@inputField).autocomplete "disable"
    else
      # re-enable if previously disabled above
      $(@inputField).autocomplete "enable"

PS: I'm not sure if this is the only place that requires changes...

Initialize

Add functionality that is executed after the document (and all scripts) have been fully loaded. Example:

mypage.js

jQuery(function() {
  var completer;

  completer = new GmapsCompleter({
    inputField: '#gmaps-input-address',
    errorField: '#gmaps-error'
  });

  completer.autoCompleteInit({
    country: "us"
  });
});

or using Coffeescript

mypage.js.coffee

jQuery ->
    completer = new GmapsCompleter
        inputField: '#gmaps-input-address'
        errorField: '#gmaps-error'

    completer.autoCompleteInit
        country: "us"

Configuration options

The constructor function can take a configuration option hash that can configure the specific workings of the GmapsCompleter. It uses the following defaults:

{
  mapElem: "#gmaps-canvas", 
  zoomLevel: 2, 
  mapType: google.maps.MapTypeId.ROADMAP,
  pos: [51.751724, -1.255284],
  inputField: '#gmaps-input-address',
  errorField: '#gmaps-error',
  debugOn: false
};

The following default methods can be replaced by configuration:

These methods are used to control how the gmaps data is used to update the UI on the page, such as the position output and map position update. Customize these needed.

The default logic (taken from GmapsCompleterDefaultAssist) is:

  defaultUpdateMap: (geometry) -> 
    map     = @map
    marker  = @marker

    map.fitBounds(geometry.viewport) if map
    marker.setPosition(geometry.location) if marker

  # fill in the UI elements with new position data
  defaultUpdateUI: (address, latLng) ->
    $(@inputField).autocomplete 'close'

    @debug 'country', @country

    updateAdr = address.replace ', ' + @country, ''
    updateAdr = address

    @debug 'updateAdr', updateAdr

    $(@inputField).val updateAdr
    @positionOutputter latLng

  defaultPositionOutputter: (latLng) ->
    $('#gmaps-output-latitude').html latLng.lat()
    $('#gmaps-output-longitude').html latLng.lng()

The default update UI logic removes the country from the address displayed.

autoCompleteInit

The function autoCompleteInit, called on an instance of GmapsCompleter, can takes an option hash. Currently only region, country and autocomplete can be used.

Example:

autoCompleteInit({region: 'DK', country: 'Denmark'});

Will make searches in the DK region and remove ', Denmark' from the result.

Note: Not sure if this is still useful with the new instance based approach!?

Parameter autocomplete allows to configure JQuery autocomplete widget

Example:

autoCompleteInit({
  region: 'DK', 
  country: 'Denmark',
  autocomplete: {
    minLength: 4,
    position: {
      my: "center top",
      at: "center bottom"
    }
  }
});

Assist object

The options hash for the constructor can now take an assist object as an argument. The assist object can be a class or a simple object containing the following:

  options:
    mapElem: '#gmaps-canvas'
    zoomLevel: 2
    mapType: google.maps.MapTypeId.ROADMAP
    pos: [0, 0]
    inputField: '#gmaps-input-address'
    errorField: '#gmaps-error'
    debugOn: true  

  # update marker and map
  updateMap: (geometry) ->

  # fill in the UI elements with new position data
  updateUI: (address, latLng) ->

  # display current position
  positionOutputter: (latLng) ->

  # optionally also message functions (see below)

If you use a class you can use Coffeescript extends (see http://coffeescript.org/#classes) to subclass the default implementation. You can then optionally use super to call the base implementation.

Example:

class MyCompleterAssist extends GmapsCompleterDefaultAssist
  updateUI: (address, latLng) ->
    console.log "Doing my own thang!"
    // ...

    super (address, latLng)

Usage (config):

    completer = new GmapsCompleter
        inputField: '#gmaps-my-input-address'
        assist: MyCompleterAssist

Usage with Rails form helpers

Simple form example:

= simple_form_for(@post) do |f|
    = f.input :address, :input_html =>{:id => 'gmaps-input-address'}, :placeholder => 'Start typing a place...'

Examples

See spec/test-gmaps-auto-coffee.html for an example page using this "plugin". Note that if you set mapElem to null or leave out that element on the page, the autocomplete will function without attempting to update the map :)

This is very useful in scenarios where you want to geolocate the address without displaying on the map.

Advanced Customization

Some of the prime candidate functions for customization are:

Here the default simple updateUI implementation:

updateUi: function( address, latLng ) {
  $(this.inputField).autocomplete("close");
  $(this.inputField).val(address);

  this.positionOutputter(latLng);
}

Let's enrich the autocomplete fields with a jobs count for "the area" for each address.

updateUi: function( address, latLng ) {
  $(this.inputField).autocomplete("close");

  var jobsCount = $.ajax(url: 'jobs/search?address=' + address + '&type=count');

  $(this.inputField).val(address + ' (' + jobsCount + ' jobs)');
}

Note that you can encapsulate this kind of customization using the assist option and an Assist object/class as demonstrated above.

Customizing messages

The following message functions can be customized, either by passing in the options hash, overriding directly on the GmapsCompleter object or even by using the Assist object/class.

Example:


GmapsCompleter.prototype.geocodeErrorMsg = function() {
  "Geocode error!"
}

Here, ensuring that ALL instances of GmapsCompleter will use this functionality as the baseline (since overriding the prototype function).

Localizing messages

For localization/internationalization support, you could customize your Assist object constructor to set the current locale and then have your custom xMsg functions use this locale to display the localized message.

Formtastic example

For formtastic something like:

= semantic_form_for @search do |f|
  = f.input :address, placeholder: 'find address'
  %span#address_error

Or,

<%= semantic_form_for(@search) do |f| %>
    <%= f.input :pickupAddress, :as => :string, :label => "House/Apt Number and Street", :input_html => { :id => "gmaps-input-address", :style => "width:350px; font-size:14px", :placeholder => "Start typing an address or location" } %>
    ...

And matching configuration in your javascript:

$(document).ready(function() { 
  var completer;
  completer = new GmapsCompleter({inputField: 'form#search #address', errorField: 'form#search #address_error'});
  completer.autoCompleteInit({region: 'DK'});
});

Tips

To avoid loading google maps script on all pages, either use turbolinks or alternatively conditionally load it depending on whether the page needs it. For this you could use a simple Page model, something like this:

class Page
  include ::ActiveModel::Model

  attr_accessor :name, :type, :mode

  def map?
    mode && mode.to_s =~ /location/
  end

Then use the Page in the controller

class PropertiesController < BaseController
  def show
    @name = params[:id]
    @mode = params[:mode] || 'gallery'
    @page = Page.new name: :property, mode: @mode
  end
end

Then use page instance to have fine-grained control over how to display the page!

<% if @page.map? %>
  <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js"></script>
<% end %>
<%= javascript_include_tag "application" %>

This could fx be integrated into your page layouts (layout files) or similar.

Alternatively perhaps use RequireJS via the requirejs-rails gem, and load it "AMD" style, and then use a HTML data attribute to set if the page should load the google map script or not. There are many ways to achieve this...

Enjoy!

Troubleshooting

Uncaught ReferenceError: google is not defined

You must remember to include google maps in your html page before gmaps-autocomplete (see application.html.erb above).

<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js"></script>

Dropdown transparency

Some developers have experiences transparency issues for the autocomplete dropdown. This is a CSS issue which can be fixed as noted in #14

.ui-autocomplete {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1000;
  float: left;
  display: none;
  min-width: 160px;
  _width: 160px;
  padding: 4px 0;
  margin: 2px 0 0 0;
  list-style: none;
  background-color: #ffffff;
  border-color: #ccc;
  border-color: rgba(0, 0, 0, 0.2);
  border-style: solid;
  border-width: 1px;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;
  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  -webkit-background-clip: padding-box;
  -moz-background-clip: padding;
  background-clip: padding-box;
  *border-right-width: 2px;
  *border-bottom-width: 2px;

  .ui-menu-item > a.ui-corner-all {
    display: block;
    padding: 3px 15px;
    clear: both;
    font-weight: normal;
    line-height: 18px;
    color: #555555;
    white-space: nowrap;

    &.ui-state-focus, &.ui-state-active {
      color: #ffffff;
      text-decoration: none;
      background-color: #0088cc;
      border-radius: 0px;
      -webkit-border-radius: 0px;
      -moz-border-radius: 0px;
      background-image: none;
    }
  }
}

TODO

Please help out with suggestions and improvements etc!

Contributing to gmaps-autocomplete-rails

Copyright

Copyright (c) 2012 Kristian Mandrup. See LICENSE.txt for further details.